Java虚拟机之本地方法调用实现

Java虚拟机规范对本地方法和本地方法栈的实现要求是非常宽松的,甚至明确说明“如果Java虚拟机不支持native方法,自己也不依赖传统栈(通常指“C Stacks”)的话,可以无需支持本地方法栈”。商业级别的HotSpot将虚拟机栈和本地方法栈合并,并采用JNI规范来实现本地方法。而本文也采用虚拟机栈和本地方法栈合并的模式来模仿编写。

很容易想到,要调用本地方法,可以采用一个Map来做映射:

1
2
3
4
5
// 一一映射
public static void registerMethod(String className, String methodName, String methodDescriptor, NativeMethod nativeMethod) {
String key = className + "~" + methodName + "~" + methodDescriptor;
register.put(key, nativeMethod);
}

暂时将registerNatives()设置为空实现。

本地方法的调用

本地方法调用还是在Java方法的架构下进行,可以是静态方法也可以是非静态方法,修改方法调用的公有逻辑MethodInvokeLogic(),将以前的钩子处理(用来忽略本地方法registerNatives,遇到该方法就将方法从虚拟机栈弹出)注释掉。

接着修改Method_的构造方法,思路无非就是如果这个方法是Native的,那么就填充一些参数(设定maxStack和maxLocals,注入code等),让原本的框架仍然“认为”它就是一个普通的方法,而在执行的时候真正去执行本地方法,执行完后,还要通过返回指令回到框架。

根据上面的思路,首先修改Method_构造方法,在末尾添加:

1
2
3
4
5
6
7
8
9
10
11
// 判断方法是否是本地方法,如果是,那么求出代表返回值的描述符片段,
// 通过该片段来判断返回字节码指令的类型
if (this.isNative()) {
// 获得方法的描述符
String des = this.getDescriptor();
Pattern pattern = Pattern.compile("(?=\\().*(?<=\\))");
Matcher matcher = pattern.matcher(des);
// 得到代表返回值的描述符片段
String returnDes = matcher.replaceAll("");
this.injectCodeAttribute(returnDes);
}

然后实现injectCodeAttribute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 注入字节码,为调用本地方法服务
private void injectCodeAttribute(String returnType) {
// 幸运数字7
this.maxStack = 7;
this.maxLocals = this.getArgSlotCount();
// 第二个字节码指令用于执行本地方法后的返回
switch (returnType.charAt(0)) {
case 'V':
this.code = new int[]{0xfe, 0xb1};
break;
case 'D':
this.code = new int[]{0xfe, 0xaf};
break;
case 'F':
this.code = new int[]{0xfe, 0xae};
break;
case 'J':
this.code = new int[]{0xfe, 0xad};
break;
case 'L':
case '[':
this.code = new int[]{0xfe, 0xb0};
break;
default:
this.code = new int[]{0xfe, 0xac};
break;
}
}

这里用到了Java虚拟机预留的字节码指令0xfe,当虚拟机遇到该指令时,就会执行指令背后的相关逻辑,也就是如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class INVOKE_NATIVE extends NoOperandsInstruction {
@Override
public void execute(StackFrame_ frame) {
Method_ method = frame.getMethod_();
String className = method.getClass_().getThisClassName();
String methodName = method.getName();
String methodDescriptor = method.getDescriptor();
// 查找相应的本地方法
NativeMethod nativeMethod = Registry.findNativeMethod(className, methodName, methodDescriptor);
// 如果返回的是null,那么抛出一个带有信息的运行时异常
if (nativeMethod == null) {
String methodInfo = className + "." + methodName + methodDescriptor;
throw new RuntimeException(methodInfo);
}
// 执行本地方法
nativeMethod.execute(frame);
}
}

最后要做的是在字节码指令工厂内对0xfe进行注册。

完成了上面的步骤,接下来只需要添加本地方法的实现就可以了。

如果想知道运行当前项目会调用几次registerNatives方法,可以在registerNatives的方法体添加打印语句,具体处理如下:

1
2
3
4
5
6
if (methodDescriptor.equals("()V") && methodName.equals("registerNatives")) {
return frame -> {
// do nothing
System.out.println(frame.getMethod_().getClass_().getThisClassName());
};
}

会打印出:

1
2
java/lang/Object
java/lang/System

这两个类都拥有registerNatives方法,在static语句块内会先运行registerNatives(),输出打印语句。

不知不觉已经完成了一次本地方法的调用。

实现hashCode()

本地方法的实现都大同小异,这里列举如何实现Object类的hashCode方法。

首先进入Object.java的源代码有:

1
public native int hashCode();

然后在OpenJDK里查询Object.c有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}

按照这个结构仿写。

JObject & JSystem

这里的JObject和JSystem对应Object.c和System.c。JObject代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JObject {
private static JNINativeMethod[] methods = {
new JNINativeMethod("hashCode", "()I", new JVM_IHashCode()),
new JNINativeMethod("wait", "(J)V", new JVM_MonitorWait()),
new JNINativeMethod("notify", "()V", new JVM_MonitorNotify()),
new JNINativeMethod("notifyAll", "()V", new JVM_MonitorNotifyAll()),
new JNINativeMethod("clone", "()Ljava/lang/Object;", new JVM_Clone()),
};
// 传入className,对本地方法进行注册
public static void Java_java_lang_Object_registerNatives(String className) {
for (JNINativeMethod jniNativeMethod : methods) {
Registry.registerMethod(className, jniNativeMethod.getMethodName()
, jniNativeMethod.getMethodDescriptor(), jniNativeMethod.getNativeMethod());
}
}
}

JNINativeMethod类用来存储参数,这些参数可以用来注册本地方法。而本地方法都声明在JVM_ENTRY接口中。

接下来需要修改Registry类,处理查找到registerNatives方法时的逻辑。前面已经提到了,在Java中Object和System都拥有registerNatives方法用来注册本地方法。当查找到registerNatives方法后,应该确定是哪一个类的registerNatives(),具体代码见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String[] temp = className.split("/");
String string = "";
for (int i = 0; i < temp.length - 1; i++) {
string = string + temp[i] + ".";
}
// 将原本className的最后的简单名前添加一个“J”,在className前添加“native_.”
String name = "native_." + string + "J" + temp[temp.length - 1];
try {
Class<?> c = Class.forName(name);
for (Method method : c.getMethods()) {
// 方法名以"registerNatives"结尾时,为要寻找的方法
if (method.getName().endsWith("registerNatives"))
// 静态方法,所以第一个参数为null
method.invoke(null, className);
}
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}

一些核心代码的理解见注释。同理可以修改JSystem.java中的代码。

完成JVM_IHashCode

接下来只要完成JVM_ENTRY下的JVM_IHashCode类,就能实现调用本地方法的功能,下面给出代码:

1
2
3
4
5
6
7
8
9
10
class JVM_IHashCode implements NativeMethod {
@Override
public void execute(StackFrame_ frame) {
Instance_ thisRef = frame.getLocalVars().getRef(0);
// 本地方法的调用
int hashCode = thisRef.hashCode();
frame.getOperandStack().pushInt(hashCode);
}
}

代码中没有自己去实现hashCode的计算,而是直接采用的虚拟机编写语言Java的API,这样就完成了本地方法调用。

小结

要完成更多本地方法的调用,就需要先行完成库的编写。本篇旨在了解基本原理,实现的过程中借用了0xfe这个保留的字节码来触发执行本地方法的逻辑,也可以采用其它方式。而如果要探究商业级虚拟机本地方法调用的过程,可以参考相关技术规范。

参考

JNI技术规范:JNI 6.0官方技术文档翻译

向JVM注册本地方法是怎么实现的