简介
JNI 的全称是 Java Native Interface,是一种 Java 的 Native 编程接口,支持 Java 与 C/C++ 直接相互调用,从 JDK 1.0 开始提供。
基本使用流程
通过一个简单的例子来介绍下 JNI 的使用方法,对整体 JNI 有个初步的整体概念。
1. native method
定义一个 Java 类,其中包含 native 方法,另外通过 loadLibrary 来加载动态库。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">package jni;
class JniDemo {
static {
// 这里是加载一个名叫 libjnidemo 的动态库,后缀会根据OS不同而不同
System.loadLibrary("jnidemo");
}
public native void nativeMethod();
}
额外说明:
- 动态库需要在 java 启动参数的 -Djava.library.path= 中定义,否则会报动态库找不到的错误
- 对于苹果的 M1 电脑,需要额外注意动态库是属于那种架构(aarch64 和 x86_64),需要与使用的 jdk 保持一致
2. generate header
JDK 默认自动的工具可以生成头文件
<pre class="prettyprint hljs ruby" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"># 1\. javac -h:java 编译为 class,并且生成头文件
${JAVA_HOME}/bin/javac -h . jni/JniDemo.java
# 2\. javah:需要在 classpath 下找到已经编译好的 class
${JAVA_HOME}/javah jni.JniDemo
生成的头文件示例如下:
<pre class="prettyprint hljs cpp" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// filename: jni_JniDemo.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_JniDemo */
#ifndef _Included_jni_JniDemo
#define _Included_jni_JniDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_JniDemo
* Method: nativeMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_jni_JniDemo_nativeMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
3. implementation
引入头文件,实现其对应的函数
<pre class="prettyprint hljs cpp" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// filename: JniDemo.cc
#include "jni_JniDemo.h"
JNIEXPORT void JNICALL Java_jni_JniDemo_nativeMethod
(JNIEnv *env, jobject obj) {
printf("native method in jni\n");
}
4. compile
编译只需要依赖 jni.h 的头文件,还有一个与OS相关的头文件
这里编译的动态库的名字,与 Java 代码中的 System.loadLibrary 的动态库名字相关,额外多一个 lib 的前缀。
<pre class="prettyprint hljs ruby" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"># for mac os
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin JniDemo.cc -o JniDemo.o
g++ -shared -fPIC -o libjnidemo.dylib JniDemo.o -lc
# for linux
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux JniDemo.cc -o JniDemo.o
g++ -dynamiclib -o libjnidemo.so JniDemo.o -lc
5. run
执行的时候只需要指定 java.library.path,在这个路径下有 libjnidemo 动态库即可
<pre class="prettyprint hljs nginx" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"># run
java -cp . -Djava.library.path=/path/to/libjnidemo jni.JniDemo
JNI Types
Primitive Types
Java 的 Primitive Types 与 JNI 中的 Native 多有对应的类型,都是以 j 开头。
这里在 Java 中定义 native 方法,其参数的类型都会被转换为对应的 native 类型。
Primitive Types and Native Equivalents
array 数组的基础类型是独立的类型,从这里就可以理解为什么说 Java 中的基础类型和基础类型的数组是两个类型,因为底层实现确实是两个。
比较特殊的是 String:有 jstring 的对应实现,但是其不属于 primitive type,属于 jobject,并且也没有单独的 array 实现。
jobject:
- jclass (java.lang.Class objects)
- jstring (java.lang.String objects)
- jarray (arrays)
- jobjectArray (object arrays)
- jbooleanArray (boolean arrays)
- jbyteArray (byte arrays)
- jcharArray (char arrays)
- jshortArray (short arrays)
- jintArray (int arrays)
- jlongArray (long arrays)
- jfloatArray (float arrays)
- jdoubleArray (double arrays)
- jthrowable (java.lang.Throwable objects)
Java VM Type Signatures
Java 在底层实现来一套 Type Signatures,通过这套签名体系来表示 field 的类型、method 的签名、array 等。
在 JNI 获取 field 和 method 等处均需要使用 Type Signature。这里的 Type Signature 是 Java 单独定义的,需要根据下面的定义来使用。
Java VM Type Signatures 的表格如下:
这里简单举例说一下其使用方法,假设有一个 Java class 的定义如下:
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public class SimpleData {
public boolean aBoolean;
public String aString;
}
这个类在 jni 中读取其中的字段,就需要使用对应的 type signature,读取上述 class 的 field 方法如下:
<pre class="prettyprint hljs elixir" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">jclass kDummyDataClass = env->FindClass("LSimpleData;");
jfieldID data_aBoolean_ = env->GetFieldID(kDummyDataClass, "aBoolean", "Z");
jfieldID data_aString_ = env->GetFieldID(kDummyDataClass, "aString", "Ljava/lang/String;");
// 假设当前已经有 jobject dataObject,其对应的 java class 为 SimpleData
jboolean aBoolean = env->GetBooleanField(dataObject, data_aBoolean_);
// jstring 有单独的实现,但是其本质也是一个 jobject
jstring aString = (jstring) env->GetObjectField(dataObject, data_aString_);
CMake 编译
这里的编译方法可以参考 Arrow 的 c data api和 gandiva的编译。
下面是一个 CMake 的示例,通过 add_jar 来生成 jni 的头文件。
<pre class="prettyprint hljs cmake" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"># Find java/jni
include(UseJava) # for add_jar
find_package(Java REQUIRED)
find_package(JNI REQUIRED)
# add_jar DESTINATION:生成的 java native method 的头文件位置
set(JNI_HEADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
# 设置 jni 的头文件
include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${JNI_INCLUDE_DIRS} ${JNI_HEADERS_DIR})
add_jar(${PROJECT_NAME}
src/main/java/jni/JniException.java
src/main/java/jni/JniWrapper.java
GENERATE_NATIVE_HEADERS
hello_jni-native
DESTINATION
${JNI_HEADERS_DIR})
set(SOURCES src/main/cpp/jni_wrapper.cc)
add_library(hello_jni SHARED ${SOURCES})
target_link_libraries(hello_jni ${JAVA_JVM_LIBRARY})
C/C++ API 差异
API 的差异主要是因为 C++ 支持 class(struct 与 class 的差异仅仅是可见性),C 只能用 struct + function pointer 来模拟。
jni.h 中,对 JNIEnv 的定义如下:
<pre class="prettyprint hljs cpp" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
虽然同为 JNIEnv,但是其内部的实现是不同的,查看对应类即可知道 C/C++ API 的差异,这里以 GetStringUTFChars 为例举例说明其 API 的差异:
<pre class="prettyprint hljs rust" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// c api
const char *str = (*env)->GetStringUTFChars(env, jstr, 0);
// c++ api
const char *str = env->GetStringUTFChars(jstr, 0);
方法的名字都是一样的,C/C++ API 下的函数是一样的,仅使用方法不同。
Arrow JNI
JNI 动态库的编译都是使用 CMake 来进行编译,JniLoader 用于加载动态库,JniWrapper 作为 native method 的类。
下面以 Arrow Gandiva 为例,简单分析下其 JNI 实现:
- 数据传输的方式为 JVM 堆外内存(从 Netty 申请的),向 JNI 传递数据 input 和 output 的时候使用 buffer address + length 的方式
- JVM 堆外内存本质是需要在 Java 侧进行内存的管理,所以在 JNI 中如果需要进行内存扩容(project 场景下,output 的内存可能需要扩容),会在 C++ 中调用 Java 的 VectorExpander 来进行扩容,保证内存全在 Java 中进行申请管理