最近在整理学习笔记的时候发现了去年年中记录的 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 的文件

 1public class JNITest {
 2    static {
 3        System.load("/home/ubuntu/workspace/java/jni/mynativelib.so");
 4    }
 5
 6    //申明一个无参的native方法,而且返回空
 7    public native void greet();
 8
 9    //测试
10    public static void main(String[] args) {
11        JNITest test = new JNITest();
12        test.greet();
13    }
14}

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

1-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 文件

1javac JNITest.java

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

1javah JNITest

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

 1/* DO NOT EDIT THIS FILE - it is machine generated */
 2#include <jni.h>
 3/* Header for class JNITest */
 4
 5#ifndef _Included_JNITest
 6#define _Included_JNITest
 7#ifdef __cplusplus
 8extern "C" {
 9#endif
10/*
11 * Class:     JNITest
12 * Method:    greet
13 * Signature: ()V
14 */
15JNIEXPORT void JNICALL Java_JNITest_greet
16  (JNIEnv *, jobject);
17
18#ifdef __cplusplus
19}
20#endif
21#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

1#include <stdio.h>
2#include <jni.h>
3#include "JNITest.h"
4
5JNIEXPORT void JNICALL Java_JNITest_greet(JNIEnv *env, jobject obj) {
6	printf("hello liubang...\n");
7	printf("this is implemented by c and called by java!\n");
8	return;
9}

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

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

**Step5:**运行 Java 程序

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

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

2 JNI in Package

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

首先实现一个 test.HelloJNI 类

 1package test;
 2
 3public class HelloJNI {
 4
 5    static {
 6        System.loadLibrary("hello");
 7    }
 8
 9    private native void sayHello();
10
11    public static void main(String[] args) {
12        new HelloJNI().sayHello();
13    }
14
15}

然后编译 Java 程序

1javac test/HelloJNI.java

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

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

实现头文件中定义的函数

1#include <stdio.h>
2#include <jni.h>
3#include "include/test_HelloJNI.h"
4
5JNIEXPORT void JNICALL Java_test_HelloJNI_sayHello(JNIEnv *env, jobject this) {
6    printf("hello liubang\n");
7    return;
8}

编译成动态链接库

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

然后运行 Java 程序

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