最近在整理学习笔记的时候发现了去年年中记录的 JNI 学习笔记,由于存放在了为知笔记中,而如今为知笔记已经不再免费,于是想到了将其重新整理一遍,一来可以巩固所学,二来能将其迁移到本地

1 简介

有时候,使用 native code(c/c++)来克服 Java 中的内存管理和性能的局限性是很有必要的。Java 支持 native codes,被称作 Java Native Interface(JNI)。

JNI 非常难,毕竟它牵涉到了两种编程语言。假设聪明的你对 Java 和 C/C++以及 GCC 编译器已经有所了解。那么下面就一起来一步步学习 JNI 吧。

2 开始

2.1 用 c 语言实现第一个 JNI 程序

Step1: 创建一个名字为 JNITest.java 的文件

public class JNITest {
    static {
        System.load("/home/ubuntu/workspace/java/jni/mynativelib.so");
    }

    //申明一个无参的native方法,而且返回空
    public native void greet();

    //测试
    public static void main(String[] args) {
        JNITest test = new JNITest();
        test.greet();
    }
}

首先用静态代码块加载本地动态链接库"mynativelib.so"。对于静态代码块,我相信写过 Java 的你应该非常清楚,它只会在类被加载的时候执行一次。这个动态链接库会被添加到 Java 的 library path(保存在 Java 系统变量 java.library.path)中,如果加载失败,就会抛出UnsatisfiedLinkError异常。也可以使用 JVM 启动参数来加载该动态链接库到 Java 的 library path 当中:

-Djava.library.path=path_to_lib

说明:JNI 中有两种常见的加载动态链接库的写法,一个是System.load(filename),另一个是System.loadLibrary(libname),二者的区别在于,第一种需要的是文件的全名称,而第二种只需要动态链接库的库名(windows 下省略.dll 后缀,linux 下省略 lib 前缀和.so 后缀)。

然后我们使用native关键字申明一个名为greet的 native instance method。被 native 修饰的方法不能有方法体。

接下来编译 JNITest.java 文件

javac JNITest.java

Step2: 生成 C/C++头文件 JNITest.h

javah JNITest

执行上述命令后自动生成一个名为 JNITest.h 的文件

/* 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:    greet
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNITest_greet
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

该头文件声明了Java_JNITest_greet的函数原型,Java 中的函数对应到 C 中的命名规则为:Java_{package_and_classname}_{method_name}(JNI arguments)

其中 JNI arguments:

  • JNIEnv *: 指向 JNI 执行环境的指针,通过它可以访问到所有的 JNI 方法
  • jobject: 指向 Java 中的"this"

**Step3:**用 c 语言实现头文件中的函数 - jnitest.c

#include <stdio.h>
#include <jni.h>
#include "JNITest.h"

JNIEXPORT void JNICALL Java_JNITest_greet(JNIEnv *env, jobject obj) {
	printf("hello liubang...\n");
	printf("this is implemented by c and called by java!\n");
	return;
}

**Step4:**编译动态链接库

gcc -fPIC -shared -I /opt/app/java/include/ -I /opt/app/java/include/linux/ jnitest.c -o mynativelib.so

**Step5:**运行 Java 程序

java JNITest

====output===
ubuntu@vm-911:~/workspace/java/jni$ java JNITest
hello liubang...
this is implemented by c and called by java!

于是第一个 c 语言实现的 JNI 程序就跑起来了,是不是很简单。别着急,这只是个开始!

2 JNI in Package

通常情况下,几乎所有的 Java 类都有自己的 package 而不是直接使用默认的无名的 package。那么对于使用了 package 的 Java 类如何来创建 JNI 程序呢,其实跟开始的例子几乎是一样的。

首先实现一个 test.HelloJNI 类

package test;

public class HelloJNI {

    static {
        System.loadLibrary("hello");
    }

    private native void sayHello();

    public static void main(String[] args) {
        new HelloJNI().sayHello();
    }

}

然后编译 Java 程序

javac test/HelloJNI.java

接着生成 C/C++头文件

//将生成的头文件存放在include文件夹
javah -d include test.HelloJNI

实现头文件中定义的函数

#include <stdio.h>
#include <jni.h>
#include "include/test_HelloJNI.h"

JNIEXPORT void JNICALL Java_test_HelloJNI_sayHello(JNIEnv *env, jobject this) {
    printf("hello liubang\n");
    return;
}

编译成动态链接库

gcc -fPIC -shared -I /opt/app/java/include/ -I /opt/app/java/include/linux/ HelloJNI.c -o libhello.so

然后运行 Java 程序

java -Djava.library.path=. test.HelloJNI

====output====
ubuntu@vm-911:~/workspace/java/jni/demo1$ java -Djava.library.path=. test.HelloJNI
hello liubang