Java JNI 编程

1.5k 技术 发表评论

本文会解决几个问题。

  1. JDK 包中的 include/ 目录下的.h头文件有什么用?
  2. 如何用 C 语言编写一个 Java 可以调用的函数?
  3. Java 如何调用 C 语言编写的方法?

1 简介

有时候,我们需要用更底层的 C 或者 C++ 语言来实现 Java 难以实现的功能,比如克服 Java 中的内存管理和性能限制。

Java 提供了本机接口(Java Native Interface,也叫JNI)来支持 Java 对 C/C++ 功能调用。

这篇文章假设你有下面的知识:

  1. Java
  2. C 和 GCC 编译器(本文暂不讨论 C++)
  3. 如果你的系统是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中也不会有这个参数,反过来也成立。

JNIEXPORTJNICALL可以暂且忽略。

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.chello.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指定动态库文件的位置,这里我们指定用当前目录。

输出5Hello JNI,程序完美运行!

参考资料

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

昵称 *