本文会解决几个问题。
- JDK 包中的
include/
目录下的.h
头文件有什么用? - 如何用 C 语言编写一个 Java 可以调用的函数?
- Java 如何调用 C 语言编写的方法?
1 简介
有时候,我们需要用更底层的 C 或者 C++ 语言来实现 Java 难以实现的功能,比如克服 Java 中的内存管理和性能限制。
Java 提供了本机接口(Java Native Interface,也叫JNI)来支持 Java 对 C/C++ 功能调用。
这篇文章假设你有下面的知识:
- Java
- C 和 GCC 编译器(本文暂不讨论 C++)
- 如果你的系统是Windows,还需要知道使用 Cygwin 或者 MinGW
2 Java 代码
第 1 步:编写一个使用 C 代码的 Java 类 HelloJNI.java
:
class HelloJNI {
static {
// 使用 static 关键词让程序在初始化时加载本地库文件,
// 每个系统对应的文件名是:
// - Windows系统:hello.dll
// - Unix系统:libhello.so
// - MacOS系统:libhello.dylib
// 这个库里面包含一个名为 sayHello 函数
System.loadLibrary("hello");
}
// 声明一个本地方法,它接收一个整型参数,不返回任何值。
// 用关键字 native 表示该方法是用另一种语言实现的,它应该
// 包含在上面的 hello 库中
public native void sayHello(int times);
// 调用本地方法 sayHello()
public static void main(String[] args) {
new HelloJNI().sayHello(5);
}
}
在使用时,我们通过 VM 参数-Djava.library.path=/path/to/lib
将库文件包含到 Java 的库路径中,下面我们会进行实际操作。(如果找不到该库,程序将抛出运行时错误UnsatisfiedLinkError
)
3 生成 C 头文件
首先,我们编译Java程序HelloJNI.java
,生成 C/C++ 头文件HelloJNI.h
。
从 JDK 8 开始,我们使用 javac -h
来完成这个任务,如下所示:
javac -h ./ HelloJNI.java
注意上面有一个-h
选项,我们用它来指定生成C/C++ 头文件,以及该头文件保存的位置。这里我们直接用./
来保存在当前目录。
如果是 JDK 8 之前的版本,则需要javac
配合javah
来生成 C/C++ 头文件,如下所示。注意,javah
命令在 JDK 10 中不再可用。
javac HelloJNI.java
javah HelloJNI
检查头文件HelloJNI.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif
这个头文件声明了一个 C 函数Java_HelloJNI_sayHello
,如下所示:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject, jint);
这个 C 函数的命名约定是:Java_{package_and_classname}_{function_name}(JNI_arguments)
,包名中的点被下划线代替。
其中3
个参数分别是:
JNIEnv *
: JNI 环境的引用,用它可以访问所有的 JNI 函数。jobject
: Java 对象本身,相当于this
。jint
:这是自定义的参数,即我们在Java 代码中的int times
参数。如果 Java 方法不带参数的话,那C中也不会有这个参数,反过来也成立。
JNIEXPORT
和JNICALL
可以暂且忽略。
extern "C"
语法仅在 C++ 编译器中有用,它告诉 C++ 编译器,请用 C 语言的函数命名协议来编译这段代码。C 和 C++ 的函数命名协议不一样,C++ 支持函数重载,并使用mangling scheme
来切分重载的函数。
4 实现 C 程序HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jint times) {
jint i;
for (i = 0; i < times; i++) {
printf("#%d - Hello JNI\n", i);
}
return;
}
将这段程序另存为HelloJNI.c
,和HelloJNI.java
在同一目录下。
其中,
jni.h
:是 JDK 提供的一个头文件,在 JDK 安装目录下,一般在<JAVA_HOME>\include
目录或者其子目录下。注意,JRE 目录是没有include/
目录的。stdio.h
:这个不用多说,C 语言提供的标准输入输入库。HelloJNI.h
:上一步生成的 C 头文件。
C 函数 Java_HelloJNI_sayHello
功能也比较简单,接收times
参数,循环打印 Hello JNI
字符串 times
次。
5 编译 C 程序HelloJNI.c
这是 JNI 编程最难的部分,在不同操作系统(Windows、Mac OS X、Ubuntu)、JDK版本(32 位、64 位)找到合适的编译器,以及正确的编译器选项,下面我们一一讲解。
5.1 Windows系统,64 位 JDK
这里我们要用到 Cygwin,对于 Windows系统,需要注意以下几点:
- Windows/Intel 使用的指令集: x86 是 32 位指令集;i868 是 x86 的增强版(也是 32 位);x86_64(或 amd64)是 64 位指令集。
- 32 位编译器可以在 32 位或 64 位(向后兼容)Windows 上运行,但 64 位编译器只能在 64 位 Windows 上运行。
- 64 位编译器可以生成 32 位或 64 位的可执行文件。
- 如果您使用 Cygwin 的 GCC,目标可能是本机 Windows 或 Cygwin。如果目标是本机 Windows,则可以在 Windows 下运行。但是,如果目标是 Cygwin,需要 Cygwin 运行时环境( cygwin1.dll)才能运行,因为 Cygwin 是 Windows 下的 Unix 模拟器。
对于 64 位 JDK,需要使用 64 位的编译器,对应的是 MinGW-W64,可以在 Cygwin 中选择包 mingw64-x86_64-gcc-core
来安装。
首先,将JAVA_HOME
环境变量设置为 JDK 安装目录(例如“ c:\program files\java\jdk10.0.x”)。
接着,使用以下命令编译HelloJNI.c
为hello.dll
:
> x86_64-w64-mingw32-gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
使用的编译器选项是:
-IheaderDir
: 指定头文件所在目录。其中JAVA_HOME
是设置为 JDK 安装目录的环境变量。-shared
: 指定生成共享库。-o outputFilename
: 用于设置输出文件名“ hello.dll”。
然后,可以通过file
命令检查生成的文件的类型,如下表明hello.dll
是 64 位 (x86_64) 的本地Windows DLL。
> file hello.dll
hello.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows
还可以试试nm
命令,它列出共享库中所有的符号,并查找sayHello()
函数。检查Java_HelloJNI_sayHello
具有类型T
的函数名称。
> nm hello.dll | grep say
00000000624014a0 T Java_HelloJNI_sayHello
5.2 Windows,32 位 JDK
这里不做阐述。
5.3 Linux, 64 位 JDK
这里包括 Ubuntu/CentOS/Debian/RedHat… 等发行版。
首先,将环境变量JAVA_HOME
指向 JDK 安装目录(该目录应包含include
子目录):
$ export JAVA_HOME=/your/java/installed/dir
$ echo $JAVA_HOME
使用 gcc 编译 C 程序HelloJNI.c
为共享模块libhello.so
:
$ gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
5.4 Mac OS X,64 位 JDK
首先,将环境变量JAVA_HOME
指向 JDK 安装目录(该目录应包含include
子目录):
$ export JAVA_HOME=/your/java/installed/dir
$ echo $JAVA_HOME
使用 gcc 编译 C 程序 HelloJNI.c
为动态共享模块libhello.dylib
:
$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c
6 运行 Java 程序
➜ java -Djava.library.path=./ HelloJNI
#0 - Hello JNI
#1 - Hello JNI
#2 - Hello JNI
#3 - Hello JNI
#4 - Hello JNI
我们用 VM 选项-Djava.library.path
指定动态库文件的位置,这里我们指定用当前目录。
输出5
次Hello JNI
,程序完美运行!
参考资料