在Ubuntu上体验一下JNI开发

系统环境

软件 版本 备注
VMware 16.1.2 build-17966106 16 Pro,虚拟机
Ubuntu 20.04.2 LTS 安装在虚拟机中的操作系统
CLion 2021.1.2 运行C++项目的软件

1.安装SDK

1、安装JDK(编译运行Java语言的)

sudo apt install openjdk-8-jdk-headless

2、安装gcc(编译运行C语言的)

sudo apt install gcc

3、安装g++ (编译运行C++语言的)

sudo apt install g++

4、安装make

sudo apt install make
Software Version
gcc 9.3.0
g++ 9.3.0
make 4.2.1

2.CLion创建一个C++项目

1、创建一个可运行的C++项目:

2、由于是第一次创建,它要求我配置gcc,g++,make的路径:

3. idea创建一个Java项目

1、New -> Project...: 我们这里选择创建一个简单的Java项目,然后点下一步

第一次使用时 Project SDK 可能需要你选择一下你的 JDK 的安装路径。

2、这步直接点击 Next: 不去选择模板

3、最后,给你的Java项目取一个名字就可以完成创建了

4. 编写使用JNI的Java项目

4.1 动态链接库

为了简单,我们在 Idea 中创建一个 HelloWorld 的 Java类:

package org.coderead.jvm.example.jni;

public class HelloWorld {
  public static native void hi();

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

此时,直接运行程序,我们会遇到第一个错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: org.coderead.jvm.example.jni.HelloWorld.hi()V
	at org.coderead.jvm.example.jni.HelloWorld.hi(Native Method)
	at org.coderead.jvm.example.jni.HelloWorld.main(HelloWorld.java:7)

出现这个错误的主要原因:org.coderead.jvm.example.jni.HelloWorld.hi() 首先不是一个Java方法,而是一个 Java Native Interface,而 native 方法会在程序加载 HelloWorld 这个类时,需要去加载动态链接库

Java程序中,加载动态链接库的函数是 System.loadLibrary

4.2 Java程序从哪里加载动态链接库

我们对程序进行改写:

package org.coderead.jvm.example.jni;

public class HelloWorld {
    public static native void hi();

    static {
        System.loadLibrary("jni");
    }
    public static void main(String[] args) {
        hi();
    }
}

△ 文件名格式: lib + 文件名 + .so,因此,在linux系统上运行时,真正寻找的文件实际上是 libjni.so
抛出以下错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no jni in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
	at java.lang.Runtime.loadLibrary0(Runtime.java:871)
	at java.lang.System.loadLibrary(System.java:1124)
	at org.coderead.jvm.example.jni.HelloWorld.<clinit>(HelloWorld.java:7)

java.library.path 是一个JVM环境变量,我们写个程序,打印它当前的值:

package org.coderead.jvm.example.jni;

public class PrintLibraryPath {

    public static void main(String[] args) {
        System.out.println(System.getProperty("java.library.path"));
    }
}

我们得到以下输出:

/usr/java/packages/lib/amd64:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib

所以我们把动态链接库文件,放在上面的任意一个文件夹中,就可以被找到了,个人比较倾向于放在 /lib 中。
△ 所以,System.loadLibaray 会在上面打印出来的目录中寻找 lib + 文件名 + .so ,比如 libjni.so。

5 JNI 头文件

虽然,我们现在需要的是动态链接库的文件(C/C++程序库),但是先不要着急,我们还需要先准备好 JNI 头文件。

5.1 生成 JNI 文件

javac 编译命令,可以帮助我们生成 JNI 文件。命令格式如下:

javac [-encoding utf8] -h targetDir sourceFile

-h 指定放置生成的本机标头文件的位置

例如我在 Idea 的 Terminal 中输出以下命令进行编译:

javac -h /home/geekziyu/IdeaProjects/MainJava/jni/ src/org/coderead/jvm/example/jni/HelloWorld.java
  • /home/geekziyu/IdeaProjects/MainJava 是我的项目根目录;
  • sourceFile 文件的路径用的是 / 且结尾是 .java,否则会出现找不到文件的情况;
  • 我在这条命令中,使用的是相对路径而不是绝对路径;

如图所示,org_coderead_jvm_example_jni_HelloWorld.h 就是我们生成的 JNI 文件。

5.2 注意事项

★ 只有当Java文件中有native方法时,才会生成JNI头文件,否则,就不会有JNI文件。

package org.coderead.jvm.example.jni;

public class HelloWorld2 {

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

这样一个程序,你用以下命令编译,也不会/jni/ 下生成 JNI 文件的!

javac -h /home/geekziyu/IdeaProjects/MainJava/jni/ src/org/coderead/jvm/example/jni/HelloWorld2.java

★ 如果Java程序中引用了其他的类,比如extends,需要关联引用的文件才能生成JNI头文件

比如说,这个例子中,Child 继承自 Parent:

package org.coderead.jvm.example.jni;

public class Child extends Parent {

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

    public native void sayHi();

    @Override
    public void introduce() {
        sayHi();
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.introduce();
    }
}

Parent 类也十分简单:

package org.coderead.jvm.example.jni;

public class Parent {

    public void introduce() {
        System.out.println("I am a father");
    }
}

执行编译的情况如下:

正确的命令应该是:

javac -h /home/geekziyu/IdeaProjects/MainJava/jni/ src/org/coderead/jvm/example/jni/Parent.java src/org/coderead/jvm/example/jni/Child.java
  • 前一条命令显然是执行失败了,报出了 error: cannot find symbol
  • 后一条命令把 src/org/coderead/jvm/example/jni/Parent.java 带着一起编译就能正常生成 JNI 文件了。

5.3 函数原型解释

我们再来看一下 Child 和 HelloWorld 生成的 JNI 文件有什么不同之处:
以下截取自 org_coderead_jvm_example_jni_Child.h 文件:

/*
 * Class:     org_coderead_jvm_example_jni_Child
 * Method:    sayHi
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_coderead_jvm_example_jni_Child_sayHi
  (JNIEnv *, jobject);

以下截取自 org_coderead_jvm_example_jni_HelloWorld.h 文件:

/*
 * Class:     org_coderead_jvm_example_jni_HelloWorld
 * Method:    hi
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_coderead_jvm_example_jni_HelloWorld_hi
  (JNIEnv *, jclass);
  • Child.h 中的 native 方法是非静态方法,所以 Java_org_coderead_jvm_example_jni_Child_sayHi 的第二个参数是 jobject 类型,即 Java语言中的对象本身的引用this;
  • HelloWorld.h 中的 native 方法是静态方法,所以 Java_org_coderead_jvm_example_jni_HelloWorld_hi 的第二个参数是 jclass 类型。

6. 回CLion编辑C/C++项目

  1. 创建 src/jniinclude/jni 文件夹;
  2. 把刚才 Idea 中生成的 org_coderead_jvm_example_jni_HelloWorld.horg_coderead_jvm_example_jni_Child.h 拷贝到 Clion项目的 include/jni 目录中;

经过上面两步之后,我们的 Clion 中的项目的文件夹如图所示:

打开拷贝过来的HelloWorld.h,你会看到如下错误:

此时,你需要在你的 CMakeLists.txt 加上以下代码:

include_directories("/usr/lib/jvm/java-8-openjdk-amd64/include")
include_directories("/usr/lib/jvm/java-8-openjdk-amd64/include/linux")

需要注意的是,/usr/lib/jvm/java-8-openjdk-amd64 是你的 JDK 安装目录,
你可以在Terminal中使用命令 sudo find /usr/ -name 'jni.h' 进行搜索

你可能还需要点击 Reload Changes... 才能让你不再报错:

  1. 接着我们新建 C/C++ Source File: src/jni/org_coderead_jvm_example_jni_Child.cpporg_coderead_jvm_example_jni_HelloWorld.cpp文件

src/jni/org_coderead_jvm_example_jni_Child.cpp 文件内容如下:

#include "../../include/jni/org_coderead_jvm_example_jni_Child.h"
#include <iostream>
/*
 * Class:     org_coderead_jvm_example_jni_Child
 * Method:    sayHi
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_coderead_jvm_example_jni_Child_sayHi
        (JNIEnv *, jobject) {
    std::cout << "C say hi to you!" << std::endl;
}

src/jni/org_coderead_jvm_example_jni_HelloWorld.cpp 文件内容如下:

#include "../../include/jni/org_coderead_jvm_example_jni_HelloWorld.h"
#include <iostream>
/*
 * Class:     org_coderead_jvm_example_jni_HelloWorld
 * Method:    hi
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_coderead_jvm_example_jni_HelloWorld_hi
        (JNIEnv *env, jclass clazz) {
    std::cout << "C Hello World!" << std::endl;
}

6.1 编译so文件

sudo g++ -shared -fPIC -I /usr/lib/jvm/java-8-openjdk-amd64/include -I /usr/lib/jvm/java-8-openjdk-amd64/include/linux -o /lib/libjni.so src/jni/org_coderead_jvm_example_jni_Child.cpp src/jni/org_coderead_jvm_example_jni_HelloWorld.cpp 

现在可以找到生成的 /lib/libjni.so:


我的 /lib 文件是软连接文件,它指向 /usr/lib,所以 /lib 中的文件和 /usr/lib 的文件是同一批文件

7 回到IDEA运行Java程序

我们运行 HelloWorld.java 程序,这下正常输出结果:

再试试 Child.java 程序,输出结果:

参考文档

《UBUNTU 中 PYCHARM 添加启动图标(桌面快捷方式)》阅读

我自己在文件夹 /usr/share/applications 创建的 Desktop Entry 文件,看不到桌面图标,头痛...

《在ubuntu下安装openjdk》阅读

posted @ 2021-07-26 23:34  极客子羽  阅读(639)  评论(0编辑  收藏  举报