Java Native Interface (JNI)是一个本地编程接口,可以让Java代码使用以其他语言(C/C++) 编写的代码和代码库。
编写Java代码
package myjni;
public class HelloJNI {
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// Declare a native method sayHello() that receives nothing and returns void
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method sayHello()
}
}
- 静态初始化块加载本地库,这个库应该被包含在Java库路径中(java.library.path 变量),否则会产生
UnsatisfiedLinkError
通过System.getProperty("java.library.path")
可以查询对应的值,在执行程序的时候可以通过VM参数-Djava.library.path=path_to_lib
显示指定路径 - native 关键字声明方法,表明使用另外一种语言实现的
在实际使用中,.so文件会被放入到
resource
目录下,通过Class.getResource
或者ClassLoader.getResource
获取资源,存储到临时文件中,再加载该临时文件,而不用通过Djava.library.path
显示指定路径
编译java代码
成字节码 "HelloJNI.java" -> "HelloJNI.class"
> javac myjni\HelloJNI.java
or
> javac -d. myjni\HelloJNI.java //-d 指定放置生成类文件的位置,必要时创建包名对应的目录
创建 C/C++ 头文件
创建一个定义native函数签名的C/ C++头文件
通过使用JDK附带javah
工具创建一个头文件,它可以生成包含class文件中native方法函数声明的头文件
// 如果使用的是IDEA 先cd project/build/class/main
javah -d myjni.HelloJNICpp // 生成 HelloJNICpp.h 文件
Java10中该工具不再提供,改为使用
javac -h myjni\HelloJNI.java
生成样式如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
头文件声明了一个C函数
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
C函数的命名惯例是
Java_{package_and_classname}_{function_name}(JNI arguments)
,包名称中的点用下划线替代,函数默认先包含两个参数:
-
JNIEnv*
: JNI environment 的引用(jni.h中定义),相当于一个函数表指针,用来访问所有的JNI函数 -
jobject
: Java 该对象引用,相当于“this”
宏定义JNIEXPORT 和 JNICALL 是对编译器说明信息,用来做出口函数
extern "C" 只会被C++编译器识别,表明使用C的函数命名协议
C/C++代码
C代码如下:
#include
#include
#include "HelloJNI.h"
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
Ubuntu下的编译C代码,如果是C++需要使用g++编译
gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
- -fPIC则表明使用地址无关代码,PIC:Position Independent Code
- -I 指定头文件,例如
jni.h
运行Java程序
显示指定.so文件
> java -Djava.library.path=. myjni.HelloJNI
JNI基础
- Java基本类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分别对应
int, byte, short, long, float, double, char, boolean - Java引用类型:jobject 应用对应于
java.lang.Object
,它同样定义了众多子类型
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
C/C++方法收到的是JNI类型,返回值也是JNI类型(例如jstring
, jintArray
),但是程序内部使用的是C/C++类型,所以中间需要JNI类型与C/C++类型的相互转换
C/C++程序步骤:
- 接收JNI类型的参数(由Java程序传递来)
- 对于引用类型(
jObject
),将参数转换或复制为本地类型,例如将jstring转换为C字符串,将jintArray转换为C的int []等;对于原始JNI类型,如jint和jdouble不需要转换,可以直接操作 - 以本地类型执行操作
- 创建JNI类型的返回对象,并将结果复制到返回对象中
JNI编程中关键点是JNI引用类型(如jstring,jobject,jintArray,jobjectArray)和本机类型(C-string,int [])之间的转换。 JNI接口提供了许多函数来进行这样的转换
JNI是一个C接口,它不是面向对象的,所以没有真正传递对象
JNIEnv函数调用方法
通过JNIEnv可以调用众多函数,内部实际上都是JNINativeInterface_
结构体内部的函数指针
struct JNINativeInterface_;
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
类型 | 语法 |
---|---|
C | (*env)->FunctionName(env, parameter) |
C++ | env->FunctionName(parameter) //使用了内联函数 |
下面的例子中多使用C语言的调用方式
传递原始类型
可以直接使用强制类型转换()
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
传递字符串
Java的字符串是16位Unicode字符序列,C的字符串是char型数组,以空字符结尾
JNIEnv提供了转换的函数
UTF-8(1到3个字节) <--> Unicode(2个字节)
//If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
//将jstring转换成为Unicode格式的char*
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
//告诉VM native 代码不在访问utf
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
//释放指向Unicode格式的char*的指针
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
//创建一个java.lang.String 对象
jstring NewStringUTF(JNIEnv *env, const char *bytes);
//创建一个Unicode格式的String对象,从Unicode字符
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
//获取UTF-8形式的串长度
jsize GetStringUTFLength(JNIEnv *env, jstring string);
//获取Unicode格式的的长度
jsize GetStringLength(JNIEnv *env, jstring string);
//把一段区域中的String,转为UTF-8,放入buf
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
//复制一段区域中的String,放入buf
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
GetStringChars
: jstring -> char* 如果内存分配失败,返回NULL,第三个参数如果不为NULL,当返回字符串是原始string的复制时,被置为JNI_TRUE;
当直接指向初始的String类型时,对应为JNI_FALSE ,jni在运行时会尽可能执行这种情况,这时本地程序如果修改字符数组的内容,对应Java程序的字符串会发生改变
传递原始类型的数组
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * GetArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void ReleaseArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void GetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void SetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType NewArray(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
jsize GetArrayLength(JNIEnv *env, jarray array);//获取数组的长度
访问对象的变量和方法
访问实例对象的变量和类的静态变量
jclass GetObjectClass(JNIEnv *env, jobject obj);
// Returns the class of an object.
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the field ID for an instance variable of a class.
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the field ID for a static variable of a class.
// 通过jfieldID读写字段值
// 包括8种原始类型和object
NativeType GetField(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetField(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
NativeType GetStaticField(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStaticField(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
GetFieldID中sig表示签名,编码形式如下
类型 | sig |
---|---|
boolen | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
class | L |
方法签名主要用在重载方法的说明,签名的形式是“(parameters)return-type”
javap -s -p 类名称
查看签名
javap 是java类文件的反编译器
-s :输出内部类型签名
-p:显示私有成员,默认只打印public的签名信息
例子:设定一个long变量
static jlong setField_long(JNIEnv *env, jobject java_obj, const char *field_name, jlong val) {
jclass clazz = env->GetObjectClass(java_obj);
jfieldID field = env->GetFieldID(clazz, field_name, "J");
if (field)
env->SetLongField(java_obj, field, val);
else {
LOGE("__setField_long:field '%s' not found", field_name);
}
return val;
}
调用实例对象的方法和静态方法
- 获取这个类对象的引用
GetObjectClass()
- 获取方法ID
GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig)
需要提供方法名和对应的签名 - 基于方法ID,
Call
,Method() CallVoidMethod()
,CallObjectMethod()
,对应
指的是返回类型,如果有参数,就在后边加上参数
JNI中对应的函数:
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the method ID for an instance method of a class or interface.
NativeType CallMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType CallMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType CallMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
// Invoke an instance method of the object.
// The includes each of the eight primitive and Object.
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the method ID for an instance method of a class or interface.
NativeType CallStaticMethod(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStaticMethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStaticMethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
// Invoke an instance method of the object.
// The includes each of the eight primitive and Object.
//调用超类的方法
NativeType CallNonvirtualMethod(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtualMethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtualMethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
创建对象和对象数组
在native代码中构建jobject,jobjectArray
创建对象
获得构造方法的ID,传递函数名为“
jclass FindClass(JNIEnv *env, const char *name);
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
// Constructs a new Java object. The method ID indicates which constructor method to invoke
jobject AllocObject(JNIEnv *env, jclass cls);
// Allocates a new Java object without invoking any of the constructors for the object.
对象数组
- FindClass(env, "java/lang/Double"); 找到对应的类
- NewObjectArray(env, 2, classDouble, NULL); 创建对应的数组
- GetMethodID(env, classDouble, "
", "(D)V"); 找到类的构造方法 - NewObject(env, classDouble, midDoubleInit, average); 创建该类
- SetObjectArrayElement(env, outJNIArray, 0, objSum); 存到数组对应的位置上
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
// Constructs a new array holding objects in class elementClass.
// All elements are initially set to initialElement.
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
// Returns an element of an Object array.
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
// Sets an element of an Object array.
本地和全局引用
JNI将native代码中的对象引用分为两类
- 本地引用
local reference
:native方法中创建的都是本地引用,方法结束后就回收,可以通过DeleteLocalRef()
显式的使本地引用无效,使得JVM尽快进行GC,同时传递到native方法的参数对象,jni方法返回的Java对象都是本地引用 - 全局引用
global reference
:通过jobject NewGlobalRef() (JNIEnv *env, jobject lobj);
获取,然后通过void DeleteGlobalRef(JNIEnv *env, jobject gref);
显示的释放
所以对全局变量赋值时,先要将本地引用转为全局引用,赋值后再释放本地引用
实际上本地引用并不是native方法里的局部变量,局部变量存放在堆栈中,而Local Reference存放在Local Reference Table中,而该Table的容量是有限的,虽然在native Method结束时,JVM会自动释放Local Reference,但在使用中,要及时使用DeleteLocalRef()
删除不必要的Local Reference,避免Local Reference Table溢出
动态加载
VM虚拟机会在native库加载的时候调用JNI_OnLoad
,例如通过System.loadLibrary
加载的时候
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
静态注册本地方法的弊端
- 需要编译所有声明了native方法的Java类,每个所生成的class文件都得用javah生成一个头文件
- javah生成的JNI层函数名特别长,书写起来很不方便。
- 初次调用native函数时要根据函数名字搜索对应用JNI层函数来建立关联关系,这样会影响运行效率。
动态注册核心是JNINativeMethod
,把Java中的方法和本地的方法关联起来
typedef struct {
char *name; //Java中原生方法的名称
char *signature;//方法签名,是参数类型和返回值类型的组合
void *fnPtr;//函数指针,
} JNINativeMethod;
看了其他的教程,发现都是固定的套路,C++代码如下:
static JNINativeMethod gMethods[] = {
{"name", "signature", fnptr},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env = NULL;
jint result = JNI_FALSE;
//获取env指针
if (jvm->GetEnv(reinterpret_cast (&env), JNI_VERSION_1_6.) != JNI_OK) {
return result;
}
if (env == NULL) {
return result;
}
//获取类引用,写类的全路径(包名+类名
jclass clazz = env->FindClass("***/***/JNIDynamicUtils");
if (clazz == NULL) {
return result;
}
//注册方法
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
return result;
}
//成功
result = JNI_VERSION_1_6;
return result;
}
在JNI_OnLoad中,可以保存JavaVM
的指针,这是跨线程的,持久有效的变量
多线程
JNIEnv只在当前线程有效,如果C/C++中创建的新线程需要访问Java VM,先调用AttachCurrentThread
方法把自己附着到VM并且获得env
JavaVM *vm;
JNIEnv *env;
vm->AttachCurrentThread(&env, NULL);
vm->DetachCurrentThread();//卸载
多Java线程加载同一个lib,或者单一线程加载多次,实际上都不会进行处理,因为ClassLoader
内部静态字段会记录进程加载的所有共享文件,首先会检查该ClassLoad是否加载过,不会进行重复加载,如果其他ClassLoad加载过,也不会进行加载,多个线程调用同一个so文件的不同函数,共享一套全局变量,因为共享库在进程中只有一套数据
ClassLoad中关于加载native library的核心字段:
// 所有加载的native library名称
private static Vector loadedLibraryNames = new Vector<>();
// Native libraries belonging to system classes.
private static Vector systemNativeLibraries
= new Vector<>();
//当前class loader对应的加载的所有Native libraries
private Vector nativeLibraries = new Vector<>();
// The paths searched for libraries
private static String usr_paths[];
private static String sys_paths[];
底层具体通过dlopen
库函数加载:
jdk/src/share/native/java/lang/ClassLoader.c#Java_java_lang_ClassLoader_00024NativeLibrary_load
src/share/vm/prims/jvm.cpp#JVM_LoadLibrary
/src/os/linux/vm/os_linux.cpp#dll_load#os::Linux::dlopen_helper#dlopen(filename, RTLD_LAZY)
JNA
JNA(Java Native Access)是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,建立在JNI基上的一个框架。JNA简化了调用本地方法的过程,可以直接调用本地共享库中的函数,不需要写Java代码之外的程序
需要添加依赖的jar包jna.jar
到CLASSPATH,也可以再添加jna-platform.jar
包,内部包含一些平台常用的库映射
net.java.dev.jna
jna
使用方法:
package com.sun.jna.examples;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {
// This is the standard, stable way of mapping, which supports extensive
// customization and mapping of Java to native types.
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)
Native.load((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);
void printf(String format, Object... args);
}
public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
}
}
- 一个接口类映射一个要加载的库,该类需要扩展自
Library
,接口内定义需要使用的本地库方法 - 接口内部需要一个公共静态常量:
INSTANCE
,通过这个常量,就可以获得这个接口的实例,从而使用接口的方法,JNA内部通过代理模式,先对数据类型进行转换,调用外部dll/so的函数 - 可以将本地库路径设定到
jna.library.path
核心是不同平台数据类型的转变:
同JNI一样,Java基本数据类型可以直接映射
数组类型:
// Original C declarations
void fill_buffer(int *buf, int len);
void fill_buffer(int buf[], int len); // same thing with array syntax
// Equivalent JNA mapping
void fill_buffer(int[] buf, int len);
结构体对应:定义一个继承Structure
的 public static
类,用来作为参数或返回值类型,类中的公共字段的顺序,必须与C 语言中的结构的顺序一致,同时定义getFieldOrder()
方法,返回有序的字段名称
Java 调用动态链接库中的C 函数,实际上就是一段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C 语言传过来的参数。同时,C 语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA 中模拟的结构体的变量顺序绝对不能错
JNA是不能完全替代JNI的,因为NA只能实现Java访问C函数,使用JNI技术,不仅可以实现Java访问C函数,也可以实现C语言调用Java代码
Reference
- Java Programming Tutorial Java Native Interface (JNI)
- Java programming with JNI
- JNA