在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++项目
- 创建
src/jni
和include/jni
文件夹; - 把刚才 Idea 中生成的
org_coderead_jvm_example_jni_HelloWorld.h
和org_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...
才能让你不再报错:
- 接着我们新建 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
:
我的/lib
文件是软连接文件,它指向/usr/lib
,所以/lib
中的文件和/usr/lib
的文件是同一批文件
7 回到IDEA运行Java程序
我们运行 HelloWorld.java 程序,这下正常输出结果:
再试试 Child.java 程序,输出结果:
参考文档
《UBUNTU 中 PYCHARM 添加启动图标(桌面快捷方式)》阅读
我自己在文件夹
/usr/share/applications
创建的 Desktop Entry 文件,看不到桌面图标,头痛...
《在ubuntu下安装openjdk》阅读