从 Java 到 C++:用 JNI 实现字符串拼接全解析

举报
不惑 发表于 2025/01/07 19:54:19 2025/01/07
【摘要】 通过本文的示例,你将掌握如何使用JNI连接Java和C++,实现简单的功能扩展。这种技术可以轻松应用于更复杂的业务场景,为项目开发带来更高效的性能支持。

在许多业务场景中,字符串操作可能因复杂逻辑或高频运算成为性能瓶颈。针对这种情况,我们可以借助 C++ 的高效特性,通过 Java Native Interface (JNI),将字符串拼接功能从 Java 扩展到 C++ 实现,为性能敏感场景提供支持。本文以字符串拼接功能为例,展示如何利用 JNI 技术完成从 Java 到 C++ 的调用,包括完整的实现过程、编译步骤和运行测试。
image.png

JNI 调用 C++ 的基础流程

JNI 提供了一种在 Java 和本地代码(如 C++)之间交互的标准机制。其核心流程如下:

第一步:编写Java代码

创建一个名为StringConcatenator.java的文件,声明本地方法,定义 Java 的 native 方法

  • 使用 native 关键字声明方法,仅需声明而无需实现。
  • 使用 System.loadLibrarySystem.load 加载本地动态链接库。
package com.neo.controller;

public class StringConcatenator {

    // 声明native方法
    public native String concatenate(String str1, String str2);

    // 加载本地动态链接库
    static {
        String projectDir = System.getProperty("user.dir");  // 获取当前项目目录
        System.load(projectDir + "\\dll\\StringConcatenator.dll");
    }

    public static void main(String[] args) {
        StringConcatenator concatenator = new StringConcatenator();
        String result = concatenator.concatenate("Hello, ", "World!");
        System.out.println("Concatenated String: " + result);
    }

}

需要注意的是,如果本地库中包含多个函数,仅需调用一次 System.load 即可完成整个库的加载。加载时,只需指定库的名称,此外,在使用 javac 编译 Java 代码(例如 hello.java)时,Java 编译器实际上并不会检查 native 方法(如 helloWorld)的具体实现是否存在。因此,即使尚未完成本地方法的实现,也可以顺利生成 .class 字节码文件。


第二步:编译Java程序.java并生成C/C++头文件.h

从 JDK 8 开始,应该使用“ javac -h ”来编译 Java 程序并生成名为JNI.h的 C/C++ 头文件,如下所示:

javac -h . JNI.java

选项生成 C/C++ 标头并将其放置在指定的目录中(在上面的示例中, '.'表示当前目录)。在JDK 8之前,需要使用javac编译Java程序并使用专用的javah实用程序生成C/C++标头,如下所示。 javah实用程序在 JDK 10 中不再可用。

javac JNI.java
javah JNI

编译Java文件生成class文件:(可能会遇到字符集的问题)

javac -encoding UTF-8 -h . StringConcatenator.java

image.png

生成的头文件内容如下:

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

#ifndef _Included_com_neo_controller_StringConcatenator
#define _Included_com_neo_controller_StringConcatenator
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_neo_controller_StringConcatenator
 * Method:    concatenate
 * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_neo_controller_StringConcatenator_concatenate
  (JNIEnv *, jobject, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif
  • 参数JNIEnv*jobject分别是JNI环境指针和Java对象的引用。

需要特别注意,native 方法对应的实现函数通常被 extern "C" 包围,这是为了让 C++ 编译器采用 C 风格的函数命名规则,而非默认的 C++ 命名规则。

为什么需要 extern "C"

C++ 支持函数重载,因此在编译时会对函数名进行一种称为 名称修饰(Name Mangling) 的处理,以确保不同重载函数能够被正确区分。而 Java 中通过 JNI 调用的本地方法要求使用 C 风格的命名协议,函数名称必须与 JNI 头文件生成的声明完全一致。通过 extern "C",可以关闭 C++ 的名称修饰功能,确保生成的函数名与 Java 头文件中的声明匹配。

extern "C" {
    JNIEXPORT void JNICALL Java_HelloWorld_printHelloWorld(JNIEnv *, jobject);
}

关于 JNI 的头文件

生成的头文件中会包含 JNI 相关的引用,例如:

#include <jni.h>

这个 jni.h 文件是 Java 提供的标准头文件,用于定义 JNI 接口,其路径位于 JDK 的安装目录下。

第三步:实现C++代码

创建一个名为StringConcatenator.cpp的文件,并将生成的.h文件,拷贝到月cpp文本同一层级目录下,然后实现拼接逻辑:

  • 在 C++ 中通过 JNI API 实现头文件中声明的函数逻辑。
  • 使用 JNI 提供的 API 操作 Java 数据类型,例如字符串或数组。
    image.png

代码如下:

#include "com_neo_controller_StringConcatenator.h"
#include <iostream>
#include <string>

JNIEXPORT jstring JNICALL Java_com_example_jni_StringConcatenator_concatenate
        (JNIEnv *env, jobject obj, jstring str1, jstring str2) {
    // 获取Java字符串的C风格字符串表示
    const char *cStr1 = env->GetStringUTFChars(str1, nullptr);
    const char *cStr2 = env->GetStringUTFChars(str2, nullptr);

    // 拼接字符串
    std::string result = std::string(cStr1) + std::string(cStr2);

    // 释放资源
    env->ReleaseStringUTFChars(str1, cStr1);
    env->ReleaseStringUTFChars(str2, cStr2);

    // 返回新的Java字符串
    return env->NewStringUTF(result.c_str());
}
  1. 使用GetStringUTFChars获取Java字符串的C字符串表示。
  2. 使用C++标准库的std::string进行拼接。
  3. 拼接完成后,释放C字符串的内存,并通过NewStringUTF将结果返回为Java字符串。

第四步:编译动态链接库

  • 使用 C++ 编译器(如 g++)生成动态链接库。
  • 确保包含 JNI 头文件和平台相关的路径。

如果 JAVA_HOME 环境变量已正确配置:

g++ -shared -o StringConcatenator.dll StringConcatenator.cpp -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32"

如果未配置 JAVA_HOME,请使用绝对路径:

假设你的 JDK 安装在 C:\Program Files\Java\jdk1.8.0_65,运行:

g++ -shared -o StringConcatenator.dll StringConcatenator.cpp -I"C:\Program Files\Java\jdk1.8.0_65\include" -I"C:\Program Files\Java\jdk1.8.0_65\include\win32"

image.png

在编译 C++ 的本地方法实现代码为动态链接库时,需要特别关注以下编译选项及其作用:

-fPIC

  • 全称为 Position Independent Code,表示生成与位置无关的代码。
  • 这样可以确保共享库在内存中加载到任何地址时,都能正确运行。
  • 在使用 -shared 选项时必须启用此选项,否则编译期可能会报错。

-shared

  • 告诉编译器生成一个共享链接库,而非普通的可执行文件。
  • 此选项会自动处理链接时的符号导出。

-o

  • 指定输出文件名。
  • 在 Linux 下,动态链接库的命名规则为 libxxx.so 的形式,例如 libhelloWorld.so

-I

  • 指定头文件搜索路径,用于告诉编译器 JNI 所需的头文件所在目录。
  • 一般需要包含:
    - $JAVA_HOME/include
    - $JAVA_HOME/include/linux(根据操作系统选择子目录)。

第五步:运行Java程序

调用 Java 中的 native 方法,验证 C++ 的实现功能。现在我将编译好的dll文件,放置到项目的dll文件夹中,然后同通过main方法运行程序。
image.png

经验总结

代码实现总结

  1. Java层定义本地方法:通过native修饰符定义方法并加载动态链接库。
  2. C++实现核心逻辑:通过JNI接口与Java交互,实现高效的字符串操作。
  3. 动态链接库集成:使用g++编译生成Linux动态链接库。

项目优点

  1. 简洁清晰:实现了基本的字符串拼接功能,适合学习JNI的入门案例。
  2. 高性能扩展:为后续添加更复杂的字符串处理功能打下基础。
  3. 平台独立性:Java层完全独立,只需为不同平台生成对应的动态链接库即可。

常见问题及解决方案

UnsatisfiedLinkError 错误

  • 检查动态链接库路径和名称是否正确。
  • 确保动态库所在目录已包含在系统路径中,或使用绝对路径加载库。

返回字符串乱码

  • Java 的字符串使用 UTF-8 编码,需确保 C++ 返回的字符串格式兼容 UTF-8。

内存泄漏

  • 使用 GetStringUTFChars 获取的字符串必须通过 ReleaseStringUTFChars 释放内存。
  • 若涉及复杂数据交互,建议使用智能指针或手动资源管理。

通过本文的示例,你将掌握如何使用JNI连接Java和C++,实现简单的功能扩展。这种技术可以轻松应用于更复杂的业务场景,为项目开发带来更高效的性能支持。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。