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 <dir> myjni.HelloJNICpp // 生成 HelloJNICpp.h 文件
Java10中該工具不再提供,改為使用
javac -h <output_dir> myjni\HelloJNI.java
生成樣式如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* 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 <jni.h>
#include <stdio.h>
#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 * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(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讀寫字段值
// <type>包括8種原始類型和object
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(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<fully-qualified-name>;//引用類型 以 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<Primitive-type>Method()
,CallVoidMethod()
,CallObjectMethod()
,對應<Primitive-type>
指的是返回類型,如果有參數,就在后邊加上參數
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 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);
// Invoke an instance method of the object.
// The <type> 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 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);
// Invoke an instance method of the object.
// The <type> includes each of the eight primitive and Object.
//調用超類的方法
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
創建對象和對象數組
在native代碼中構建jobject,jobjectArray
創建對象
獲得構造方法的ID,傳遞函數名為“<init>”,“V”作為返回類型
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, "<init>", "(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<void **> (&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<String> loadedLibraryNames = new Vector<>();
// Native libraries belonging to system classes.
private static Vector<NativeLibrary> systemNativeLibraries
= new Vector<>();
//當前class loader對應的加載的所有Native libraries
private Vector<NativeLibrary> 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
包,內部包含一些平臺常用的庫映射
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
使用方法:
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代碼