Java调用C++实战:JNI环境配置、代码编写与问题排查指南

📅 2026/7/22 5:45:55
Java调用C++实战:JNI环境配置、代码编写与问题排查指南
1. 项目概述为什么要在Java里调用C在Java项目里有时候你会遇到一些性能瓶颈比如密集的数学计算、图像处理或者需要直接操作硬件、复用一些历史悠久的C/C库。这时候纯Java可能就有点力不从心了。JNIJava Native Interface就是Java官方提供的“桥梁”让你能在Java世界里安全、规范地调用用C或C写的本地代码。这就像在Java这个现代化的城市里开了一条专用通道可以直接访问旁边C这个工业区里的重型设备。我最近在重构一个老项目里面有个核心的图像处理算法是用C写的效率很高但业务层是Java Web。重写算法不现实所以就得用JNI把它封装起来给Java调用。网上教程很多但真自己动手从环境配置、代码编写到编译打包每一步都可能踩坑。这篇文章我就把我从零开始把一个简单的C数学计算库封装成JNI接口并让Java成功调用的全过程以及中间遇到的各种“坑”和解决方案详细记录下来。目标就是让你看完后能避开我走过的弯路快速搞定自己的JNI封装。整个过程会涉及几个关键部分首先是环境准备JDK、C编译器、构建工具然后是编写Java端的Native方法声明和C/C端的本地函数实现接着是编译生成动态链接库Windows上是.dllLinux/Mac上是.so最后是在Java中加载并调用。我会用VSCode作为主要开发环境来演示因为它轻量且跨平台但原理同样适用于Visual Studio、CLion或命令行。2. 环境与工具链准备兵马未动粮草先行搞JNI开发环境配置是第一步也是劝退很多新手的门槛。它要求你的机器上同时具备Java和C/C两套开发环境并且它们要能“对话”。2.1 核心组件清单你需要准备以下三样东西缺一不可Java Development Kit (JDK)不仅是运行Java更重要的是需要里面的javac编译器和javah生成头文件工具不过从JDK 10开始推荐用javac -h替代。我用的JDK 17确保JAVA_HOME环境变量正确设置并且%JAVA_HOME%\binWindows或$JAVA_HOME/binLinux/Mac在系统PATH里。C/C编译器这是把C代码编译成机器码的关键。Windows最常用的是MinGW-w64或Microsoft Visual C Build Tools。MinGW-w64更接近Linux环境生成GCC兼容的库VC Build Tools则是微软原生工具链。如果你后续的C库依赖VC的运行时那就选后者。我为了通用性选择了MinGW-w64。Linux通常系统自带GCC/G用包管理器安装即可如sudo apt install g。Mac安装Xcode Command Line Tools它会包含Clang/LLVM。构建工具可选但推荐对于简单的例子手动敲命令还行。但项目稍复杂依赖多个源文件时就需要CMake或Make来管理构建过程。这里我用CMake因为它跨平台配置文件CMakeLists.txt写一次到处都能编译。2.2 VSCode环境配置实操我选择VSCode因为它轻量通过插件可以很好地支持Java和C。安装必要插件Java安装扩展包“Extension Pack for Java”它会自动安装语言支持、调试器、项目管理等所有Java相关插件。C/C安装微软官方的“C/C”扩展提供代码提示、调试等功能。CMake安装“CMake”和“CMake Tools”扩展用于CMake项目的配置和构建。配置C编译器路径 在VSCode中按CtrlShiftP输入“C/C: Edit Configurations (UI)”打开配置界面。在“编译器路径”这里需要指向你安装的G或Clang。例如我的MinGW-w64安装在C:\mingw64\bin\g.exe。这里配置好后C插件的智能提示和错误检查才会正常工作。验证环境 打开终端VSCode内置或系统终端分别执行以下命令确保都能正确输出版本信息。# 验证Java java -version javac -version # 验证C编译器 (Windows MinGW示例) g --version # 验证CMake cmake --version如果任何一条命令报“不是内部或外部命令”说明PATH环境变量没配好需要回去检查。注意环境变量配置后通常需要重启VSCode或者新开一个终端窗口才能生效。很多“命令找不到”的问题都是因为没重启。2.3 项目目录结构规划在开始写代码前先规划好目录能让后续步骤清晰很多。我创建了如下的项目结构jni_demo_project/ ├── java/ # Java端代码 │ └── com/example/jni/ │ └── NativeCalculator.java ├── cpp/ # C端代码 │ ├── native_calculator.cpp │ ├── calculator.h │ └── calculator.cpp ├── include/ # 自动生成的JNI头文件会放在这里 ├── lib/ # 编译生成的动态库放在这里 ├── CMakeLists.txt # CMake构建脚本 └── build/ # CMake构建输出目录可忽略这个结构将Java和C代码分离include目录用于存放由Java类生成的、C需要引用的头文件lib目录存放最终产物干净明了。3. JNI工作原理与代码编写搭建沟通的桥梁环境好了我们来理解一下JNI到底是怎么工作的。整个过程就像一个协议握手Java端声明一个方法加上native关键字告诉JVM“这个方法的具体实现不在我这儿在本地库里头。”生成头文件利用JDK工具javac -h根据Java的native方法声明生成一个C/C语言的头文件.h。这个头文件里定义了符合JNI规范的函数原型它是Java和C之间的“合同”。C端实现这个头文件里声明的函数。在这里面你可以写任何C代码并且通过JNIEnv这个指针提供的系列函数来和Java端交互比如获取Java传来的参数创建Java对象返回结果等。编译与链接将C实现文件编译成动态链接库。Java加载与调用在Java代码里使用System.loadLibrary或System.load加载编译好的动态库然后就可以像调用普通Java方法一样调用那个native方法了。3.1 Java端声明Native方法我们从一个简单的例子开始封装一个C的加法函数。在java/com/example/jni/NativeCalculator.java中package com.example.jni; public class NativeCalculator { // 声明一个native方法用于计算两个整数的和 public native int add(int a, int b); // 再声明一个用于计算阶乘展示稍微复杂的逻辑 public native long factorial(int n); // 静态初始化块在类加载时自动调用用于加载本地库 static { // 这里加载的是最终生成的动态库的名字不含平台特定的前缀如lib和后缀如.dll/.so // 在Windows上它会尝试加载NativeCalculator.dll // 在Linux上它会尝试加载libNativeCalculator.so System.loadLibrary(NativeCalculator); } // 一个简单的main方法用于测试 public static void main(String[] args) { NativeCalculator calc new NativeCalculator(); int sum calc.add(10, 20); System.out.println(10 20 sum); long fact calc.factorial(5); System.out.println(5! fact); } }关键点native关键字是核心。System.loadLibrary的参数是库的“逻辑名”。在Windows下实际文件是NativeCalculator.dll在Linux下是libNativeCalculator.so。这个方法会在Java库路径java.library.path中查找。也可以使用System.load(“/绝对/路径/到/库文件.dll”)来指定绝对路径这在开发调试时更直接。3.2 生成JNI头文件签订“合同”有了Java类我们需要生成C需要实现的头文件。在项目根目录jni_demo_project下打开终端执行javac -h ./include java/com/example/jni/NativeCalculator.java这个命令做了两件事javac编译了NativeCalculator.java在相同目录下生成NativeCalculator.class。-h ./include参数指示将生成的头文件输出到./include目录。执行后查看include目录会发现生成了一个名为com_example_jni_NativeCalculator.h的文件。文件名是包名_类名.h的格式这是JNI的命名规范。我们打开看看/* DO NOT EDIT THIS FILE - it is machine generated */ #include jni.h /* Header for class com_example_jni_NativeCalculator */ #ifndef _Included_com_example_jni_NativeCalculator #define _Included_com_example_jni_NativeCalculator #ifdef __cplusplus extern C { #endif /* * Class: com_example_jni_NativeCalculator * Method: add * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_example_jni_NativeCalculator_add (JNIEnv *, jobject, jint, jint); /* * Class: com_example_jni_NativeCalculator * Method: factorial * Signature: (I)J */ JNIEXPORT jlong JNICALL Java_com_example_jni_NativeCalculator_factorial (JNIEnv *, jobject, jint); #ifdef __cplusplus } #endif #endif这就是我们的“合同”。里面声明了两个函数Java_com_example_jni_NativeCalculator_addJava_com_example_jni_NativeCalculator_factorial函数名很长是Java_{包名}_{类名}_{方法名}的格式。参数方面JNIEnv* env这是最重要的参数一个指向JNI环境的指针所有与Java交互的API都通过它调用。jobject obj如果native方法是实例方法非static这个参数就是调用该方法的Java对象this的引用。如果是静态方法则是jclass类型。后面的jintjlong这是JNI定义的基本类型对应Java的int和long。JNI定义了一套与Java类型对应的本地类型如jint,jlong,jboolean,jstring,jobjectArray等。JNIEXPORT和JNICALL是编译器相关的宏确保函数能被正确导出和调用。3.3 C端实现本地函数现在我们来履行“合同”在C端实现这两个函数。首先在cpp目录下创建实现文件native_calculator.cpp// native_calculator.cpp #include jni.h // 必须包含JNI头文件 #include “com_example_jni_NativeCalculator.h” // 包含生成的头文件 #include iostream #include “calculator.h” // 假设我们有一个独立的C类 // 实现加法函数 JNIEXPORT jint JNICALL Java_com_example_jni_NativeCalculator_add (JNIEnv *env, jobject obj, jint a, jint b) { // 这里可以简单地返回ab但为了演示我们调用一个独立的C函数 std::cout “[C] Received add request: “ a “ “ b std::endl; jint result a b; // jint可以直接当int用 return result; } // 实现阶乘函数 JNIEXPORT jlong JNICALL Java_com_example_jni_NativeCalculator_factorial (JNIEnv *env, jobject obj, jint n) { std::cout “[C] Calculating factorial of “ n std::endl; if (n 0) { // 在JNI中可以抛出Java异常。这里简单返回-1表示错误。 // 更佳实践是使用env-ThrowNew()抛出IllegalArgumentException。 return -1; } long long fact 1; for (int i 1; i n; i) { fact * i; } return (jlong)fact; // 将C的long long转换为jlong }同时为了模拟封装一个已有的C库我们在cpp/calculator.h和cpp/calculator.cpp里创建一个简单的C类// calculator.h #ifndef CALCULATOR_H #define CALCULATOR_H class Calculator { public: int multiply(int a, int b); double divide(double a, double b); }; #endif// calculator.cpp #include “calculator.h” int Calculator::multiply(int a, int b) { return a * b; } double Calculator::divide(double a, double b) { if (b 0.0) { // 在实际项目中这里应该抛出异常或返回错误码 return 0.0; } return a / b; }然后我们可以在native_calculator.cpp中调用这个Calculator类展示如何封装更复杂的C对象。这里为了简化先实现最基本的。实操心得在JNI函数内部使用std::cout打印日志对于调试非常有用。这些输出会打印在启动Java程序的控制台上比如在VSCode的终端里能让你清晰地看到C代码是否被调用、参数是否正确。4. 编译与构建生成动态链接库这是将C代码变成Java能加载的库的关键步骤。我们使用CMake来管理这个跨平台的构建过程。4.1 编写CMakeLists.txt在项目根目录创建CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(NativeCalculatorJNI) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键找到Java的JNI头文件路径 find_package(JNI REQUIRED) if (JNI_FOUND) message(STATUS “JNI Include directories: ${JNI_INCLUDE_DIRS}“) message(STATUS “JNI Libraries: ${JNI_LIBRARIES}“) include_directories(${JNI_INCLUDE_DIRS}) endif() # 包含我们自己生成的头文件目录 include_directories(${PROJECT_SOURCE_DIR}/include) # 添加C源文件 add_library(NativeCalculator SHARED cpp/native_calculator.cpp cpp/calculator.cpp ) # 设置输出库的名称和位置 set_target_properties(NativeCalculator PROPERTIES # 设置输出库文件的基础名System.loadLibrary(“NativeCalculator”)会找这个名字 OUTPUT_NAME “NativeCalculator” # 将库文件输出到项目根目录的lib子目录下 LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib # 在Windows上确保生成.dll而不是.lib SUFFIX “.dll” ) # 在非Windows系统上库文件后缀通常是.so if (UNIX AND NOT APPLE) set_target_properties(NativeCalculator PROPERTIES SUFFIX “.so”) elseif (APPLE) set_target_properties(NativeCalculator PROPERTIES SUFFIX “.dylib”) endif() # 链接JNI库在某些平台可能需要 target_link_libraries(NativeCalculator ${JNI_LIBRARIES})这个CMake脚本做了几件重要的事find_package(JNI REQUIRED)自动查找你系统中JDK的路径并设置好JNI_INCLUDE_DIRS包含jni.h的目录和JNI_LIBRARIES。这是解决“找不到jni.h”错误的关键。include_directories告诉编译器去哪里找头文件包括JNI的头文件和我们自己生成的头文件。add_library(... SHARED)指定我们要构建一个共享库即动态链接库。set_target_properties精细控制输出文件的名称和位置。我们统一输出到lib目录方便管理。4.2 执行CMake构建打开终端在项目根目录执行以下命令假设使用MinGW Makefiles生成器在Windows上# 创建一个构建目录并进入 mkdir build cd build # 生成构建系统。注意指定生成器如果用的是Visual Studio则指定“-G “Visual Studio 16 2019””等。 # 这里以MinGW为例 cmake -G “MinGW Makefiles” .. # 开始编译 cmake --build .如果一切顺利你会在项目根目录的lib文件夹下找到生成的动态库NativeCalculator.dllWindows或libNativeCalculator.soLinux。常见问题1CMake找不到JNI如果CMake配置时报错找不到JNI可以手动指定JAVA_HOME。在CMake命令中传递参数cmake -G “MinGW Makefiles” -DJAVA_HOME“C:\path\to\your\jdk-17” ..或者确保你的JAVA_HOME系统环境变量已正确设置。**常见问题2编译错误“undefined reference to__imp_JNI_CreateJavaVM’...”** 这通常是在链接阶段找不到JNI库。确保target_link_libraries(NativeCalculator ${JNI_LIBRARIES})这一行被正确执行。在Windows MinGW环境下有时需要明确链接-ljvm并指定库路径。可以在CMakeLists.txt中手动添加link_directories(${JAVA_HOME}/lib/server) # 或 ${JAVA_HOME}/jre/lib/amd64/server target_link_libraries(NativeCalculator ${JNI_LIBRARIES} jvm)5. 运行、测试与问题排查库已经生成现在回到Java端运行我们的测试程序。5.1 运行Java程序在终端中确保位于项目根目录然后执行# 编译Java类如果之前没编译或修改过 javac java/com/example/jni/NativeCalculator.java -d ./java_out # 运行程序并指定本地库的搜索路径 # -Djava.library.path./lib 告诉JVM去./lib目录下找动态库 java -Djava.library.path./lib -cp ./java_out com.example.jni.NativeCalculator如果一切正常你将看到类似以下输出[C] Received add request: 10 20 10 20 30 [C] Calculating factorial of 5 5! 120恭喜你成功实现了Java通过JNI调用C代码。5.2 常见错误与排查技巧实录在实际操作中你几乎一定会遇到各种错误。下面是我踩过的坑和解决方法错误1java.lang.UnsatisfiedLinkError: no NativeCalculator in java.library.path含义JVM在java.library.path指定的路径里找不到名为NativeCalculator的库。排查检查System.loadLibrary的参数是否与库文件的基础名一致不含“lib”前缀和“.dll/.so”后缀。检查-Djava.library.path参数指定的路径是否正确是否包含了库文件所在的目录。检查库文件是否真的存在文件名是否正确。在Windows上应该是NativeCalculator.dll在Linux上应该是libNativeCalculator.so。使用System.load(“绝对路径/库文件.dll”)来直接加载排除路径问题。错误2java.lang.UnsatisfiedLinkError: com.example.jni.NativeCalculator.add(II)I含义找到了库但找不到指定的native方法。这是最棘手的错误之一。排查函数签名不匹配这是最常见原因。使用javap -s -p com.example.jni.NativeCalculator查看Java类中方法的签名Signature。确保C函数名完全匹配Java_包名_类名_方法名并且参数类型JNIEnv*, jobject, ...和返回类型JNIEXPORT jint JNICALL等完全正确。一个空格或下划线错误都会导致失败。C函数未正确导出确保在C实现文件中函数声明前有JNIEXPORT和JNICALL。如果是C文件.cpp确保头文件被extern “C”包裹以防止C的名称修饰Name Mangling破坏函数名。库文件未更新修改C代码后必须重新编译生成新的动态库并确保Java程序加载的是最新的库。依赖缺失你的C库可能依赖其他DLL或SO。在Windows上可以用Dependency Walker工具检查在Linux上用ldd libNativeCalculator.so检查。将缺失的依赖库放到java.library.path下或系统库路径中。错误3java.lang.UnsatisfiedLinkError: Can‘t load IA 32-bit .dll on a AMD 64-bit platform含义Java是64位的但加载的DLL是32位的或者反过来。解决统一架构。确保你的JDKjava -version查看、C编译器g --version和目标平台CMake生成器都是同一架构都是x64或都是x86。错误4fatal error: jni.h: No such file or directory含义C编译器找不到jni.h。解决确保CMakeLists.txt中find_package(JNI)成功并且include_directories(${JNI_INCLUDE_DIRS})被调用。可以手动添加包含路径例如在CMake中include_directories(“C:/Program Files/Java/jdk-17/include” “C:/Program Files/Java/jdk-17/include/win32”)路径根据你的JDK安装位置调整。调试技巧在C代码中打印日志如前所述使用std::cout或printf。这是最直接的调试方式。使用Process Explorer(Windows)或lsof(Linux)查看你的Java进程是否真的加载了你期望的DLL/SO文件。分步验证先写一个最简单的native方法比如无参无返回值确保加载和调用流程通。再逐步增加复杂度。6. 进阶处理复杂数据类型与异常基础的整型传递跑通了但实际项目不可能只传int和long。我们经常需要处理字符串、数组、甚至自定义Java对象。6.1 字符串传递Java的String在JNI中对应jstring但它不能直接当C的char*用。必须通过JNIEnv的函数进行转换。Java端public native String greet(String name);C端实现JNIEXPORT jstring JNICALL Java_com_example_jni_NativeCalculator_greet (JNIEnv *env, jobject obj, jstring jname) { // 1. 将jstring转换为C风格的字符串UTF-8编码 const char *cName env-GetStringUTFChars(jname, nullptr); if (cName nullptr) { return nullptr; // 内存不足异常已抛出 } // 2. 使用C字符串注意不要修改它 std::string greeting “Hello, “; greeting cName; greeting “!”; // 3. 释放从Java获取的字符串资源必须 env-ReleaseStringUTFChars(jname, cName); // 4. 将C字符串转换回jstring并返回 return env-NewStringUTF(greeting.c_str()); }关键点GetStringUTFChars和ReleaseStringUTFChars必须成对出现防止内存泄漏。使用GetStringUTFChars获取的指针是只读的不要试图修改它。如果处理中文字符可能需要使用GetStringChars和ReleaseStringCharsUTF-16编码。6.2 数组传递传递数组也很常见比如Java传递一个int[]给C处理。Java端public native int sumArray(int[] numbers);C端实现JNIEXPORT jint JNICALL Java_com_example_jni_NativeCalculator_sumArray (JNIEnv *env, jobject obj, jintArray jarray) { // 1. 获取数组长度 jsize length env-GetArrayLength(jarray); // 2. 获取数组元素的指针。第二个参数是isCopy可以传nullptr不关心或传一个jboolean指针获取是否拷贝的信息。 jint *cArray env-GetIntArrayElements(jarray, nullptr); if (cArray nullptr) { return 0; // 异常已抛出 } // 3. 操作数组元素 jint sum 0; for (jsize i 0; i length; i) { sum cArray[i]; } // 4. 释放数组元素。第三个参数是模式 // 0: 将内容拷贝回Java数组并释放C数组 // JNI_COMMIT: 将内容拷贝回Java数组但不释放C数组 // JNI_ABORT: 不拷贝回直接释放C数组 env-ReleaseIntArrayElements(jarray, cArray, 0); return sum; }关键点GetIntArrayElements可能返回一个指向Java数组原始数据的指针也可能返回一个拷贝的指针。这由JVM决定出于性能优化考虑。无论如何最后都必须调用对应的ReleaseIntArrayElements。对于其他基本类型数组有类似函数GetFloatArrayElements,GetDoubleArrayElements等。对于对象数组Object[]处理更复杂需要使用GetObjectArrayElement和SetObjectArrayElement逐个元素操作。6.3 异常处理在C代码中如果发生错误最好的做法是向Java层抛出异常而不是返回错误码。C端抛出异常JNIEXPORT void JNICALL Java_com_example_jni_NativeCalculator_doSomethingRisky (JNIEnv *env, jobject obj, jint value) { if (value 0) { // 1. 查找异常类 jclass exceptionClass env-FindClass(“java/lang/IllegalArgumentException”); if (exceptionClass ! nullptr) { // 2. 抛出异常 env-ThrowNew(exceptionClass, “Input value cannot be negative.”); } // 抛出异常后C函数应立即返回。JVM会接管控制流跳转到Java端的异常处理块。 return; } // ... 正常逻辑 ... }Java端捕获异常try { calculator.doSomethingRisky(-5); } catch (IllegalArgumentException e) { System.out.println(“Caught exception from native code: “ e.getMessage()); }重要原则在JNI函数中抛出异常后应该立即清理本地资源如释放字符串、数组等然后返回。不要继续执行可能出错的代码。JVM会在控制权返回Java后处理这个异常。7. 内存管理与性能优化要点JNI编程不当很容易引起内存泄漏和性能问题。7.1 局部引用与全局引用局部引用Local Reference在JNI函数中创建的大部分Java对象通过NewObject,NewStringUTF,NewIntArray等返回的或通过GetObjectArrayElement获取的都是局部引用。它们在本JNI函数返回后会自动被JVM垃圾回收。但是如果在单个本地方法中创建了大量局部引用或者创建了大的对象如大数组应该及时调用env-DeleteLocalRef(ref)来手动删除防止局部引用表溢出。全局引用Global Reference如果你需要在多个本地方法调用间或者在不同线程间持有一个Java对象的引用就必须创建全局引用。使用env-NewGlobalRef(localRef)将局部引用提升为全局引用。务必在不再需要时调用env-DeleteGlobalRef(globalRef)来释放否则会导致Java对象永远无法被回收造成内存泄漏。弱全局引用Weak Global Reference类似全局引用但不会阻止垃圾回收。使用env-NewWeakGlobalRef创建env-DeleteWeakGlobalRef释放。在使用前需要用env-IsSameObject(ref, nullptr)检查对象是否已被回收。7.2 性能优化建议减少JNI调用次数JNI调用开销相对较大。应避免在循环中频繁进行JNI调用。尽量一次传递数据如数组在C侧处理完再一次性返回结果。使用直接缓冲区Direct Buffer对于需要Java和C共享的大块内存如图像数据、音频流使用java.nio.ByteBuffer.allocateDirect创建直接缓冲区然后在JNI中通过GetDirectBufferAddress获取内存地址直接操作。这避免了在Java堆和本地堆之间复制数据性能极高。谨慎使用Get/ReleaseTypeArrayElements如前所述这个函数可能拷贝数据。如果你确信不会修改数组内容或者可以接受修改但不需写回可以使用JNI_ABORT模式。对于只读访问考虑使用GetPrimitiveArrayCritical但它会暂停垃圾回收必须尽快用ReleasePrimitiveArrayCritical释放。缓存ID查找类FindClass、方法IDGetMethodID、字段IDGetFieldID是比较耗时的操作。如果频繁使用应该在类初始化时如JNI_OnLoad函数中一次性查找并缓存为全局引用或静态变量。7.3 线程安全JNIEnv指针JNIEnv*是线程相关的。你不能在一个线程中保存另一个线程的JNIEnv指针并在后者中使用。如果需要在本地创建的线程非由JVM创建的线程中调用JNI函数必须首先通过JavaVM*指针可以在JNI_OnLoad中保存调用AttachCurrentThread来获取当前线程的JNIEnv并在线程结束前调用DetachCurrentThread。// 保存JavaVM指针 JavaVM* g_jvm nullptr; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { g_jvm vm; // ... 其他初始化如缓存类、方法ID ... return JNI_VERSION_1_8; // 返回你使用的JNI版本 } // 在本地线程中的函数 void* nativeThreadFunc(void* arg) { JNIEnv *env; // 将当前线程附加到JVM jint result g_jvm-AttachCurrentThread((void**)env, nullptr); if (result ! JNI_OK) { // 处理错误 return nullptr; } // 现在可以安全使用env调用JNI函数了 // ... // 分离线程 g_jvm-DetachCurrentThread(); return nullptr; }封装JNI接口是一个细致活从环境搭建、代码编写、编译构建到调试排错每一步都需要耐心。但一旦打通它就为你的Java应用打开了通往高性能、底层能力的大门。我个人的经验是把JNI接口设计得尽可能简单、稳定复杂的逻辑放在C库内部实现同时做好错误处理和资源管理避免内存泄漏。对于复杂的对象交互可以考虑使用像SWIG这样的工具来自动生成部分粘合代码但对于学习和控制细节来说手写JNI代码仍然是不可替代的。最后多写日志分模块测试是保证JNI项目稳定性的不二法门。