java呼叫c++動態庫之jni呼叫

circlelll發表於2024-07-16

一、背景

存在java程式呼叫c++程式的場景,對常見的jni技術進行使用方法的總結。

二、技術

Java Native Interface(JNI)是一種程式設計框架,它允許Java程式碼與使用其他程式語言(如C、C++)編寫的應用程式和庫進行互動。JNI提供了一組API,使Java虛擬機器(JVM)能夠呼叫原生代碼(native code),反之亦然。

jni的兩個常見使用:

  1. 從Java程式呼叫C/C++(常用,本文也是主要介紹這個)
  2. 從C/C++程式呼叫Java程式碼

三、基本使用

(一)所需工具

javac.exe: Java 編譯器:隨JDK一起提供的
java.exe: Java 虛擬機器(JVM):隨 JDK一起提供的 。
javah.exe: 本機方法 C 檔案生成器:隨JDK一起提供的 。

(二)java載入檔案的方法

方式1:只需要指定動態庫的名字,不需要加lib字首,也不要加.so、.dll和.jnilib字尾
方式2:指定動態庫的絕對路徑,需要加上字首和字尾

針對方式1:讓java從java.library.path找到動態連結庫檔案:

  1. 將動態連結庫複製到java.library.path目錄下
  2. 給jvm新增“-Djava.library.path=動態連結庫搜尋目錄”引數,指定系統屬性java.library.path的值

Linux/Unix環境下可以透過設定LD_LIBRARY_PATH環境變數,指定庫的搜尋目錄。

在linux中新增系統動態庫依賴方式如下:
1、臨時新增
export LD_LIBRARY_PATH=動態庫所在的目錄:$LD_LIBRARY_PATH
如:
動態庫在/home/test/下:
export LD_LIBRARY_PATH=/home/test/:$LD_LIBRARY_PATH
動態庫在當前目錄下:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
2、永久新增
vim ~/.bashrc中新增export LD_LIBRARY_PATH=/your/custom/library/path:$LD_LIBRARY_PATH(bash -c 'echo "export LD_LIBRARY_PATH=/your/custom/library/path:$LD_LIBRARY_PATH">>~/.bashrc')
source ~/.bashrc

示例:

static {
    System.loadLibrary("native-lib"); // 須在系統或者呼叫引數中指定動態庫路徑
}

static {
    System.load(/home/libnative-lib.so);
}

(三)基本呼叫過程

1、編寫宣告瞭native方法的Java類

2、將Java原始碼編譯成class位元組碼檔案

3、用javah -jni命令生成.h標頭檔案(javah是jdk自帶的一個命令,-jni參數列示將class中用native宣告的函式生成jni規則的函式)

4、用原生代碼(c、c++、其他語言)實現.h標頭檔案中的函式

5、將原生代碼編譯成動態庫(windows:.dll,linux/unix:.so,mac-os x:*.jnilib)

6、複製動態庫至 java.library.path 本地庫搜尋目錄下,或設定jvm引數-Djava.library.path=連結庫所在目錄,並執行Java程式

(替代:linux下直接設定LD_LIBRARY_PATH環境變數,指定庫的搜尋目錄)

7、執行java程式測試java呼叫

四、示例

(一)基礎示例

1.GetInfo.java
2.生成標頭檔案GetInfo.h
javac GetInfo.java
javac -h . GetInfo.java
3.編寫cpp實現檔案GetInfo.cpp
4.生成動態庫libextract.so
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
5.使用java測試呼叫
javac GetInfo.java
執行程式前將動態庫路徑新增到系統中:此處的點可以替換為實際動態庫存放路徑
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH #臨時
java GetInfo

(二)示例環境

使用openjdk

前提環境:
1、使用ubuntu環境
2、系統中無jdk
3、使用以下命令安裝jdk
apt-get install default-jdk
4、透過find命令查詢jni需要的jni.h檔案的目錄,jni.h中依賴的jni
jni.h所在目錄:/usr/lib/jvm/java-11-openjdk-amd64/include/
jni_md所在目錄:/usr/lib/jvm/java-11-openjdk-amd64/include/linux

(三)預設場景

場景一:使用openjdk
場景二:場景一+增加包名
場景三:場景一+動態庫依賴其他動態庫
場景四:場景一+增加包名+動態庫依賴其他動態庫

針對場景二:包名對標頭檔案和方法生成有所影響:包名為com.util,生成方法出現報名會出現Java_com_util_GetInfo_getHeadInfo,標頭檔案名也會相應改變

(四)具體示例

場景一

檔案目錄結構:部分為手動新增,部分為生成

|--GetInfo.h #生成,jni生成,使用java -h命令
|--GetInfo.java #手動,新增和編寫
|--GetInfo.class #生成,javac編譯生成
|--GetInfo.cpp #手動,根據動態生成的標頭檔案編寫cpp檔案
|--libextract.so #生成,c++動態庫

1.GetInfo.java

public class GetInfo {
    static {
        System.loadLibrary("extract"); // // 載入本地庫Load native library at runtime, libextract.so
    }
    // 宣告本地方法
    public native String getHeadInfo(String filePath); // the native method
    public static void main(String[] args) {
        GetInfo getInfo = new GetInfo();
        String info = getInfo.getHeadInfo("");  // invoke the native method
        System.out.println(info);
    }
}

2.生成標頭檔案GetInfo.h

javac GetInfo.java
javac -h . GetInfo.java

生成內容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class GetInfo */

#ifndef _Included_GetInfo
#define _Included_GetInfo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     GetInfo
 * Method:    getHeadInfo
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

3.編寫cpp實現檔案GetInfo.cpp

#include "GetInfo.h"

extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
     // 建立一個 C 字串
    const char *greeting = "Hello from JNI!";

    // 將 C 字串轉換為 jstring 並返回
    return env->NewStringUTF(greeting);
}

4.生成動態庫libextract.so

g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux

5.使用java測試呼叫

javac GetInfo.java
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo

場景二

檔案目錄結構:部分為手動新增,部分為生成

|--com
|--util
|--GetInfo.java #手動,新增和編寫
|--GetInfo.class #生成,javac編譯生成
|--com_util_GetInfo.h #生成,jni生成,使用java -h命令
|--com_util_GetInfo.cpp #手動,根據動態生成的標頭檔案編寫cpp檔案
|--libextract.so #生成,c++動態庫

GetInfo.java:增加包名com.util,程式碼package com.util;

package com.util;

public class GetInfo {
    static {
        System.loadLibrary("extract"); // 載入本地庫Load native library at runtime, libextract.so, 靜態程式碼塊在類被載入時執行,呼叫 System.loadLibrary 方法載入名為 extract 的本地庫
    }
    // 宣告本地方法:native 關鍵字表明 getHeadInfo 方法是在原生代碼(如 C 或 C++)中實現的。
    public native String getHeadInfo(String filePath); // the native method
    public static void main(String[] args) {
        GetInfo getInfo = new GetInfo();
        String info = getInfo.getHeadInfo("");  // invoke the native method
        System.out.println(info);
    }
}

生成的com_util_GetInfo.h:生成的方法中自動增加包的標識

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_util_GetInfo */

#ifndef _Included_com_util_GetInfo
#define _Included_com_util_GetInfo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_util_GetInfo
 * Method:    getHeadInfo
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_util_GetInfo_getHeadInfo
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

建立com_util_GetInfo.cpp檔案

#include "com_util_GetInfo.h"

extern "C"
JNIEXPORT jstring JNICALL Java_com_util_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
     // 建立一個 C 字串
    const char *greeting = "Hello from JNI!";

    // 將 C 字串轉換為 jstring 並返回
    return env->NewStringUTF(greeting);
}

編譯成動態庫:

g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -I./com/util

測試的java檔案:當前建立com/utilwen資料夾,放入java檔案

javac com/util/GetInfo.java
java com/util/GetInfo

總結:

增加包名後:

①自動生成的標頭檔案和方法增加包名

②使用java程式測試時,需按照包結構進行編譯和執行

場景三

場景一的基礎上增加動態庫的依賴

檔案目錄結構:部分為手動新增,部分為生成

|--GetInfo.java #手動,新增和編寫
|--GetInfo.class #生成,javac編譯生成
|--GetInfo.h #生成,jni生成,使用java -h命令
|--GetInfo.cpp #手動,根據動態生成的標頭檔案編寫cpp檔案
|--libextract.so #生成,c++動態庫
|--base.h #手動
|--base.cpp #手動
|--libbase.so #生成,libextract.so的依賴

示例解釋:java呼叫動態庫依賴A,動態庫A依賴動態庫B

動態庫B製作:返回要列印的字串
base.h

#ifndef _Included_Base
#define _Included_Base

#include <string>

#ifdef __cplusplus
extern "C" {
#endif

std::string getString();

#ifdef __cplusplus
}
#endif
#endif

base.cpp

#include "GetInfo.h"
#include "base.h"

extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
     // 建立一個 C 字串
    //const char *greeting = "Hello from JNI!";
    std::string greeting = getString();

    // 將 C 字串轉換為 jstring 並返回
    return env->NewStringUTF(greeting.c_str());
}

編譯成動態庫:

g++ -shared -fPIC -o libbase.so base.cpp

動態庫A製作:參照1GetInfo.cpp中修改呼叫B中的方法

#include "GetInfo.h"
#include "base.h"

extern "C"
JNIEXPORT jstring JNICALL Java_GetInfo_getHeadInfo(JNIEnv *env, jobject obj, jstring filePath)
{
     // 建立一個 C 字串
    //const char *greeting = "Hello from JNI!";
    std::string greeting = getString();

    // 將 C 字串轉換為 jstring 並返回
    return env->NewStringUTF(greeting.c_str());
}

編譯成動態庫:

g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -L./ -lbase

java測試程式呼叫:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo

總結:

由此可見java呼叫的動態庫存在其他依賴動態庫的情況下,只需要在系統中執行依賴路徑即可(此路徑包括呼叫動態庫路徑和依賴動態庫路徑)

場景四

前三種場景的結合

總結,增加包名後的:

1.GetInfo.java(此處增加包名,如com.util)
2.生成標頭檔案GetInfo.h(增加包名後,自動生成的標頭檔案名及方法包含包名,如com_util_GetInfo.h)
javac GetInfo.java
javac -h . GetInfo.java
3.編寫cpp實現檔案GetInfo.cpp(增加包名後,可使用com_util_GetInfo.cpp)
4.生成動態庫libextract.so:(若有其他依賴,-l新增其他依賴)
g++ -shared -fPIC -o libextract.so GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
5.使用java測試呼叫
javac GetInfo.java(增加包名後,需要按照包結果,如當前目錄下建立com/util資料夾,裡面方GetInfo.java,呼叫程式程式設計javac com/util/GetInfo.java)
執行程式前將動態庫路徑新增到系統中:此處的點可以替換為實際動態庫存放路徑
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
java GetInfo(增加包名後執行命令java com/util/GetInfo)

總結-重要

1、增加包名後:

①自動生成的標頭檔案和方法增加包名,

②使用java程式測試時,需按照包結構進行編譯和執行

2、java呼叫的動態庫存在其他依賴動態庫的情況下,只需要在系統中執行依賴路徑即可(此路徑包括呼叫動態庫路徑和依賴動態庫路徑)

(五)錯誤及解決

1、系統中存在jni,但是編譯時提示沒有那個檔案或目錄

【解決】

編譯動態庫指定標頭檔案路徑:

g++ -shared -fPIC GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
/usr/lib/jvm/java-11-openjdk-amd64/include:jni.h路徑
/usr/lib/jvm/java-11-openjdk-amd64/include/linux:jni.h中引用的jni

2、返回值jstring找不到方法

【報錯資訊】

g++ -shared -fPIC GetInfo.cpp -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
GetInfo.cpp: In function ‘_jstring* Java_GetInfo_getHeadInfo(JNIEnv*, jobject, jstring)’:
GetInfo.cpp:9:18: error: base operand of ‘->’ has non-pointer type ‘JNIEnv {aka JNIEnv_}’
return (*env)->NewStringUTF(env, greeting);

【原因】

env本身為指標型別-->不同的編譯器和jni實現方式不同

【解決】

將return (*env)->NewStringUTF(env, greeting);
改為return env->NewStringUTF(greeting);

3、呼叫時產生錯誤java.lang.UnsatisfiedLinkError

【報錯資訊】

root@lym-vm:/home/work/testuse/jni# java GetInfo
Exception in thread "main" java.lang.UnsatisfiedLinkError: no extract in java.library.path: [/usr/java/packages/lib, /usr/lib/x86_64-linux-gnu/jni, /lib/x86_64-linux-gnu, /usr/lib/x86_64-linux-gnu, /usr/lib/jni, /lib, /usr/lib]
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2673)
at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:830)
at java.base/java.lang.System.loadLibrary(System.java:1873)
at GetInfo.<clinit>(GetInfo.java:3)

【原因】

java呼叫動態庫找不到庫

【解決】

將動態庫新增進系統

①臨時新增
此處的點可以替換為實際動態庫存放路徑,如/opt/libso
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
②永久新增
sudo bash -c 'echo "export LD_LIBRARY_PATH=/opt/libso:$LD_LIBRARY_PATH">>~/.bashrc'
source ~/.bashrc

4、javac呼叫錯誤

【背景】

java檔案增加包名,package com.util;,執行javac時報錯

【錯誤】

# javac GetInfo

錯誤: 僅當顯式請求註釋處理時才接受類名稱 'GetInfo'
1 個錯誤

【原因】

包結構影響

【解決】

確保程式碼儲存為 GetInfo.java 並放在適當的目錄中。例如,確保路徑為 com/util/GetInfo.java
編譯檔案
javac com/util/GetInfo.java
執行程式
java com.util.GetInfo

5、jni呼叫動態庫的依賴還未指定

【錯誤】

# java com/alibaba/datax/plugin/writer/platformwriter/GetEmlHeadInfoJNI

Exception in thread "main" java.lang.UnsatisfiedLinkError: 'java.lang.String com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.getEmlHeadInfo(java.lang.String)'
at com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.getEmlHeadInfo(Native Method)
at com.alibaba.datax.plugin.writer.platformwriter.GetEmlHeadInfoJNI.main(GetEmlHeadInfoJNI.java:16)

【解決】

同3

參考

1、https://blog.csdn.net/qq877728715/article/details/116664282(主要看這個,這篇很詳細並且思路很清晰)

相關文章