Java JNI 使用笔记

简介

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, &quot;Courier New&quot;, 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();
}

额外说明:

  1. 动态库需要在 java 启动参数的 -Djava.library.path= 中定义,否则会报动态库找不到的错误
  2. 对于苹果的 M1 电脑,需要额外注意动态库是属于那种架构(aarch64 和 x86_64),需要与使用的 jdk 保持一致

2. generate header

JDK 默认自动的工具可以生成头文件

<pre class="prettyprint hljs ruby" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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 实现:

  1. 数据传输的方式为 JVM 堆外内存(从 Netty 申请的),向 JNI 传递数据 input 和 output 的时候使用 buffer address + length 的方式
  2. JVM 堆外内存本质是需要在 Java 侧进行内存的管理,所以在 JNI 中如果需要进行内存扩容(project 场景下,output 的内存可能需要扩容),会在 C++ 中调用 Java 的 VectorExpander 来进行扩容,保证内存全在 Java 中进行申请管理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值