C/C++与Java混合的JNI编程
<p>Java与C++混合编程可以实现两种语言的优势结合,C++的程序性能很高且支持强大的系统调用能力,Java则生态丰富且开发效率较高。JNI是Java与C++进行混合编程的关键桥梁,本章将基于JNI技术讲述Java与C++混合编程的方法和技巧。</p><h2 id="1-java与jni">1. Java与JNI</h2>
<h3 id="11-什么是java">1.1. 什么是Java?</h3>
<p><strong>Java</strong>是一种高级编程语言,也是一个计算平台(通常指Java虚拟机)。最初由Sun Microsystems公司(后被Oracle收购)的James Gosling和他的团队在1995年发布。Java语言的设计目标是简单性、健壮性和跨平台兼容性。以下是Java的一些关键特点:</p>
<ul>
<li><strong>面向对象:</strong> Java是一种面向对象的语言,这意味着它基于对象和类的概念。对象代表现实世界中的实体或概念,而类是创建对象的模板。</li>
<li><strong>平台无关性:</strong> Java的一个核心特性是“一次编写,到处运行”(<code>Write Once, Run Anywhere</code>,WORA)。Java程序在执行前会被编译成字节码,这种中间形式的代码可以在任何安装了Java虚拟机(JVM)的设备上运行。</li>
<li><strong>自动内存管理:</strong> Java提供了自动垃圾回收机制,这意味着程序员不需要手动管理内存的分配和释放,从而减少了内存泄漏和其他内存相关错误。</li>
<li><strong>丰富的标准库:</strong> Java拥有一个庞大的标准库(也称为Java API),提供了大量预先构建的类和接口,用于处理文件输入/输出、网络编程、多线程编程、数据结构等。</li>
<li><strong>跨平台兼容性:</strong> Java不仅可以在不同的操作系统上运行,还可以在嵌入式系统、移动设备和大型服务器上运行。</li>
</ul>
<p>Java的应用场景广泛,是目前最流行的后端系统开发语言,此外Java还是Android系统的主要编程语言,绝大部分的Android应用程序都基于Java语言进行开发。</p>
<h3 id="12-什么是jvm">1.2. 什么是JVM?</h3>
<p><strong>JVM</strong>(<code>Java Virtual Machine</code>)是一个可以执行Java字节码的虚拟计算机。它是Java平台的核心组成部分,提供了Java程序运行所需的环境。JVM是Java语言能做到“一次编写,到处运行”的基础。以下是JVM的一些关键特点和功能:</p>
<ul>
<li><strong>平台无关性</strong>:JVM的主要目的是实现Java的跨平台特性。Java源代码被编译成平台无关的字节码,这些字节码可以在任何安装了相应JVM的设备上运行。</li>
<li><strong>字节码解释器</strong>:JVM包含一个字节码解释器,它负责将字节码转换为特定平台的机器码。这个过程使得Java程序可以在不同的操作系统和硬件上运行。</li>
<li><strong>即时编译器(JIT)</strong>:为了提高性能,现代JVM通常包含一个即时编译器。JIT编译器会将热点代码(频繁执行的代码)编译成优化的机器码,以提高执行效率。</li>
<li><strong>垃圾回收</strong>:JVM负责管理内存分配和回收。它提供了自动垃圾回收机制,帮助程序员管理内存,减少内存泄漏和其他内存相关错误。</li>
<li><strong>安全沙箱</strong>:JVM提供了一个安全的执行环境,可以限制代码对系统资源的访问。这有助于防止恶意代码对系统造成破坏。</li>
<li><strong>类加载器</strong>:JVM包含一个类加载器子系统,负责动态加载、验证和准备类文件以供执行。类加载器确保类文件的完整性和安全性。</li>
<li><strong>本地接口</strong>:JVM提供了与本地库交互的接口(如JNI,Java Native Interface),允许Java代码调用本地代码(C/C++等),以实现特定功能或性能优化。</li>
<li><strong>多线程支持</strong>:JVM支持多线程执行,允许程序同时执行多个任务。</li>
</ul>
<h3 id="13-什么是jni">1.3. 什么是JNI?</h3>
<p><strong>JNI</strong>(<code>Java Native Interface</code>)是一个允许Java代码与本地代码(如:C/C++)进行交互的接口。通过JNI,Java应用程序可以调用本地库中的函数,也可以被本地代码调用,它是实现Java与C/C++混合编程的关键机制。</p>
<p>JNI主要包含以下两部分内容:</p>
<ul>
<li>Java代码与本地代码交互的接口。</li>
<li>支持JNI开发的一套开发工具,如: <code>javah</code>、<code>javac</code>等。</li>
</ul>
<p>JNI接口的官方文档:https://docs.oracle.com/en/java/javase/21/docs/specs/jni/index.html</p>
<h3 id="14-环境说明">1.4. 环境说明</h3>
<p>本章所有的示例代码的开发环境如下:</p>
<ul>
<li>操作系统: Ubuntu 24.04</li>
<li>JDK版本: 21.0.5</li>
<li>GCC版本: 13.3.0</li>
<li>开发工具:VSCode</li>
</ul>
<h2 id="2-开发环境搭建">2. 开发环境搭建</h2>
<h3 id="21-windows">2.1. Windows</h3>
<ol>
<li>
<p>在官网下载最新版本的安装包,官网下载地址:https://www.oracle.com/cn/java/technologies/downloads/<br>
<img src="https://img2024.cnblogs.com/other/504387/202508/504387-20250808074126601-2069142165.png"></p>
</li>
<li>
<p>双击安装包,根据提示一步步安装即可。</p>
</li>
<li>
<p>打开命令行输入一下命令,验证是否安装成功,如果有显示相应的版本号则说明安装成功。</p>
<pre><code class="language-bash">java -version
</code></pre>
</li>
</ol>
<h3 id="22-linuxubuntu">2.2. Linux(Ubuntu)</h3>
<p><strong>安装JDK:</strong></p>
<pre><code class="language-bash"># 1. 更新软件包列表
sudo apt update
# 3. 该命令将自动选择并安装最新的 LTS 版本,当前是 OpenJDK 21。
sudo apt install default-jdk
# 3. 验证是否安装成功,如果有显示相应的版本号则说明安装成功。
java --version
</code></pre>
<p><strong>设置环境变量:</strong></p>
<pre><code class="language-bash"># 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
</code></pre>
<h3 id="23-macos">2.3. macOS</h3>
<p>以下是通过Homebrew工具的安装步骤,确保已经安装Homebrew。</p>
<pre><code class="language-bash"># 1. 更新软件包列表
brew update
# 2.1 安装Oracle JDK的最新版本
brew install oracle-jdk
# 2.2 安装Open JDK的最新版本
brew install java
# 3. 验证是否安装成功,如果有显示相应的版本号则说明安装成功。
java --version
</code></pre>
<h3 id="24-open-jdk与oracle-jdk">2.4. Open JDK与Oracle JDK</h3>
<p>Java语言最初由Sun公司研发,并发布了<strong>Java SE</strong>(<code>Standard Edition</code>)的规范和开源的Open JDK。Sun公司后被Oracle公司收购,Oracle基于<code>Open JDK</code>开发了 <code>Oracle JDK</code>。</p>
<p><strong>Open JDK</strong>是一个完全开源的项目,遵循<code>GPL v2</code>许可。任何人都可以下载、使用、修改和分发它的代码。主要的Linux发行版(如:Fedora,Ubuntu等)提供OpenJDK作为默认的Java SE实现。</p>
<p><strong>Oracle JDK</strong>则基于<code>Open JDK</code>构建,但包含一些闭源组件,如Java插件、Java WebStart的实现和一些第三方组件。这些组件包括了一些商业功能,未开源。</p>
<p><code>Open JDK</code>和<code>Oracle JDK</code>都遵循<code>Java SE</code>的规范,只是<code>Oracle JDK</code>提供了更多商业版的未开源的功能。</p>
<h2 id="3-say-hello程序">3. <code>Say Hello</code>程序</h2>
<h3 id="31-新建sayhellojava">3.1. 新建<code>SayHello.java</code></h3>
<p>新建一个<code>say_hello</code>的测试目录,然后在该目录下新建一个<code>SayHello.java</code>文件,并编写如下代码:</p>
<pre><code class="language-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("陌尘");
}
}
</code></pre>
<p><strong>说明:</strong></p>
<ul>
<li>
<p>这里有两个被声明为<code>native</code>的方法,表示这两个方法需要native代码(C/C++)实现。这里一个是普通的类成员方法,一个是静态的类方法。</p>
<pre><code class="language-java">private native void sayHello(String name);
private static native void sayGoodbye(String name);
</code></pre>
</li>
<li>
<p><code>static</code>包含的代码块,表示在程序初始化时加载native动态库(<code>libhello.so</code>)</p>
<pre><code class="language-java">static {
System.loadLibrary("hello");
}
</code></pre>
</li>
</ul>
<h3 id="32-编译sayhellojava">3.2. 编译<code>SayHello.java</code></h3>
<pre><code class="language-bash">javac ./SayHello.java
</code></pre>
<p>执行完成后,会生成一个<code>SayHello.class</code>的字节码文件。</p>
<h3 id="33-生成sayhelloh">3.3. 生成<code>SayHello.h</code></h3>
<p>执行以下命令生成native代码的头文件</p>
<pre><code class="language-bash"># JDK 9.0 之前
javah -cp ./ -d ./ SayHello
# `-cp ./`表示设置classpath为当前目录,在当前目录下查找.class文件
# `-d ./`表示设置头文件的输出目录为当前目录
# JDK 9.0 及之后
javac -h ./ ./SayHello.java
# 第一个`./` 表示设置头文件的输出目录为当前目录
</code></pre>
<p>执行成功后会在当前目录下生成<code>SayHello.h</code>头文件,内容如下:</p>
<pre><code class="language-C++">/* 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
</code></pre>
<p><strong>代码说明:</strong></p>
<ul>
<li>
<p>头文件中包含两个函数分别和<code>.java</code>中的两个方法一一对应。</p>
</li>
<li>
<p>函数声明中开通的部分<code>JNIEXPORT void JNICALL</code>,这个与《导出接口的定义》一文中的<code>EAPI int CALLType</code>是不是非常类似?是的,它就是JNI提供的动态库导出接口声明和调用约定声明。</p>
</li>
<li>
<p>函数的命名非常有规律,其实它是遵循了JNI的函数命名规范:</p>
<pre><code class="language-bash">Java_{package_name}_{class_name}_{function_name}(JNI arguments)。
</code></pre>
</li>
<li>
<p>函数的参数</p>
<ul>
<li>第一个参数:<code>JNIEnv *env</code>是一个指向JNI运行环境的指针,提供了JNI接口的各种功能函数。</li>
<li>第二个参数:
<ul>
<li>如果是一个普通的成员方法则参数为<code>jobject obj</code>,指代java中的this对象,可以通过该参数来获取Java对象的方法和属性。</li>
<li>如果是一个静态的类方法则参数为<code>jclass cls</code>,指代java中的类,可以通过该参数来获取Java类的静态方法和静态属性。</li>
</ul>
</li>
<li>其他参数: 按从左到右的顺序与<code>.java</code>中声明的方法的参数一一对应。</li>
</ul>
</li>
</ul>
<h3 id="34-实现sayhellocp">3.4. 实现<code>SayHello.cp</code></h3>
<p>新建<code>SayHello.cpp</code>文件,并实现头文件声明的两个函数,内容如下:</p>
<pre><code class="language-C++">#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;
}
</code></pre>
<h3 id="35-编译sayhellocpp">3.5. 编译<code>SayHello.cpp</code></h3>
<pre><code class="language-bash">g++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux SayHello.cpp -o libhello.so
</code></pre>
<p>执行成功后会生成<code>libhello.so</code>文件。</p>
<h3 id="36-运行sayhello程序">3.6. 运行<code>SayHello</code>程序</h3>
<pre><code class="language-bash">java -Djava.library.path=./ SayHello
Hello, Spencer!
Goodbye, 陌尘!
</code></pre>
<p><code>-Djava.library.path=./</code>表示在当前目录下查找<code>libhello.so</code>。</p>
<h2 id="4-jni开发的关键步骤">4. JNI开发的关键步骤</h2>
<p>根据前面“Say Hello程序”的示例,可以总结出JNI开发的关键步骤和原理如下:</p>
<ol>
<li><strong>本地方法声明</strong>:在Java中,你可以声明一个或多个本地方法,这些方法没有Java实现,而是通过JNI与本地代码关联。</li>
<li><strong>生成头文件</strong>:使用<code>javah</code>(或<code>javac -h</code>)工具从Java类生成C/C++头文件,该头文件包含JNI函数的原型。</li>
<li><strong>实现本地方法</strong>:在C或C++代码中实现这些本地方法。这些方法必须遵循JNI的调用约定和数据类型规范。</li>
<li><strong>编译和加载</strong>:编译本地代码并生成动态链接库(如<code>.so</code>、<code>.dll</code>或<code>.dylib</code>文件),然后在Java程序中加载这些库。</li>
<li><strong>调用本地方法</strong>:Java程序可以通过JNI调用这些本地方法,就像调用普通Java方法一样。</li>
</ol>
<h2 id="5-数据类型的对应关系">5. 数据类型的对应关系</h2>
<h3 id="51-对应关系">5.1. 对应关系</h3>
<p>JNI定义了一套以<code>j</code>开头的C/C++的数据类型,与Java进行一一对应,他们之间的对应关系如下:</p>
<table>
<thead>
<tr>
<th>分类</th>
<th>Java数据类型</th>
<th>JNI数据类型</th>
<th>C/C++数据类型</th>
</tr>
</thead>
<tbody>
<tr>
<td>基础类型</td>
<td>boolean</td>
<td>jboolean</td>
<td><code>unsigned char</code>,相当于<code>uint8_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>byte</td>
<td>jbyte</td>
<td><code>signed char</code>,相当于<code>int8_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>char</td>
<td>jchar</td>
<td><code>unsigned short</code>,相当于<code>uint16_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>short</td>
<td>jshort</td>
<td><code>short</code>,相当于<code>int16_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>int</td>
<td>jint</td>
<td><code>int</code>,相当于<code>int32_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>long</td>
<td>jlong</td>
<td><code>long</code>,相当于<code>int64_t</code>。</td>
</tr>
<tr>
<td>基础类型</td>
<td>float</td>
<td>jfloat</td>
<td><code>float</code>,4字节</td>
</tr>
<tr>
<td>基础类型</td>
<td>double</td>
<td>jdouble</td>
<td><code>double</code>,8字节</td>
</tr>
<tr>
<td>引用类型</td>
<td>Object</td>
<td>jobject</td>
<td>jobject的定义:<br><code>class _jobject {};</code><br> <code>typedef _jobject *jobject;</code><br> 所以jobject的作用类似于<code>void*</code>,表示通用对象指针。</td>
</tr>
<tr>
<td>引用类型</td>
<td>Class</td>
<td>jclass</td>
<td><code>class _jclass : public _jobject {};</code><br> <code>typedef _jclass *jclass;</code></td>
</tr>
<tr>
<td>引用类型</td>
<td>String</td>
<td>jstring</td>
<td><code>class _jstring : public _jobject {};</code><br> <code>typedef _jstring *jstring;</code></td>
</tr>
<tr>
<td>引用类型</td>
<td>数组</td>
<td>jarray</td>
<td><code>class _jarray : public _jobject {};</code><br> <code>typedef _jarray *jarray;</code></td>
</tr>
<tr>
<td>引用类型</td>
<td>Throwable</td>
<td>jthrowable</td>
<td><code>class _jthrowable : public _jobject {};</code><br> <code>typedef _jthrowable *jthrowable;</code></td>
</tr>
</tbody>
</table>
<h3 id="52-类型说明">5.2. 类型说明</h3>
<p>Java的数据类型分<code>基础数据类型</code>(如<code>int</code>)和<code>引用数据类型</code>(如:<code>Object</code>、<code>Class</code>)。</p>
<p><strong>基础数据类型:</strong> 会直接转换为C/C++的基础数据类型,例如<code>int</code>类型映射为<code>jint</code>类型。由于 <code>jint</code>是C/C++类型,所以可以直接当作普通C/C++变量使用,而不用做任何转换。<br>
<strong>引用数据类型:</strong> 对象只会转换为一个C/C++指针,例如<code>Object</code>类型映射为<code>jobject</code>类型。由于指针指向Java虚拟机内部的数据结构,所以不可能直接在C/C++代码中操作对象,而是需要依赖JNIEnv环境对象。另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。</p>
<p>Java中的数组对应于C/C++中的jarray,它是一个通用的数组类型。而具体数据类型的数组,对应于Java数组的特定类型,对应关系如下。</p>
<table>
<thead>
<tr>
<th>Java数据类型</th>
<th>JNI数据类型</th>
</tr>
</thead>
<tbody>
<tr>
<td>boolean[]</td>
<td>jbooleanArray</td>
</tr>
<tr>
<td>byte[]</td>
<td>jbyteArray</td>
</tr>
<tr>
<td>char[]</td>
<td>jcharArray</td>
</tr>
<tr>
<td>short[]</td>
<td>jshortArray</td>
</tr>
<tr>
<td>int[]</td>
<td>jintArray</td>
</tr>
<tr>
<td>long[]</td>
<td>jlongArray</td>
</tr>
<tr>
<td>float[]</td>
<td>jfloatArray</td>
</tr>
<tr>
<td>double[]</td>
<td>jdoubleArray</td>
</tr>
<tr>
<td>Object[]</td>
<td>jobjectArray</td>
</tr>
</tbody>
</table>
<hr>
<p>历史文章推荐:</p>
<blockquote>
<p>01. 概念篇:什么是SDK</p>
<p>02. 概念篇:SDK的设计目标</p>
<p>03. 概念篇:接口设计与规范</p>
<p>04. 概念篇:接口注释与接口文档</p>
<p>05. 原理篇:字符集与字符编码(一)</p>
<p>06. 原理篇:字符集与字符编码(二)</p>
<p>07. 原理篇:多字节字符与宽字节字符</p>
<p>08. 原理篇:静态库、动态库与运行库</p>
<p>09. 跨平台:C++标准的版本</p>
<p>10. 跨平台:源码的保存格式与中文乱码问题</p>
<p>11. 跨平台:宏定义隔离平台差异</p>
<p>12. 跨平台:基础数据类型的定义</p>
<p>13. 跨平台:文件系统的操作</p>
<p>14. 跨平台:头文件包含的差异</p>
<p>15. 跨平台:导出接口的定义</p>
<p>16. 跨平台:字节序大端与小端</p>
<p>17. 跨平台:内存和资源管理</p>
<p>18. 工程篇:C/C++常用编译器</p>
<p>19. 工程篇:用VSCode搭建C++开发环境</p>
<p>20. 工程篇:CMake实现跨平台构建</p>
<p>21. 工程篇:VSCode中使用CMake插件运行和调试程序</p>
<p>22. 跨语言:跨语言的混合编程</p>
<p>23. 跨语言:C++接口设计和代码实现</p>
<p>24. 跨语言:C语言接口设计和代码实现</p>
<p>25. 跨语言:C/C++与Python混合编程(一)</p>
<p>26. 跨语言:C/C++与Python混合编程(二)</p>
<p>27. 跨语言:C/C++与Python混合编程(三)</p>
<p>28. 跨语言:C/C++与JavaScript的WebAssembly编程(一)</p>
<p>29. 跨语言:C/C++与JavaScript的WebAssembly编程(二)</p>
<p>30. 跨语言:C/C++与JavaScript的WebAssembly编程(三)</p>
<p>31. 跨语言:C/C++与Java混合的JNI编程(一)</p>
<p>32. 跨语言:C/C++与Java混合的JNI编程(二)</p>
<p>33. 跨语言:C/C++与Java混合的JNI编程(三)</p>
<p>34. 跨语言:Android NDK编程(一)</p>
<p>35. 跨语言:Android NDK编程(二)</p>
<p>36. 跨语言:C/C++与Swift&Objective-C混合编程(一)</p>
<p>37. 跨语言:C/C++与Swift&Objective-C混合编程(二)</p>
<p>附录A:计算机术语中成对出现的单词</p>
<p>附录B:计算机术语中常见的单词缩写</p>
<p>附录C:本专栏源码仓库及说明</p>
</blockquote>
<hr>
<p>大家好,我是陌尘。</p>
<p>IT从业10年+, 北漂过也深漂过,目前暂定居于杭州,未来不知还会飘向何方。</p>
<p>搞了8年C++,也干过2年前端;用Python写过书,也玩过一点PHP,未来还会折腾更多东西,不死不休。</p>
<p>感谢大家的关注,期待与你一起成长。</p>
<hr>
<hr>
<div align="center">【SunLogging】</div>
<div align="center">
<img src="https://images.cnblogs.com/cnblogs_com/luoweifu/2404783/o_240618085322_wechat_official_account.jpg">
</div>
<div align="center">扫码二维码,关注微信公众号,阅读更多精彩内容</div><br><br>
来源:https://www.cnblogs.com/luoweifu/p/19027767
頁:
[1]