1. 通过java定义需要调用的函数接口

需要用到的工具:IDEA或Eclipse

1.1 新建java工程 定义用到库函数的类

  1. import java.util.ArrayList;
  2. public class HelloWorld {
  3. public native void displayHelloWorld();
  4. public native String getString();
  5. public native float[] getCoordF();
  6. public native int[] getCoordI();
  7. }

我定义了一个HelloWorld类,其中用到 displayHelloWorld()方法,没有输入参数和返回值,作用就是显示一行”helloworld”字符串。
这次尝试主要是项目需求要得到返回三个浮点数的数组,所以定义了getCoordF()方法,返回一个float类型的数组,其他的方法就是顺带的测试一下。
native关键字指明该方法需要用到本地的c++库文件。

1.2 用命令行编译用到库函数的类生成.h头文件

命令行转到项目目录下,一开始项目目录中文件如下,有用的是HelloWorld.java文件:
image.png
执行命令

  1. javac -d . HelloWorld.java

生成HelloWorld.class的类文件
image.png
再通过类文件HelloWorld.class,执行命令

  1. javah -jni HelloWorld

注意:最后一个参数是包名,如package为package com.example.arserver 类名class为ARcompute
则执行

  1. javah -jni com.example.arserver.ARcompute

得到HelloWorld.h文件
image.png
注:根据java版本的不同,也有可能出现javah命令不存在的情况(比如我的windows,装的java是16.0.1)这是因为javah命令被合并到javac -h里面了,具体怎么用可以百度一下,有点忘了。我这个项目是在linux服务器上运行的,java版本是1.8.292,运行没有问题。
image.png
生成HelloWorld.h文件,这个文件就是jni通过java定义的接口函数自动生成的c++的函数类型声明,之后的函数实现只要以这个.h文件为原型去实现就可以啦。文件内容如下:

  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. #include <jni.h>
  3. /* Header for class HelloWorld */
  4. #ifndef _Included_HelloWorld
  5. #define _Included_HelloWorld
  6. #ifdef __cplusplus
  7. extern "C" {
  8. #endif
  9. /*
  10. * Class: HelloWorld
  11. * Method: displayHelloWorld
  12. * Signature: ()V
  13. */
  14. JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld
  15. (JNIEnv *, jobject);
  16. /*
  17. * Class: HelloWorld
  18. * Method: getString
  19. * Signature: ()Ljava/lang/String;
  20. */
  21. JNIEXPORT jstring JNICALL Java_HelloWorld_getString
  22. (JNIEnv *, jobject);
  23. /*
  24. * Class: HelloWorld
  25. * Method: getCoordF
  26. * Signature: ()[F
  27. */
  28. JNIEXPORT jfloatArray JNICALL Java_HelloWorld_getCoordF
  29. (JNIEnv *, jobject);
  30. /*
  31. * Class: HelloWorld
  32. * Method: getCoordI
  33. * Signature: ()[I
  34. */
  35. JNIEXPORT jintArray JNICALL Java_HelloWorld_getCoordI
  36. (JNIEnv *, jobject);
  37. #ifdef __cplusplus
  38. }
  39. #endif
  40. #endif

可以看到这里声明了四个函数,分别是前面在HelloWorld.java中定义的四个方法,前面的函数返回值和参数类型是jni自动生成的将java数据类型转换成的对应的c++数据类型,类型声明都存放在jni.h的头文件里,也就是第2行include进来的那个文件。
函数名就是 包名类名方法名 组成的,比较有辨识度。

1.3 找到jni.h的位置

这里有个坑,自动生成的HelloWorld.h文件好是好,但是他自动引入的#include<jni.h>不一定总是找的到,因为尖括号方式引入的头文件会在环境变量里找,一般是找不到的。网上多数的方法是找到这个文件,将他拖动到cpp源文件的同一目录下,然后将尖括号改成引号,#include"jni.h",这个方法是可行的,但是为了项目目录整洁一点,我还是通过g++编译的参数来引入头文件。
jni.h文件位于java SDK安装目录下,我这里的服务器/usr/lib/jvm/java-8-openjdk-amd64在这个目录下安装了java,(好像和前面安装的版本号对不上?不管了,找得到能用就行)。在这个目录的/include/目录下,就有我们要的jni.h文件。
image.png
除了这个文件,还需要jni_md.h文件,他在/include/liunx/
image.png

2. 编写cpp源文件并编译成动态库

注:.cpp文件貌似和.cc文件区别不大,g++都一样编译

2.1 开写

这里为了测试,我写了多文件
helloworld.cc

  1. // helloworld.cc
  2. #include "HelloWorld.h"
  3. #include "extra.h"
  4. #include <stdio.h>
  5. JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld
  6. (JNIEnv *, jobject){
  7. printf("Hello Java!");
  8. print();
  9. }
  10. JNIEXPORT jstring JNICALL Java_HelloWorld_getString
  11. (JNIEnv *env, jobject){
  12. return env->NewStringUTF((char *)"Hello from JNI !");
  13. }
  14. JNIEXPORT jfloatArray JNICALL Java_HelloWorld_getCoordF
  15. (JNIEnv *env, jobject){
  16. jfloatArray array_f = env->NewFloatArray(3);
  17. jint start=0, range=1;
  18. jfloat data[3] = {1.0, 2.0, 3.0};
  19. env->SetFloatArrayRegion(array_f, start++, range, data);
  20. env->SetFloatArrayRegion(array_f, start++, range, data+1);
  21. env->SetFloatArrayRegion(array_f, start++, range, data+2);
  22. return array_f;
  23. }
  24. JNIEXPORT jintArray JNICALL Java_HelloWorld_getCoordI
  25. (JNIEnv *env, jobject){
  26. jintArray array_i = env->NewIntArray(3);
  27. jint start=0, range=1;
  28. jint data[3] = {1, 2, 3};
  29. env->SetIntArrayRegion(array_i, start++, range, data);
  30. env->SetIntArrayRegion(array_i, start++, range, data+1);
  31. env->SetIntArrayRegion(array_i, start++, range, data+2);
  32. return array_i;
  33. }

extra.c

  1. // extra.c
  2. #include "extra.h"
  3. #include "stdio.h"
  4. void print(){
  5. printf("In extra.c");
  6. }

extra.h

  1. // extra.h
  2. void print();

注:这里编写程序有很多坑,要遵循jni定义的接口使用,各种数据类型的使用可以参照jni.h和网上的一些教程。我参考的是这篇:博客
如果最后报错UnsatisfiedLinkError:方法名(),一般就是c++源文件函数实现有问题。
还有要注意的是,JNI里c++和c的函数实现方式是不同的,这里也会导致报错。比如env->...*(env)->两种区别,多百度。

2.2 编译C文件

在命令行输入

  1. g++ -fPIC -c helloworld.cc -I /usr/lib/jvm/java-8-openjdk-amd64/include -I /usr/lib/jvm/java-8-openjdk-amd64/include/linux/
  1. 这里 `-I` 后面跟的是需要include的文件地址,这里引入刚刚找到的`jni.h``jni_md.h`,得到`helloworld.o`中间文件。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22026529/1627993301293-211aefd9-b8f4-4d99-9d1d-7b19e0fa648b.png#clientId=u582979cd-95ca-4&from=paste&id=u404dbb54&margin=%5Bobject%20Object%5D&name=image.png&originHeight=384&originWidth=1442&originalType=binary&ratio=1&size=56800&status=done&style=none&taskId=ub44347c8-0d6e-457d-8d92-50d54250612)<br />还需要编译`extra.c`文件:
  1. g++ -fPIC -c extra.cc

image.png

将得到的helloworld.oextra.o连接,生成动态链接库helloworld.so文件

  1. g++ -shared helloworld.o extra.o -o helloworld.so

image.png

2.3 写一个makefile

写一个makefile自动编译,省的老是敲命令行

  1. helloworld.so: helloworld.o extra.o
  2. g++ -shared helloworld.o extra.o -o helloworld.so
  3. helloworld.o: helloworld.cc HelloWorld.h extra.h
  4. g++ -fPIC -c helloworld.cc -I /usr/lib/jvm/java-8-openjdk-amd64/include -I /usr/lib/jvm/java-8-openjdk-amd64/include/linux/
  5. extra.o: extra.cc extra.h
  6. g++ -fPIC -c extra.cc
  7. clean:
  8. rm helloworld.o extra.o

注:在vscode里好像一个tab键和linux上的tab键不一样,如果makefile运行报错的话,就在linux上重新敲缩进,编译指令变红就ok了。
image.png
make 运行一下, make clean可以清除中间文件:
image.png
现在能一键生成.so文件了~

3. Java调用动态库函数

3.1 安装运行idea(Eclipse也行)报错处理

在安装的bin目录下运行./idea.sh启动idea,需要预装xmanager等图形界面程序。
出现报错java.awt.AWTError: Can't connect to X11 window server using 'localhost:12.0' as the value of the DISPLAY variable.
参考以下方法

  1. // idea图形界面启动不了:
  2. vncserver
  3. export DISPLAY=localhost:x // 看vnc启用的端口
  4. xhost +
  5. // 或者:
  6. exit
  7. // 重新登录

运行vncserver根据输出信息,或者ps -ef | grep vnc查看vnc服务的进程
我这边已经运行过vncserver进程了,所以采用后面的命令查看进程
image.png
可以看到在冒号后面的数字就是端口号,是1
运行 export DISPLAY=localhost:1
然后运行 xhost +
看到返回信息 access control disabled, clients can connect from any host就表示成功了
image.png
然后快乐地打开idea~

好像又出现了
X Input extension isnt available, error: {0}
java.lang.Throwable: toolbar creation trace 这样的报错
image.png
退出服务器,重新登录,然后就进去了。不知道什么原因,有可能是我上次登录之后没有退出?
image.png

3.2 写一个test程序,测试动态库

先把生成的helloworld.so文件移动到程序的目录下
测试文件编写如下

  1. public class test {
  2. static {
  3. // System.load("/root/IdeaProjects/hw/src/libTEST.so");
  4. // System.load("/root/IdeaProjects/hw/src/librelocalization.so");
  5. System.load("/root/IdeaProjects/hw/src/helloworld.so");
  6. }
  7. public native String displayHelloWorld();
  8. public static void main(String []args){
  9. HelloWorld hw = new HelloWorld();
  10. // hw.displayHelloWorld();
  11. float[] coord = hw.getCoordF();
  12. // System.out.println(hw.getString());
  13. System.out.println(coord[0]+" " +coord[1]+" " +coord[2]);
  14. // hw.displayHelloWorld();
  15. }
  16. }
  1. 然后再在idea里选择添加动态库文件:`File-->Project Structure-->Libraries`添加生成的库文件,然后在程序入口初始化 `System.load("helloworld");`这个方法我在windows上可以,但是linux上不行,所以程序里我直接写了绝对地址,程序正常运行~~<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22026529/1627996335908-06ef69ea-e631-4c30-82d0-4e4591b5a973.png#clientId=u582979cd-95ca-4&from=paste&id=ue44d4bcd&margin=%5Bobject%20Object%5D&name=image.png&originHeight=185&originWidth=502&originalType=binary&ratio=1&size=12697&status=done&style=none&taskId=uf9e18315-bd3d-47f2-bd8a-2641a6dfd9c)

4. Windows平台调用动态库dll

与linux的动态库.so文件不同, windows上的动态库文件后缀是.dll。这个文件生成比较简单,可以直接在Visual Studio2017上新建DLL工程,注意要选择release模式下x64的generator。
image.png
生成dll文件后,用类似的方法导入java工程。
参考博客:链接

5. PS

摸索了两三天,踩了无数坑,感谢互联网上各种各样的教程!今天整理了下这些天总结的经验。