系统环境

软件

版本

备注

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++项目:

在Ubuntu上体验一下JNI开发_java

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

在Ubuntu上体验一下JNI开发_linux_02

3. idea创建一个Java项目

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

在Ubuntu上体验一下JNI开发_ide_03

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

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

在Ubuntu上体验一下JNI开发_ide_04

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

在Ubuntu上体验一下JNI开发_java_05

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


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

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


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

在Ubuntu上体验一下JNI开发_linux_06

如图所示,​​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");
}
}


执行编译的情况如下:

在Ubuntu上体验一下JNI开发_c++_07

正确的命令应该是:

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/jni​​ 和 ​​include/jni​​ 文件夹;
  2. 把刚才 Idea 中生成的 ​​org_coderead_jvm_example_jni_HelloWorld.h​​ 和 ​​org_coderead_jvm_example_jni_Child.h​​ 拷贝到 Clion项目的 ​​include/jni​​ 目录中;

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

在Ubuntu上体验一下JNI开发_linux_08

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

在Ubuntu上体验一下JNI开发_linux_09

此时,你需要在你的 ​​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'​​ 进行搜索
在Ubuntu上体验一下JNI开发_JVM_10

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

在Ubuntu上体验一下JNI开发_ide_11

  1. 接着我们新建 C/C++ Source File: ​​src/jni/org_coderead_jvm_example_jni_Child.cpp​​和​​org_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​​:

在Ubuntu上体验一下JNI开发_ide_12

在Ubuntu上体验一下JNI开发_java_13
我的 ​​/lib​​ 文件是软连接文件,它指向 ​​/usr/lib​​,所以 ​​/lib​​ 中的文件和 ​​/usr/lib​​ 的文件是同一批文件

7 回到IDEA运行Java程序

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

在Ubuntu上体验一下JNI开发_java_14

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

在Ubuntu上体验一下JNI开发_linux_15

参考文档

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

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