C/C++與Java混合的JNI編程
Java與C++混合編程可以實現兩種語言的優勢結合,C++的程序性能很高且支持強大的系統調用能力,Java則生態豐富且開發效率較高。JNI是Java與C++進行混合編程的關鍵橋梁,本章將基于JNI技術講述Java與C++混合編程的方法和技巧。
1. Java與JNI
1.1. 什么是Java?
Java是一種高級編程語言,也是一個計算平臺(通常指Java虛擬機)。最初由Sun Microsystems公司(后被Oracle收購)的James Gosling和他的團隊在1995年發布。Java語言的設計目標是簡單性、健壯性和跨平臺兼容性。以下是Java的一些關鍵特點:
- 面向對象: Java是一種面向對象的語言,這意味著它基于對象和類的概念。對象代表現實世界中的實體或概念,而類是創建對象的模板。
- 平臺無關性: Java的一個核心特性是“一次編寫,到處運行”(
Write Once, Run Anywhere,WORA)。Java程序在執行前會被編譯成字節碼,這種中間形式的代碼可以在任何安裝了Java虛擬機(JVM)的設備上運行。 - 自動內存管理: Java提供了自動垃圾回收機制,這意味著程序員不需要手動管理內存的分配和釋放,從而減少了內存泄漏和其他內存相關錯誤。
- 豐富的標準庫: Java擁有一個龐大的標準庫(也稱為Java API),提供了大量預先構建的類和接口,用于處理文件輸入/輸出、網絡編程、多線程編程、數據結構等。
- 跨平臺兼容性: Java不僅可以在不同的操作系統上運行,還可以在嵌入式系統、移動設備和大型服務器上運行。
Java的應用場景廣泛,是目前最流行的后端系統開發語言,此外Java還是Android系統的主要編程語言,絕大部分的Android應用程序都基于Java語言進行開發。
1.2. 什么是JVM?
JVM(Java Virtual Machine)是一個可以執行Java字節碼的虛擬計算機。它是Java平臺的核心組成部分,提供了Java程序運行所需的環境。JVM是Java語言能做到“一次編寫,到處運行”的基礎。以下是JVM的一些關鍵特點和功能:
- 平臺無關性:JVM的主要目的是實現Java的跨平臺特性。Java源代碼被編譯成平臺無關的字節碼,這些字節碼可以在任何安裝了相應JVM的設備上運行。
- 字節碼解釋器:JVM包含一個字節碼解釋器,它負責將字節碼轉換為特定平臺的機器碼。這個過程使得Java程序可以在不同的操作系統和硬件上運行。
- 即時編譯器(JIT):為了提高性能,現代JVM通常包含一個即時編譯器。JIT編譯器會將熱點代碼(頻繁執行的代碼)編譯成優化的機器碼,以提高執行效率。
- 垃圾回收:JVM負責管理內存分配和回收。它提供了自動垃圾回收機制,幫助程序員管理內存,減少內存泄漏和其他內存相關錯誤。
- 安全沙箱:JVM提供了一個安全的執行環境,可以限制代碼對系統資源的訪問。這有助于防止惡意代碼對系統造成破壞。
- 類加載器:JVM包含一個類加載器子系統,負責動態加載、驗證和準備類文件以供執行。類加載器確保類文件的完整性和安全性。
- 本地接口:JVM提供了與本地庫交互的接口(如JNI,Java Native Interface),允許Java代碼調用本地代碼(C/C++等),以實現特定功能或性能優化。
- 多線程支持:JVM支持多線程執行,允許程序同時執行多個任務。
1.3. 什么是JNI?
JNI(Java Native Interface)是一個允許Java代碼與本地代碼(如:C/C++)進行交互的接口。通過JNI,Java應用程序可以調用本地庫中的函數,也可以被本地代碼調用,它是實現Java與C/C++混合編程的關鍵機制。
JNI主要包含以下兩部分內容:
- Java代碼與本地代碼交互的接口。
- 支持JNI開發的一套開發工具,如:
javah、javac等。
JNI接口的官方文檔:https://docs.oracle.com/en/java/javase/21/docs/specs/jni/index.html
1.4. 環境說明
本章所有的示例代碼的開發環境如下:
- 操作系統: Ubuntu 24.04
- JDK版本: 21.0.5
- GCC版本: 13.3.0
- 開發工具:VSCode
2. 開發環境搭建
2.1. Windows
-
在官網下載最新版本的安裝包,官網下載地址:https://www.oracle.com/cn/java/technologies/downloads/
![]()
-
雙擊安裝包,根據提示一步步安裝即可。
-
打開命令行輸入一下命令,驗證是否安裝成功,如果有顯示相應的版本號則說明安裝成功。
java -version
2.2. Linux(Ubuntu)
安裝JDK:
# 1. 更新軟件包列表
sudo apt update
# 3. 該命令將自動選擇并安裝最新的 LTS 版本,當前是 OpenJDK 21[5]。
sudo apt install default-jdk
# 3. 驗證是否安裝成功,如果有顯示相應的版本號則說明安裝成功。
java --version
設置環境變量:
# 1. 查找JDK的安裝路徑
update-alternatives --config java
There are 2 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 auto mode
1 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 manual mode
# 2. vim打開.zshrc(如果你的SHELL用的是.bashrc,替換成相應的.bashrc)
vim ~/.zshrc
# 3. 在文件末尾添加如下內容
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=${JAVA_HOME}/bin:${PATH}
# 4. 重新加載配置
source ~/.zshrc
2.3. macOS
以下是通過Homebrew工具的安裝步驟,確保已經安裝Homebrew。
# 1. 更新軟件包列表
brew update
# 2.1 安裝Oracle JDK的最新版本
brew install oracle-jdk
# 2.2 安裝Open JDK的最新版本
brew install java
# 3. 驗證是否安裝成功,如果有顯示相應的版本號則說明安裝成功。
java --version
2.4. Open JDK與Oracle JDK
Java語言最初由Sun公司研發,并發布了Java SE(Standard Edition)的規范和開源的Open JDK。Sun公司后被Oracle公司收購,Oracle基于Open JDK開發了 Oracle JDK。
Open JDK是一個完全開源的項目,遵循GPL v2許可。任何人都可以下載、使用、修改和分發它的代碼。主要的Linux發行版(如:Fedora,Ubuntu等)提供OpenJDK作為默認的Java SE實現。
Oracle JDK則基于Open JDK構建,但包含一些閉源組件,如Java插件、Java WebStart的實現和一些第三方組件。這些組件包括了一些商業功能,未開源。
Open JDK和Oracle JDK都遵循Java SE的規范,只是Oracle JDK提供了更多商業版的未開源的功能。
3. Say Hello程序
3.1. 新建SayHello.java
新建一個say_hello的測試目錄,然后在該目錄下新建一個SayHello.java文件,并編寫如下代碼:
public class SayHello {
// 類方法
private native void sayHello(String name);
// 靜態方法
private static native void sayGoodbye(String name);
static {
// 在程序初始化時加載native動態庫(libhello.so)
System.loadLibrary("hello");
}
public static void main(String[] args) {
new SayHello().sayHello("Spencer");
SayHello.sayGoodbye("陌塵");
}
}
說明:
-
這里有兩個被聲明為
native的方法,表示這兩個方法需要native代碼(C/C++)實現。這里一個是普通的類成員方法,一個是靜態的類方法。private native void sayHello(String name); private static native void sayGoodbye(String name); -
static包含的代碼塊,表示在程序初始化時加載native動態庫(libhello.so)static { System.loadLibrary("hello"); }
3.2. 編譯SayHello.java
javac ./SayHello.java
執行完成后,會生成一個SayHello.class的字節碼文件。
3.3. 生成SayHello.h
執行以下命令生成native代碼的頭文件
# JDK 9.0 之前
javah -cp ./ -d ./ SayHello
# `-cp ./`表示設置classpath為當前目錄,在當前目錄下查找.class文件
# `-d ./`表示設置頭文件的輸出目錄為當前目錄
# JDK 9.0 及之后
javac -h ./ ./SayHello.java
# 第一個`./` 表示設置頭文件的輸出目錄為當前目錄
執行成功后會在當前目錄下生成SayHello.h頭文件,內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class SayHello */
#ifndef _Included_SayHello
#define _Included_SayHello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: SayHello
* Method: sayHello
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_SayHello_sayHello
(JNIEnv *, jobject, jstring);
/*
* Class: SayHello
* Method: sayGoodbye
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_SayHello_sayGoodbye
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
代碼說明:
-
頭文件中包含兩個函數分別和
.java中的兩個方法一一對應。 -
函數聲明中開通的部分
JNIEXPORT void JNICALL,這個與《導出接口的定義》一文中的EAPI int CALLType是不是非常類似?是的,它就是JNI提供的動態庫導出接口聲明和調用約定聲明。 -
函數的命名非常有規律,其實它是遵循了JNI的函數命名規范:
Java_{package_name}_{class_name}_{function_name}(JNI arguments)。 -
函數的參數
- 第一個參數:
JNIEnv *env是一個指向JNI運行環境的指針,提供了JNI接口的各種功能函數。 - 第二個參數:
- 如果是一個普通的成員方法則參數為
jobject obj,指代java中的this對象,可以通過該參數來獲取Java對象的方法和屬性。 - 如果是一個靜態的類方法則參數為
jclass cls,指代java中的類,可以通過該參數來獲取Java類的靜態方法和靜態屬性。
- 如果是一個普通的成員方法則參數為
- 其他參數: 按從左到右的順序與
.java中聲明的方法的參數一一對應。
- 第一個參數:
3.4. 實現SayHello.cp
新建SayHello.cpp文件,并實現頭文件聲明的兩個函數,內容如下:
#include "SayHello.h"
#include <iostream>
JNIEXPORT void JNICALL Java_SayHello_sayHello(JNIEnv* env, jobject obj, jstring name)
{
// 將jstring轉化成C風格的UTF-9字符串
const char* cName = env->GetStringUTFChars(name, nullptr);
if (cName == nullptr)
{
return;
}
std::cout << "Hello, " << cName << "!" << std::endl;
}
JNIEXPORT void JNICALL Java_SayHello_sayGoodbye(JNIEnv* env, jclass cls, jstring name)
{
// 將jstring轉化成C風格的UTF-9字符串
const char* cName = env->GetStringUTFChars(name, nullptr);
if (cName == nullptr)
{
return;
}
std::cout << "Goodbye, " << cName << "!" << std::endl;
}
3.5. 編譯SayHello.cpp
g++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux SayHello.cpp -o libhello.so
執行成功后會生成libhello.so文件。
3.6. 運行SayHello程序
java -Djava.library.path=./ SayHello
Hello, Spencer!
Goodbye, 陌塵!
-Djava.library.path=./表示在當前目錄下查找libhello.so。
4. JNI開發的關鍵步驟
根據前面“Say Hello程序”的示例,可以總結出JNI開發的關鍵步驟和原理如下:
- 本地方法聲明:在Java中,你可以聲明一個或多個本地方法,這些方法沒有Java實現,而是通過JNI與本地代碼關聯。
- 生成頭文件:使用
javah(或javac -h)工具從Java類生成C/C++頭文件,該頭文件包含JNI函數的原型。 - 實現本地方法:在C或C++代碼中實現這些本地方法。這些方法必須遵循JNI的調用約定和數據類型規范。
- 編譯和加載:編譯本地代碼并生成動態鏈接庫(如
.so、.dll或.dylib文件),然后在Java程序中加載這些庫。 - 調用本地方法:Java程序可以通過JNI調用這些本地方法,就像調用普通Java方法一樣。
5. 數據類型的對應關系
5.1. 對應關系
JNI定義了一套以j開頭的C/C++的數據類型,與Java進行一一對應,他們之間的對應關系如下:
| 分類 | Java數據類型 | JNI數據類型 | C/C++數據類型 |
|---|---|---|---|
| 基礎類型 | boolean | jboolean | unsigned char,相當于uint8_t。 |
| 基礎類型 | byte | jbyte | signed char,相當于int8_t。 |
| 基礎類型 | char | jchar | unsigned short,相當于uint16_t。 |
| 基礎類型 | short | jshort | short,相當于int16_t。 |
| 基礎類型 | int | jint | int,相當于int32_t。 |
| 基礎類型 | long | jlong | long,相當于int64_t。 |
| 基礎類型 | float | jfloat | float,4字節 |
| 基礎類型 | double | jdouble | double,8字節 |
| 引用類型 | Object | jobject | jobject的定義:class _jobject {};typedef _jobject *jobject;所以jobject的作用類似于 void*,表示通用對象指針。 |
| 引用類型 | Class | jclass | class _jclass : public _jobject {};typedef _jclass *jclass; |
| 引用類型 | String | jstring | class _jstring : public _jobject {};typedef _jstring *jstring; |
| 引用類型 | 數組 | jarray | class _jarray : public _jobject {};typedef _jarray *jarray; |
| 引用類型 | Throwable | jthrowable | class _jthrowable : public _jobject {};typedef _jthrowable *jthrowable; |
5.2. 類型說明
Java的數據類型分基礎數據類型(如int)和引用數據類型(如:Object、Class)。
基礎數據類型: 會直接轉換為C/C++的基礎數據類型,例如int類型映射為jint類型。由于 jint是C/C++類型,所以可以直接當作普通C/C++變量使用,而不用做任何轉換。
引用數據類型: 對象只會轉換為一個C/C++指針,例如Object類型映射為jobject類型。由于指針指向Java虛擬機內部的數據結構,所以不可能直接在C/C++代碼中操作對象,而是需要依賴JNIEnv環境對象。另外,為了避免對象在使用時突然被回收,在本地方法返回前,虛擬機會固定(pin)對象,阻止其 GC。
Java中的數組對應于C/C++中的jarray,它是一個通用的數組類型。而具體數據類型的數組,對應于Java數組的特定類型,對應關系如下。
| Java數據類型 | JNI數據類型 |
|---|---|
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| Object[] | jobjectArray |
歷史文章推薦:
21. 工程篇:VSCode中使用CMake插件運行和調試程序
28. 跨語言:C/C++與JavaScript的WebAssembly編程(一)
29. 跨語言:C/C++與JavaScript的WebAssembly編程(二)
30. 跨語言:C/C++與JavaScript的WebAssembly編程(三)
36. 跨語言:C/C++與Swift&Objective-C混合編程(一)
大家好,我是陌塵。
IT從業10年+, 北漂過也深漂過,目前暫定居于杭州,未來不知還會飄向何方。
搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點PHP,未來還會折騰更多東西,不死不休。
感謝大家的關注,期待與你一起成長。


浙公網安備 33010602011771號