Java之native函数

本文介绍了Java中native方法的实现。因Java加载类和解释执行效率不如C++,且底层操作能力有限,可通过JNI实现native方法。文中以Hello World为例,详细阐述了Java程序编写、头文件生成、C++函数编写及dll生成的步骤,最后还对多native函数情况进行了思考。


我们在使用hashmap时,有时候会看到hashcode的源码。最原始的hashcode源码是位于Object类中,是一个native方法。native方法是指,它没有方法体,不是用Java语言实现的。而是通过动态链接库(dll)加载而调用的函数。动态链接库一般使用C、C++、汇编语言等编写,这就实现了和底层函数的交互,类似于API的功能。我们今天就来实现一个native方法。当然,操作系统是windows10,64位。Linux也类似,只不过生成的动态链接库是so文件。

1、native方法介绍

Java虽然功能强大,但是存在一个缺点,就是JVM本身也是一个动态链接库(内部有class文件的解释器),它加载类和解释执行的效率不如直接编译的C++高。再有就是,Java设计系统API等底层操作时可能无能为力。一些经常调用的函数,或者和操作系统交互的函数必须用其他语言来完成。

Java中的JNI(Java Native Interface)就是实现native方法的途径。它通过C/C++的编程接口(头文件)来达到和C/C++交互的目的。

我们先来看一下,native方法的执行过程。

首先,在类被加载时,需要加载native方法实现的动态链接库,因此这段加载代码必须是静态加载器中的程序段。比如下面的ExampleClass类:

public class ExampleClass {
    static {
        System.load("example.dll");
    }
    
    public native void example();
}

然后,当JVM执行到native函数时,查找已经加载好的动态链接库,如果找到对应函数的实现,则把执行权转交操作系统,操作系统将进程调度至动态链接库,开始函数执行,Java程序则等待其返回值;如果未找到则报错(属于Error型异常,也就是JVM级别的异常,不可捕获)。

了解到这一点,我们就可以编写自己的native方法了。程序员出生时说的第一句话(滑稽)是Hello World,所以我们也以Hello World为例。环境如下:

操作系统:windows 10 64bit

Java IDE:IDEA 2018.3–64bit(eclipse的小伙伴自行对应)

JDK:1.8(AMD64)

C++ IDE:codeblocks 17

C++ compiler:mingw64-g++-8.1(注意,codeblocks自带的编译器是32位的,生成的动态链接库也是32位的,不能被64位的JVM加载。所以稍后我们将手动修改codeblocks的编译器路径)

2、Java程序的编写和头文件生成

我们首先新建Java工程,编写一个简单的Java程序,直接在主类中就行。如下:

public class Main {

    static {
        System.load("D:\\jni.dll");
    }

    public native static void hello(); // 必须有static,因为静态函数不能直接调用同一个类的非静态函数

    public static void main(String[] args) {
        hello();
    }
}

这个hello函数就是我们要实现的native函数,功能是输出字符串“Hello World”。在这个主类中,需要加载D:\jni.dll,所以把它写在static初始化器中。

接下来,我们需要编译出class文件,并生成一个C++的头(.h)文件。我们单击idea的一键编译运行即可。这次运行是一定会报错的,提示无法加载动态链接库。但我们不需要运行,只需要那个class文件。

找到你的class文件所在位置。比如,我的是在工程目录\out\production\doorsymbol下,其中doorsymbol是我的工程名。eclipse的小伙伴自行找。反正看到Main.class就对了。如下图:

在这里插入图片描述
然后,打开命令行,转到当前目录下。输入命令:

javah -jni Main

之后便会看到一个Main.h文件。这一步算大功告成。

3、C++函数编写及dll生成

3.1、创建C++工程

这一步我们将生成dll文件。我们用codeblocks新建一个C++工程。注意,这次不是控制台程序,而是动态链接库了。工程目录不要有空格,比如我选择D:\cppprojects\jni

这一步一定要选这个:

在这里插入图片描述

3.2、修改编译器设置

刚才提到,codeblocks自带的编译器是mingw32,无法编译64位的动态链接库。所以我们需要把默认编译器路径改成mingw64所在的路径。没有mingw64的小伙伴可以下载这里的压缩包并解压。链接:https://pan.baidu.com/s/1jsxoCnntqCo4xRBIFF1ACw
提取码:tian

解压后,转到mingw64文件夹中。如图:

在这里插入图片描述我们只需要把编译器路径改成这个文件夹路径即可。

codeblocks–settings–compiler

在这里插入图片描述点可执行工具链(executable tool chain)选项卡,把编译器的安装目录改成刚才的mingw64文件夹。

接着,修改编译选项。单击compiler settings选项卡。选择遵循c++11规范,以及生成x86_64目标。尤其是生成64位目标至关重要,这将直接决定我们生成动态链接库的格式。

在这里插入图片描述
单击确定。

然后再修改调试器路径,也就是gdb。如下图:

settings–debugger settings

在这里插入图片描述单击default选项卡,把调试器路径(executable path)改成刚才的mingw64文件夹下的bin/gdb.exe即可。单击确定。

3.3、添加头文件

编译器设置好之后,我们的工程中,有一个默认的main.cpp文件和main.h文件。其中main.h文件没有任何用处,把它删掉。我们需要用的是刚才用class文件生成的Main.h。打开工程目录,把这个头文件移动到和main.cpp同级目录下。然后,打开你的jdk路径(以下简称java_home),如下图:

在这里插入图片描述把以下两个头文件放入和main.cpp同级的目录下:

%java_home%\include\jni.h
%java_home%\include\win32\jni_md.h

最终的效果如下图

在这里插入图片描述

3.4、修改Main.h以及实现其中的函数

打开Main.h,发现它声明了一个函数Java_Main_hello。这个函数就是Java中native函数hello的真正接口。另外,我们看到头文件包含了#include <jni.h>,我们把它改成#include "jni.h"

然后打开main.cpp文件,删除全部内容,改成如下:

#include "Main.h"

#include <iostream>

using namespace std;

JNIEXPORT
void JNICALL Java_Main_hello(JNIEnv *, jclass)
{
    cout << "Hello World!" << endl;
}

注意一定要包含头文件Main.h,并且函数头必须与Main.h中的声明完全相同。为避免出错,建议复制粘贴。

3.5、编译生成dll文件并运行Java程序

这一切都完毕之后,单击编译按钮,如果没有出错,则生成动态链接库成功。我们到工程目录下bin\Debug中,找到一个dll文件。把它复制到D盘根目录下,并改名为jni.dll

在这里插入图片描述
返回IDEA,再次运行Java程序,可见输出Hello World!字样。这样,我们的native函数就圆满成功了。

4、题外话

思考:如果一个类有多个native函数是什么情况?如果有多个含native函数的类,又是什么情况?

答案:一个类多个native,只生成一个头文件,但头文件中有多个函数声明。多个含native的类,则每个类分别生成一个头文件。

### Java 中封装 Native 函数并使用 JNI 调用本地方法 在 Java 中封装 Native 方法并通过 JNI 调用本地 C 或 C++ 实现是一种常见的跨语言编程方式。以下是实现这一功能的具体说明和示例。 #### 1. 定义 Java 类并声明 Native 方法 首先,在 Java 文件中定义一个类,并通过 `native` 关键字声明需要调用的本地方法。这些方法的实际逻辑将在 C 或 C++ 中实现。 ```java public class JNITest { static { // 加载本地库文件,名称为 mynative 的动态链接库 (.so/.dll) System.loadLibrary("mynative"); } // 声明 native 方法 public native static void JavaHello(); public static void main(String[] args) { // 调用 native 方法 JavaHello(); } } ``` 上述代码展示了如何加载本地库以及声明一个静态的原生方法 `JavaHello()`[^1]。 --- #### 2. 编译 Java 文件生成 `.class` 文件 为了生成头文件供 C/C++ 使用,需先将 Java 文件编译成 `.class` 文件: ```bash javac JNITest.java ``` 此命令会生成名为 `JNITest.class` 的文件[^2]。 --- #### 3. 创建 JNI 头文件 利用 JDK 提供的工具 `javah` 可以为 Java 类生成对应的 JNI 头文件。该头文件包含了用于绑定 Java 和 C/C++ 的函数签名。 运行以下命令生成头文件: ```bash javah -jni JNITest ``` 这会产生一个名为 `JNITest.h` 的头文件,其中包含类似如下内容: ```cpp /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class JNITest */ #ifndef _Included_JNITest #define _Included_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class: JNITest * Method: JavaHello * Signature: ()V */ JNIEXPORT void JNICALL Java_JNITest_JavaHello(JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ``` 这个头文件提供了 JNI 接口所需的函数原型[^3]。 --- #### 4. 实现 C/C++ 动态库 接下来编写实际的 C/C++ 代码来实现 `JavaHello()` 方法的功能。例如: ```cpp #include <jni.h> #include <stdio.h> // 对应于 Java_JNITest_JavaHello 的实现 JNIEXPORT void JNICALL Java_JNITest_JavaHello(JNIEnv *env, jclass clazz) { printf("Hello from the C world!\n"); } ``` 保存以上代码到文件 `mynative.c` 并将其编译为目标平台支持的共享库(Linux 下为 `.so`, Windows 下为 `.dll`)。对于 Linux 系统可以执行以下命令完成编译: ```bash gcc -shared -fPIC -o libmynative.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux mynative.c ``` 此处 `${JAVA_HOME}` 是指安装路径下的 JDK 主目录位置。 --- #### 5. 运行程序验证结果 最后一步是在终端上启动 Java 应用程序以测试整个流程是否正常工作: ```bash java -Djava.library.path=. JNITest ``` 如果一切配置无误,则会在控制台看到输出消息:“Hello from the C world!”。 --- ### 总结 通过上述过程可以看出,JNI 技术允许开发者轻松地让 Java 与底层硬件更接近的语言如 C/C++ 结合起来操作资源密集型任务或访问特定设备驱动等功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值