项目地址:
Jvm在启动一个程序的时候,会涉及到非常多的内容,其中就有一些JNI方法。拿 System.out 来说,这个变量是 final 修饰的,按照一般情况来说,我们是无法更改这个变量的值的:
java
public final static PrintStream out = null;
但是 System 类提供了一个 setOut 方法:
scss
public static void setOut(PrintStream out) {
checkIO();
setOut0(out);
}
可以看出是使用了 native 方法来实现变量的赋值。
我们之前一直是使用 hack 的方式来打印输出:
scss
if ("println".equals(methodRef.getName())) {
print(methodRef, operandStack);
operandStack.popRef();
return;
}
如果想真正的实现 System.out.println 方法,我们就需要实现这些JNI方法。
本地方法实现
本地方法在class文件中没有Code属性,所以需要主动给maxStack和maxLocals字段赋值。
本地方法帧的操作数栈至少要能容纳返回值,为了简化代码,暂时给maxStack字段赋值为4。
因为本地方法帧的局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals字段刚好。
至于code字段,也就是本地方法的字节码,它本是没有字节的,但是我们需要模拟本地方法的调用,所以就自己设定一个操作符 0xFE,它表示 invokenative,返回指令则根据函数的返回值选择相应的返回指令。
我们在创建Method的时候,判断:
less
if (method.isNative()) {
method.injectCodeAttribute(methodDescriptor.getReturnType());
}
injectCodeAttribute 里面就加入两条指令:
arduino
private void injectCodeAttribute(String returnType) {
this.maxStack = 4;
this.maxLocals = argsSlotCount;
switch (returnType.charAt(0)) {
case 'V':
this.code = new byte[]{(byte) 0xfe, (byte) 0xb1}; // return
return;
case 'D':
this.code = new byte[]{(byte) 0xfe, (byte) 0xaf}; // dreturn
return;
case 'F':
this.code = new byte[]{(byte) 0xfe, (byte) 0xae}; // freturn
return;
case 'J':
this.code = new byte[]{(byte) 0xfe, (byte) 0xad}; // lreturn
return;
case 'L':
case '[':
this.code = new byte[]{(byte) 0xfe, (byte) 0xb0}; // areturn
return;
default:
this.code = new byte[]{(byte) 0xfe, (byte) 0xac}; // ireturn
}
}
这样我们的 NativeMethod 就创建好了,但是还有一个问题,就是这个 code 里面没有逻辑,我们该如何实现该方法的具体逻辑呢?
本地方法的实现只能靠自己去翻译JNI代码。拿 System 类来说,它的 static 代码段里面就有一个 native 方法:
csharp
private static native void registerNatives();
static {
registerNatives();
}
我们实现一个本地方法注册表:
csharp
public static void init() {
NativeRegistry.getInstance().registerNativeMethod(
"java/lang/System",
"arraycopy",
"(Ljava/lang/Object;ILjava/lang/Object;II)V",
new ArrayCopy());
NativeRegistry.getInstance().registerNativeMethod(
"java/lang/System",
"initProperties",
"(Ljava/util/Properties;)Ljava/util/Properties;",
new InitProperties());
NativeRegistry.getInstance().registerNativeMethod(
"java/lang/System",
"registerNatives",
"()V",
new EmptyNativeMethod());
}
注册的时候需要传递一个实现了 NativeMethod 接口的类,这个类里面就是具体的实现逻辑:
ini
public static class ArrayCopy implements NativeMethod {
@Override
public void invoke(StackFrame frame) {
LocalVariableTable localVariableTable = frame.getLocalVariableTable();
ArrayObject src = (ArrayObject) localVariableTable.getRef(0);
int srcPos = localVariableTable.getInt(1);
ArrayObject dst = (ArrayObject) localVariableTable.getRef(2);
int dstPos = localVariableTable.getInt(3);
int length = localVariableTable.getInt(4);
if (src == null || dst == null) {
throw new MyJvmException("java.lang.NullPointerException");
}
if (!checkArrayCopy(src, dst)) {
throw new MyJvmException("java.lang.ArrayStoreException");
}
if (srcPos < 0 || dstPos < 0 || srcPos + length > src.getArrayLength()
|| dstPos + length > dst.getArrayLength()) {
throw new MyJvmException("java.lang.IndexOutOfBoundsException");
}
for (int i = 0; i < length; i++) {
dst.setArrayElement(dstPos + i, src.getArrayElement(srcPos + i));
}
}
当执行到 InvokeNative 指令的时候,我们根据方法签名去匹配本地方法:
ini
@Override
public void execute(StackFrame frame) {
MyMethod myMethod = frame.getMyMethod();
String thisClassName = myMethod.getMyClass().getThisClassName();
String methodName = myMethod.getName();
String methodDescriptor = myMethod.getDescriptor();
NativeMethod nativeMethod = NativeRegistry.getInstance().findNativeMethod(thisClassName, methodName, methodDescriptor);
if (nativeMethod == null) {
String methodInfo = thisClassName + "." + methodName + methodDescriptor;
throw new MyJvmException("java.lang.UnsatisfiedLinkError: " + methodInfo);
}
nativeMethod.invoke(frame);
}
这样,本地方法就搞定了。
但是很蛋疼的是,JVM在启动的时候会调用到很多的本地方法。所以实现起来特别的累,到现在项目里面还有很多本地方法没有实现,后面才会慢慢补上。不过话说回来,实现这些本地方法对理解项目也没多少帮助是,属于吃力不讨好。
反射实现
前面在实现运行时数据区的时候,创建了两个类:MyClass 和 MyObject。
对于 String 的字节码文件来说,MyClass 表示的是String.class。MyObject 表示的是一个 String 对象。
但是对于 Class 的字节码来说,MyClass 表是的是 Class.class 。MyObject 表示的是 String.class 或者其他的 clas。
嗯,可能比较难理解。没关系,多看几遍就自然懂了。
为了记录 MyObject 与 MyClass 的关系,我们使用 extra 字段来建立联系。
每次MyClassLoader加载类的时候,我们都建立如下联系:
ini
MyClass classValue = loadedClasses.get("java/lang/Class");
if (classValue != null) {
MyObject classObject = classValue.newObject();
classObject.setExtra(loadedClass);
loadedClass.setJClass(classObject);
}
这样,就可以正确的运行 String.class 这样的代码了。
代码测试
csharp
public static void main(String[] args) {
System.out.println(void.class.getName()); // void
System.out.println(boolean.class.getName()); // boolean
System.out.println(byte.class.getName()); // byte
System.out.println(char.class.getName()); // char
System.out.println(short.class.getName()); // short
System.out.println(int.class.getName()); // int
System.out.println(long.class.getName()); // long
System.out.println(float.class.getName()); // float
System.out.println(double.class.getName()); // double
System.out.println(Object.class.getName()); // java.lang.Object
System.out.println(int[].class.getName()); // [I
System.out.println(int[][].class.getName()); // [[I
System.out.println(Object[].class.getName()); // [Ljava.lang.Object;
System.out.println(Object[][].class.getName()); // [[Ljava.lang.Object;
System.out.println(Runnable.class.getName()); // java.lang.Runnable
System.out.println("abc".getClass().getName()); // java.lang.String
System.out.println(double[].class.getName()); // [D
System.out.println(String[].class.getName()); //[Ljava.lang.String;
}
运行配置:
运行结果: