java virtual关键字_谈编程语言互操作-Java/C++互操作机制

1 背景

本文是《谈编程语言互操作-概述》系列的第三篇文章。本篇探讨一下Java/C++(含C)互操作技术的设计原理和实现思路,以便对语言互操作有更深入对认识。Java,C/C++都是目前主流的编程语言,其互操作Java Native Interface (JNI)技术也被大部分程序员熟知。在Android开发中,存在大量的Java/C++语言互操作调用,我们以Android支持的Java语言标准(java8)和运行Java的ART虚拟机为语言实现,进行原理说明。

2 Java/C++影响语言互操作各纬度对比

维度JavaC++影响
对象模型支持单继承,多接口,泛型支持支持多继承,泛型对象模型差异大
内存模型GC手工管理内存内存模型差异大
异常机制默认函数不抛异常,异常模型基于虚拟机实现默认函数抛异常,异常模型实现基于异常表itanium异常模型异常模型差异大
calling conventions基于JVM实现的解释器和编译器决定基于标准体系结构调用规范差异大
并发模型posix线程模型posix线程模型差异小
性能较更互操作开销大
代码执行方式基于虚拟机的解释,JIT和AOT三种模式机器码差异大

2.1 对象模型对比

  • Java VS C++语法/语义主要区别:
  1. 单继承extends <--> 多继承
  2. 多接口实现implements <--> 多继承
  3. 仅允许methods重载 <--> methods和operators都可以被重载
  4. 所有非静态方法缺省都是virtual函数(final除外) <--> 仅virtual修饰的函数
  • Java VS C++运行表示:
  1. 所有对象类型根基类是Object类 <--> 无统一的根基类
  2. 编译后的文件(字节码文件)携带大量的meta信息 <--> 无描述信息或者少量RTTI
  3. 支持反射 <--> 不直接支持反射
  • Android ART虚拟机中一个对象的描述
//ART虚拟机中在运行的时候,每个实例对象都会用Object进行描述
//对应的class描述信息,用Class类描述。
java.lang.Object
art/runtime/mirror/object.h
art/runtime/mirror/object.cc
java.lang.Class
art/runtime/mirror/class.h
art/runtime/mirror/class.cc

可以使用dexdump工具查看每个类的定义及其函数字节码

dexdump -d *.dex
  • C++对象描述
clang -cc1 -fdump-record-layouts *.cpp

...
*** Dumping AST Record Layout
         0 | class Derived
         0 |   class Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int foo
        12 |   int bar
        16 |   int baz
        24 |   struct Point a_point
        24 |     double cx
        32 |     double cy
        40 |   char c
           | [sizeof=48, dsize=41, align=8,
           |  nvsize=41, nvalign=8]

Java和C++对象模型不同,尤其在内存中表示不同,导致Java/C++不能直接访问对方的对象实例。

2.2 内存模型

Java是通过GC回收内存,基本算法就是Mark-Sweep,从Root节点开始,然后遍历所有可以访问的对象,如果对象没用被任何的根节点引用,则可以被回收,C++/C完全是程序员来管理内存。Java回收内存的时机虚拟机控制,程序员并不知道对象被回收的时机,而C++每个对象回收点是固定确定的,例如使用RTTI机制函数自动回收内存。内存回收的确定性是Java/C++互操作的需要关键考虑因素。

2.3 异常机制

C++异常模型在上一篇文章中做了一些介绍,Java的异常模型try/throw/catch/finally在虚拟机中进行实现,在try/throw/catch/finally会转成对应的字节码和Exception table

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.12​docs.oracle.com Chapter 3. Compiling for the Java Virtual Machine​docs.oracle.com
Exception table:
From To Target Type
0 4 5 Class TestExc

Java的异常处理依赖虚拟机的具体实现,本身JVM会为Java执行维护一个managed code stacks,当发生异常的时候(throw的Checked异常或者虚拟机内部出现的Unchecked异常),进行匹配对应的异常表,如果异常表匹配则会找到对应处理块处理函数。

有兴趣的可以去看一下ART虚拟机里面对应的实现,也可以参考《Android虚拟机异常博客文章》。

2.4 调用规范

C/C++调用规范在前面已经说过,JAVA的调用规范完全是虚拟机自己定义的。ART虚拟机在执行的过程中,存在几种执行函数模式之间的转化调用:

  • quick code:编译器编译method产生的机器指令
  • Interpreter:由解释器解释执行 Dalvik 字节码
  • jni method:JNI实现的方法

在不同执行模式切换的时候,调用规范需要进行转化,有兴趣的可以参考代码

  • Interpreter-> Interpreter:ArtInterpreterToInterpreterBridge
  • quick code->quick code:art_quick_invoke_stub或者art_quick_invoke_static_stub
  • quick code->Interpreter:art_quick_to_interpreter_bridge
  • Interpreter->quick code:ArtInterpreterToCompiledCodeBridge
  • quick code/Interprete->Jni method:后面再详细介绍,涉及到Java函数如何准备好参数调用C/C++函数。

2.5 代码执行方式

Java被编译成字节码,Android上被转化为dex文件,依赖ClassLoader机制进行加载,衔接后可以被执行,执行方式包括解释器,JIT及其AOT方式。ART的JIT和AOT都是以method为单元进行的。C/C++直接被编译成机器码,直接被OS(linker)程序进行加载和执行到实际的物理机器上执行。因此Java与C++互操作必须依赖虚拟机进行。

3 Java/C++语言互操作JNI机制使用介绍

3.1 JNI简介

The JNI is a native programming interface. It allows Java code that runs inside a Java Virtual Machine (VM) to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly。

Java Native Interface Specification​docs.oracle.com

JNI规范规定了如何跟C/C++/汇编进行互操作。

3.2 Java调用C和C++

介绍JNI使用的文章非常多,这个地方只示意一下,基本思路就是:

  1. java中使用native关键字标记
  2. 使用工具javah或者手工按照JNI命名规范命名对应函数(dynamic linking需要)
  3. 使用C/C++实现对应的函数即可
https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
给出了一个例子

备注:JNI中有两种将java和native函数进行绑定方式:通过JNI_OnLoad->env->RegisterNatives绑定或者通过Dynamic linkers resolve entries based on their names。

3.3 C/C++调用Java

C/C++调用Java,基本通过env里面提供的一组函数进行完成,学会了这些函数就可以访问Java对象里面的field,method进行各种调用了。

//The JNI functions for accessing instance variable are:
jclass GetObjectClass(JNIEnv *env, jobject obj);
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

//
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
   
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
...

4 Java/C++语言互操作实现原理

如果想搞清楚Java/C++ JNI实现原理,站在虚拟机实现角度,需要去思考几个问题:

  1. Java是如何知道对应C/C++实现的函数在那个so里面?
  2. Java的native函数跟对应C++函数是如何绑定的?
  3. Java方法如何调用到C/C++函数,参数和返回值,异常是如何处理的?
  4. C/C++函数调用Java对象中方法,Field,参数和返回值,异常是如何处理的?
  5. C++/Java对象内存回收机制如何正确保障的

4.1 Java是如何知道对应C/C++实现的函数在那个so里面?

在写JNI相关代码时候,程序员必须写一个类似的代码:

 System.loadLibrary("hello"); 

一切谜语就在这个调用里面:

System.loadLibrary (libcore/ojluni/src/main/java/java/lang/System.java)
 ->Runtime.getRuntime().loadLibrary0(caller, name) (java/lang/Runtime.java)
  ->Runtime.nativeLoad()
-------JNI------------------
    ->Runtime_nativeLoad (ojluni/src/main/native/Runtime.c, libopenjdk.so)
    ->JVM_NativeLoad (art/openjdkjvm/OpenjdkJvm.cc, libopenjdkjvm.so)
    ->vm->LoadNativeLibrary (JavaVMExt::LoadNativeLibrary, jni/java_vm_ext.cc)

在http://java_vm_ext.cc中Libraries和SharedLibrary数据结构,记录所有被加载的so,并且在LoadNativeLibrary中回调"JNI_OnLoad"函数进行初始化,有兴趣的可以跟踪一下代码。

这个里面有个奇怪问题,就是为啥System.loadLibrary可以找到对应的native函数,在虚拟机中针对llibopenjdk.so,在虚拟机创建的时候就会直接去把java基础库libcore的native函数进行动态绑定,给Java一个基础的运行环境,这样就不会出现互相依赖的问题。

Runtime::InitNativeMethods(art/runtime/runtime.cc)

4.2 Java的native函数跟对应C++函数是如何绑定的?

所谓绑定,即调用Java native函数的时候可以找到匹配的C/C++函数。Java中提供了两种不同的绑定方法:

  • JNI_OnLoad/env->RegisterNatives动态注册
// art/runtime/jni/jni_internal.cc
jint RegisterNatives(JNIEnv* env, jclass java_class,
                              const JNINativeMethod* methods,
                              jint method_count) {

1,根据java_class找到匹配的methods里面的名字,找到匹配的java类,可能native在父类
2,找到匹配的ArtMethod m
3,m->RegisterNative(fnPtr);
4,m->SetEntryPointFromJni(new_native_method);
5, SetNativePointer记录C/C++对应的函数指针地址
  • Dynamic linkers resolve动态衔接

动态衔接是在函数真正调用的时候,按需lazy的方式的查找,在调用中会触发查找流程

art_jni_dlsym_lookup_stub
art_quick_generic_jni_trampoline->artQuickGenericJniTrampoline

--> artFindNativeMethod() {
  void* native_code = soa.Vm()->FindCodeForNativeMethod(method);
  ...
  // Register so that future calls don't come here
  return method->RegisterNative(native_code);
}
1, FindCodeForNativeMethod会调用libraries_->FindNativeMethod
2,libraries_即4.1中记录的so列表,根据java method所在的classloader,名字匹配
3,dlsym查找对应的符号是否存在,返回地址
4,调用ArtMethod注册函数。

4.3 Java方法如何调用到C/C++函数

java调用绑定到native函数的C/C++函数,ja执行分两种方式,一种就是解释器执行,另外就是JIT/AOT产生的机器指令代码。

  • 解释器执行调用native函数流程
1,art::interpreter::EnterInterpreterFromInvoke
2,InterpreterJni:Native函数则调用InterpreterJni
3,fntype* const fn = reinterpret_cast<fntype*>(method->GetEntryPointFromJni());
   函数会获得ArtMethod的Jni EntryPoint执行
  • JIT/AOT编译后代码调用native函数流程

JIT调用optimizingCompiler::JitCompile编译一个Native Java方法,会产生对应的stub代码

// Generate the JNI bridge for the given method, general contract:
// - Arguments are in the managed runtime format, either on stack or in
//   registers, a reference to the method object is supplied as part of this
//   convention.
//
1, ArtQuickJniCompileMethod->ArtJniCompileMethodInternal
2, JniCallingConvention根据指令结构选择Arm64JniCallingConvention
3,  Plant call to native code associated with method.
    MemberOffset jni_entrypoint_offset =
    ArtMethod::EntryPointFromJniOffset(InstructionSetPointerSize(instruction_set));

编译器调用ArtJniCompileMethodInternal为每个native java函数产生一段glue代码,按照C/C++调用规范,安装jni_entrypoint_offset 对应的ArtMethod记录的native地址。Java代码调用native函数的时候,基本就是先调用编译产生的jni bridge代码,为调用c/c++函数准备好参数,处理好返回值。这块实现细节比较多,会根据fast,critical不同的标记,产生相应的优化代码,还需要处理好gc,local reference的问题。具体细节看代码吧。

4.4 C/C++函数调用Java对象中方法

这个流程相对来说,不太复杂,以CallVoidMethod实现为例,简单看一下ART虚拟机的实现流程:

art/runtime/jni/jni_internal.cc
CallVoidMethod
 --> InvokeVirtualOrInterfaceWithVarArgs
 --> ArtMethod::Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
  -->art_quick_invoke_stub or art_quick_invoke_static_stub
这两个函数负责参数转换,并调用java函数。

4.5 C++/Java对象内存回收机制如何正确保障的

在JNI中C++对象和Java对象完全分属到两个不同世界

C++申请和访问所有java对象,必须通过Env里面相关函数进行,不能直接访问对应Java对象。C++通过env里面的函数返回是java local reference:soa.AddLocalReference

  1. Reference:可以理解是一个句柄,ART里面是一个IndirectReferfenceTable维护,本身IndirectReferfenceTable也是GC一个Root节点,因此Local表里面的对象是无法回收的,这样可以保证C++持有的对象句柄,不会被释放,除非函数执行完或者显示的调用delete
  2. ScopedObjectAccess:可以保证在调用jni函数的之后,访问管理对象时候调用,一是将jobject加入LocalReference二是线程切换为kRunnable状态,即离开安全区。

Java对象也可以通过long类型地址,持有c++对象的指针,通过提供显示的销毁函数或者实现finalize() 进行生命周期管理,在Android中使用非常多。

4.6 异常和unwind

在JNI中C++异常和Java异常完全分属到两个不同世界。C++异常不影响Java异常,Java异常也不会影响Java,他们的实现方式完全不同,一个依赖elf里面的异常table表,另外依赖java虚拟机实现。

5 业界其他尝试

5.1 Java Native Access (JNA)

JNA​github.com
JNA allows you to call directly into native functions using natural Java method invocation. The Java call looks just like the call does in native code. Most calls require no special handling or configuration; no boilerplate or generated code is required.

提供了一种新的Java到Native调用方式,只需要写Java代码而不用写JNI代码,但是性能可能更低。JNA只能实现Java访问C函数,不能实现C语言调用Java代码。

5.2 bridj

nativelibs4java/BridJ​github.com
960d4ec84681cb9be7059dc872d78bfe.png

a Java / native interoperability library that focuses on speed and ease of use

5.3 JAVA Foreign Function Interface

JEP 191: Foreign Function Interface​openjdk.java.net https://github.com/jnr/jnr-ffi​github.com

一个PR,可以Define a Foreign Function Interface that can bind native functions, such as those found in shared libraries and operating-system kernels, to Java methods, and also directly manage blocks of native memory。

6 总结

总之,Java/C++在对象模型,内存模型,异常机制,代码执行方式上都存在较大的差异。这些因素决定了Java/C++进行互操作,必须要有中转层进行桥接转化,才可以有效的进行互通。目前主流JVM虚拟机都支持JNI规范,用于Java与C++的互操作。

参考文献:

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html​www3.ntu.edu.sg Differences between the C++ and the Java object model​stackoverflow.com
a1e580fa5d2323be3d2c24bb1cdede21.png
https://www.intexsoft.com/blog/post/dvm-vs-art.html​www.intexsoft.com Dumping a C++ object's memory layout with Clang​eli.thegreenplace.net https://source.android.com/devices/tech/dalvik/dex-format​source.android.com Java Memory Model​tutorials.jenkov.com
b3cd28ba8abc4e342a74c6075957f0c2.png
https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973​medium.com https://source.android.com/devices/tech/dalvik/dalvik-bytecode​source.android.com Java 异常表与异常处理原理​blog.liexing.me https://www.oracle.com/technetwork/java/jvmls2013nutter-2013526.pdf​www.oracle.com

书籍:

  • The Java® Language Specification(Java SE 8 Edition)
  • The Java® Virtual Machine Specification(Java SE 8 Edition)
  • The Java® Native Interface Specification
  • Advanced Design and Implementation of Virtual Machine
  • The Garbage Collection Handbook: The Art of Automatic Memory Management
  • Garbage Collection: Algorithms for Automatic Dynamic Memory Management
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值