java JNI介紹

JNI是Java Native Interface的全稱。

oracle文件中是這樣描述的

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.

翻譯過來就是 JNI 是本機程式設計介面。 它允許在 Java 虛擬機器 (VM) 內執行的 Java 程式碼與使用其他程式語言(例如 C、C++ 和彙編)編寫的應用程式和庫進行互操作。

說白了就是java程式碼和其他程式語言的程式碼相互呼叫。

今天就主要記錄下日常的一些使用方式

1、 Java呼叫C++程式碼

  1. 首先建立我們的java類 JNIDemo.java

    package com.wbo112.jni;
    
    import java.util.*;
    import java.util.concurrent.*; public class JNIDemo {
    public native String callHello(); public native boolean sendMsg(String str); //儲存JNI返回的結果
    private BlockingQueue<Entry> queue = new ArrayBlockingQueue<Entry>(10); //處理jni返回的任務 執行緒池
    private ExecutorService executor = Executors.newSingleThreadExecutor(); public static void main(String[] args) throws InterruptedException { //載入so檔案 libJNIDemo.so
    System.loadLibrary("JNIDemo"); //System.load(); 也可以用這種方式,引數是so檔案的全路徑
    JNIDemo jniDemo = new JNIDemo(); //儲存需要提交的任務
    Set<String> sets = new ConcurrentSkipListSet<>(); String str;
    for (int i = 0; i < 10; i++) {
    str = UUID.randomUUID().toString();
    sets.add(str);
    System.out.println("commit task :" + str); //在這裡會呼叫jni 提交任務,jni中會新啟動一個執行緒,非同步去執行任務,執行完了會呼叫putEntry方法,新增到需要回調的任務列表中
    jniDemo.sendStr(str);
    } Thread thread = new Thread(() -> {
    while (true) { try {
    Entry entry = jniDemo.queue.take();
    sets.remove(entry.str); //線上程中中,進行回撥通知,比如向呼叫方傳送任務處理結果
    jniDemo.executor.execute(() -> System.out.println(entry.str + "process finish")); //所有任務進行回撥後,結束添加回調任務執行緒,同時關閉執行緒池
    if (sets.isEmpty()) {
    jniDemo.executor.shutdown();
    break;
    }
    } catch (InterruptedException e) {
    System.out.println("thread interrupt "); jniDemo.executor.shutdown();
    break;
    } }
    });
    thread.start(); //也可以通過這種方式,比如在其他地方在中斷回撥任務的新增執行,關閉執行緒池
    //thread.interrupt(); } public boolean sendStr(String str) {
    return sendMsg(str);
    } public void putEntry(Entry entry) {
    queue.add(entry);
    } private static class Entry { //表示一個任務
    private String str; //表示任務處理結果
    private String result; public String getStr() {
    return str;
    } public void setStr(String str) {
    this.str = str;
    } public String getResult() {
    return result;
    } public void setResult(String result) {
    this.result = result;
    } @Override
    public String toString() {
    return "Entry{" +
    "str='" + str + '\'' +
    ", result='" + result + '\'' +
    '}';
    }
    } }
  2. 編譯成class檔案

    javac com/wbo112/jni/JNIDemo.java
  3. 生成標頭檔案

    javah com.wbo112.jni.JNIDemo

    這時就會在當前目錄下生成com_wbo112_jni_JNIDemo.h。

  4. 編寫對應的cpp檔案 JNIDemo1.cpp

    #include "com_wbo112_jni_JNIDemo.h"
    #include <iostream>
    #include <thread>
    #include <cstdlib>
    #include <ctime>
    #include <unistd.h> JavaVM *vm=NULL; unsigned seed; //在java載入so檔案的時候,就會呼叫到這個JNI_OnLoad
    //oracle文件這裡有介紹:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNJI_OnLoad
    jint JNI_OnLoad(JavaVM *jvm, void *reserved){
    //這個表示java虛擬機器,這個需要儲存下來,因為JNIEnv只是當前執行緒有效,如果要在其他執行緒獲取JNIEnv,就需要通過這個jvm來獲取,後面有相應程式碼
    vm=jvm; //這個是為了模擬後面的回撥時間,業務中應該不關注
    seed = time(0);
    srand(seed); return JNI_VERSION_1_8; } //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNI_OnUnload
    //so檔案解除安裝的時候,會執行這個函式,在這裡面可以做一些清理工作
    void JNI_OnUnload_L(JavaVM *vm, void *reserved){
    vm=NULL;
    } void threadfunc(jobject jobj,jstring jstr)
    {
    JNIEnv* env = NULL;
    sleep(rand() % 10); //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#GetEnv
    //這裡有介紹,如果不是當前執行緒,是獲取不到env的,這時,返回值就是JNI_EDETACHED,這時就需要呼叫AttachCurrentThread,給當前執行緒繫結env
    jint status = vm->GetEnv((void **)&env, JNI_VERSION_1_8); if (status == JNI_EDETACHED) {
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#AttachCurrentThread
    if (vm->AttachCurrentThread((void **)&env, NULL)!= JNI_OK){
    return;
    } } else if(status!= JNI_OK){
    std::cout<<"getEnv err"<<std::endl;
    return; } //這個是將jstring轉成char*
    const char *str = env->GetStringUTFChars(jstr, 0); std::cout << "start process task " + (std::string)str << std::endl; //通過物件獲取對應的類
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetObjectClass
    jclass jcls=env->GetObjectClass(jobj); //另一種方式獲取對應的類
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#FindClass
    jclass jentrycls=env->FindClass("com/wbo112/jni/JNIDemo$Entry"); //獲取無參的構造方法
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetMethodID
    jmethodID jinit=env->GetMethodID(jcls,"<init>", "()V"); //呼叫構造方法獲取物件
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewObject
    jobject jentryobj=env->NewObject(jentrycls,jinit); //獲取entry類的str欄位
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetFieldID
    jfieldID jfieldStr=env->GetFieldID(jentrycls,"str","Ljava/lang/String;"); //給str欄位設定值
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#Set_type_Field_routines
    env->SetObjectField(jentryobj,jfieldStr,jstr);
    char* msg = "Hello World!";
    jstring result = env->NewStringUTF(msg); jfieldID jfieldResult=env->GetFieldID(jentrycls,"result","Ljava/lang/String;"); env->SetObjectField(jentryobj,jfieldResult,result); //獲取方法
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetMethodID
    jmethodID jmtd=env->GetMethodID(jcls,"putEntry", "(Lcom/wbo112/jni/JNIDemo$Entry;)V"); //呼叫方法
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#Call_type_Method_routines
    env->CallVoidMethod(jobj,jmtd,jentryobj);
    std::cout<<" end process task " + (std::string)str<<std::endl;
    //釋放前面構造的char*
    env->ReleaseStringUTFChars( jstr, str); //釋放全域性物件
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#DeleteGlobalRef
    env->DeleteGlobalRef(jobj);
    env->DeleteGlobalRef(jstr); //當前執行緒和jvm進行分離
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#DetachCurrentThread
    vm->DetachCurrentThread();
    } JNIEXPORT jstring JNICALL Java_com_wbo112_jni_JNIDemo_callHello
    (JNIEnv *env, jobject jobj){
    char* msg = "Hello World!";
    jstring result = env->NewStringUTF(msg); // C style string to Java String
    return result; }
    JNIEXPORT jboolean JNICALL Java_com_wbo112_jni_JNIDemo_sendMsg
    (JNIEnv *env, jobject jobj, jstring jstr){ //jobject物件是不能跨執行緒傳遞的,需要先轉成全域性引用
    //https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewGlobalRef
    jobject globalJobj=env->NewGlobalRef(jobj);
    jobject globalJstr=env->NewGlobalRef(jstr); //這裡使用的是std::thread ,所以編譯的時候需要加引數-std=c++11
    std::thread t1(threadfunc,globalJobj,(jstring)globalJstr);
    //t1.join();
    t1.detach();
    return 1; }

    編譯cpp檔案

    g++ -shared -std=c++11  -I $JAVA_HOME/include/linux -I $JAVA_HOME/include  -fPIC -o libJNIDemo.so JNIDemo1.cpp
  5. 執行java程式

    java -Djava.library.path=./ com.wbo112.jni.JNIDemo

2、C++程式碼呼叫java程式碼

這個完全就是Oracle官方的例子了

The Invocation API (oracle.com)

  1. 首先建立個ctj.cpp檔案

     #include <jni.h>       /* where everything is defined */
    int main(){ JavaVM *jvm; /* denotes a Java VM */
    JNIEnv *env; /* pointer to native method interface */
    JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
    JavaVMOption* options = new JavaVMOption[1];
    options[0].optionString = "-Djava.class.path=./";
    vm_args.version = JNI_VERSION_1_8;
    vm_args.nOptions = 1;
    vm_args.options = options;
    vm_args.ignoreUnrecognized = false;
    /* load and initialize a Java VM, return a JNI interface
    * * pointer in env */
    JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
    delete options;
    /* invoke the Main.test method using the JNI */
    jclass cls = env->FindClass("Main");
    jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
    env->CallStaticVoidMethod(cls, mid, 100);
    /* We are done. */
    jvm->DestroyJavaVM();
    return 0;
    }
  2. 再建立c++需要呼叫的java檔案 Main.java

    public class Main {
    public static void test(int a) {
    System.out.println(" Main test:" + a);
    }
    }
  3. 編譯ctj.cpp檔案

    g++ -I $JAVA_HOME/include/linux -I $JAVA_HOME/include   -L"$JAVA_HOME/jre/lib/amd64/server/" ctj.cpp -o  ctj   -ljvm
  4. 執行生成的ctj檔案

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JAVA_HOME/jre/lib/amd64/server/
    ./ctj