记录学习内容
C编译dll环境:
IDE:Clion
ToolChains:MinGW64
Java环境:
版本:jdk1.8 64位
JNA依赖版本:5.2.0 (可根据需要升级,本文以此版本为例)
JNA(Java Native Access )提供一组Java工具类用于在运行期间动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。(概念来自百度)
先导入JNA依赖
如果是用maven就在pom文件中引入
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.2.0</version>
</dependency>
不是maven的就下载jar包引入
一、编译动态库
编译动态库的方法参照上一篇CLion中C++加载静态库和动态库。
二、JNA调用dll——简单参数传递方法示例
CLion目录结构:
Math:工程目录名
cmake-build-debug-mingw:MinGW cmake编译生成的文件夹
output:工程库文件输出目录
CMakeLists.txt:cmake配置文件 内容如下
cmake_minimum_required(VERSION 3.23)
project(Math)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/output) #修改所生成库的目标文件夹
set(CMAKE_CXX_STANDARD 11)
add_library(Math SHARED library.cpp)
C的代码如下:
以返回参数的平方为例
library.h:
#ifndef MATH_LIBRARY_H
#define MATH_LIBRARY_H
extern "C" double customLibTest(const char * num);
#endif //MATH_LIBRARY_H
library.cpp:
#include "library.h"
#include <iostream>
using namespace std;
double customLibTest(const char * num) {
double x = strtod(num, nullptr);
double res = x * x;
cout << "输入x: " << x << "的平方是" << res << endl;
return res;
}
将以上代码在CLion中生成dll
Java端创建测试类TestNative.java
调用customLibTest方法 传入字符串,代码如下:
package com.web.natives;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class TestNative {
public interface MathDll extends Library {
MathDll instance = Native.load("libMath", MathDll.class);
//简单调用dll中的方法 字符串参数 double返回值
double customLibTest(String num);
}
public static void main(String[] args) {
System.out.println("java输出返回值:" + MathDll.instance.customLibTest("5"));
}
}
结果:
本例可见 Java端只需要创建接口interface类并继承Library,并在接口中用Native.load方法声明实例,声明接口方法,JNA框架会自动将dll与接口进行映射和解析,我们只需要调用接口的方法即可,JNA会帮我们与dll的参数和返回值的数据类型等进行匹配。
ps:上面例子中Native.load方法,第一个参数是dll库的存放路径,这里我没有给后缀.dll是因为JNA会自动给路径拼后缀,做到不同操作系统调用相同名字的库,而不需要区分到底调用的是dll还是so。
三、JNA调用dll——复杂结构体传递方法示例
有时候根据需求会传递复杂的参数,java会给C传递比较复杂的对象实体和实体的嵌套,对应的就是C的结构体和结构体嵌套。
下面以老师Teacher和学生Student为例,设定一个老师带一个班,这个班有3个学生,其中一个学生是班长。我们的目的是打印老师的信息和学生的信息。
dll中的代码如下:
library.h
#ifndef MATH_LIBRARY_H
#define MATH_LIBRARY_H
//例子1
extern "C" double customLibTest(const char * num);
//定义结构体
typedef struct {
char *name;
int age;
int sex;
long long id;
}Student;
typedef struct {
char *name;
Student monitor;
Student students[3];
}Teacher;
extern "C" void printTeacherAndStudent(Teacher *teacher);
#endif //MATH_LIBRARY_H
library.cpp
#include "library.h"
#include <iostream>
using namespace std;
//例子1
double customLibTest(const char * num) {
double x = strtod(num, nullptr);
double res = x * x;
cout << "输入x: " << x << "的平方是" << res << endl;
return res;
}
//打印老师和学生信息
void printTeacherAndStudent(Teacher *teacher) {
cout << "老师姓名:" << (*teacher).name << endl;
cout << "班长姓名:" << (*teacher).monitor.name << endl;
int size = sizeof((*teacher).student)/sizeof((*teacher).student[0]);
for (int i = 0; i < size; ++i) {
cout << "学生学号:" << (*teacher).student[i].id << " 学生姓名:" << (*teacher).student[i].name << endl;
}
}
再将上面的代码生成dll
Java端代码:
首先 我们要创建与C结构体Teacher和Student相对应的Java类
Student.java:
package com.web.natives;
import com.sun.jna.Structure;
//下面的注解 声明了字段顺序,只要下面的数组中元素的顺序与C中结构体的变量顺序一致即可
@Structure.FieldOrder({"name", "age", "sex", "id"})
public class Student extends Structure {
public String name;
public int age;
public int sex;
public long id;
//内存对齐
public Student() {
super(ALIGN_DEFAULT);
}
//实现Structure.ByValue 接口,就表示这个类代表结构体本身
public static class ByValue extends Student implements Structure.ByValue{
public ByValue(long id, String name, int age,int sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}
public ByValue() {}
}
}
Teacher,java
package com.web.natives;
import com.sun.jna.Structure;
@Structure.FieldOrder({"name", "monitor", "students"})
public class Teacher extends Structure {
public String name;
//结构体内部可以包含结构体对象的指针的数组
//必须给students数组赋值,否则不会分配3个结构体数组的内存,导致JNA中的内存大小和C代码中结构体的内存大小不一致而调用失败。
public Student.ByValue[] students = new Student.ByValue[3];
//结构体内部可以嵌套结构体
public Student.ByValue monitor;
//内存对齐
public Teacher() {
super(ALIGN_DEFAULT);
}
//实现Structure.ByReference 接口,就表示这个类代表结构体指针
public static class ByReference extends Teacher implements Structure.ByReference{}
}
测试类TestNative.java:
package com.web.natives;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class TestNative {
public interface MathDll extends Library {
MathDll instance = Native.load("libMath", MathDll.class);
//例子1 简单调用dll中的方法 字符串参数 double返回值
double customLibTest(String num);
//复杂的结构体传参
void printTeacherAndStudent(Teacher.ByReference teacher);
}
public static void main(String[] args) {
//创建一个Teacher结构体指针
Teacher.ByReference teacher = new Teacher.ByReference();
teacher.name = "王老师";
//创建3个学生结构体
Student.ByValue student1 = new Student.ByValue(10003101L, "张三", 18, 1);
Student.ByValue student2 = new Student.ByValue(10003102L, "李四", 17, 0);
Student.ByValue student3 = new Student.ByValue(10003103L, "王五", 19, 1);
//给teacher中嵌套的结构体班长赋值
teacher.monitor = student1;
//给teacher中嵌套的结构体数组学生赋值
teacher.students[0] = student1;
teacher.students[1] = student2;
teacher.students[2] = student3;
//调用dll方法 传递Teacher指针给dll
MathDll.instance.printTeacherAndStudent(teacher);
}
}
结果:
上例中可见,java端只需要创建继承Structure的类,并根据具体情况实现ByValue或ByReference可以模拟C的结构体和结构体参数传递。
需要注意的是
1、结构体内存对齐,内存对齐的相关内容可查看这篇博客
结构体的内存对齐规则_利刃Cc的博客内存对齐
2、C与Java的结构体成员字段对应问题,字段的数据类型要对应;字段的顺序要一致,例如 C中的结构体是char *和int,那么Java声明的就应该是String和int,上例中Java的注解@Structure.FieldOrder就是为了这个规则而存在的。
3、如果C中需要的是结构体本身,那么Java端的类就实现ByValue接口;如果C中需要的是结构体指针,那么Java端的类就实现ByReference接口,上例中printTeacherAndStudent这个方法在C中的参数是指针,所以传的是Teacher的ByReference;当然,两个都实现也可以,看需要。
4、 当C中的结构体不像上例那样用Student结构体本身创建数组,而是用结构体指针数组
typedef struct {
char *name;
Student monitor;
Student *students[3]; //将例子中的Student students[3]改成指针形式
}Teacher;
那么Java端用Student实现ByReference后传给C依然会报错,需要调用Structure的wirte()方法固定住Java对象的内存不让GC回收。
四、JNA调用dll——回调示例
有时根据需求,Java调用dll时,也需要有一个回调方法来达到C调用Java里的方法。
这时就引出了JNA的Callback
看例子:
library.h
#ifndef MATH_LIBRARY_H
#define MATH_LIBRARY_H
typedef void (*callback)(char *str);
extern "C" void testDllCallback(char *str, callback cb);
#endif //MATH_LIBRARY_H
library.cpp
#include "library.h"
#include <iostream>
using namespace std;
void testDllCallback(char *str, callback cb) {
cb(str);
}
头文件定义一个有参数无返回值的callback类型和传入字符串和callback两个参数的方法
cpp文件中只需要将传入的字符串传递给callback
Java端TestNative.java如下:
package com.web.natives;
import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class TestNative {
public interface MathDll extends Library {
MathDll instance = Native.load("libMath", MathDll.class);
//回调用法 自定义接口继承Callback 实现Callback 调用dll的方法并将回调方法当参数传过去
interface TestCallback extends Callback {
void invoke(String str);
}
class TestCallbackImpl implements TestCallback {
@Override
public void invoke(String str) {
System.out.println("dll传过来的字符串: " + str);
}
}
void testDllCallback(String str, Callback cb);
}
public static void main(String[] args) {
MathDll.instance.testDllCallback("我是传给C的字符串参数", new MathDll.TestCallbackImpl());
}
}
结果:
上例可见,JNA的回调,只需要创建继承Callback接口,然后实现该接口,然后传递给C即可。
五、总结
1、C的基本数据类型与Java基本数据类型对照表(来自JNA官方)
2、JNA调用Native时,不管是继承Library还是Structure或者Callback,方法名,变量类型、变量名、参数类型和参数名等尽量一致。
这个错误将是使用JNA最常见的”非法访问内存“错误,这时就要检查上面说的一致性的情况。
3、由于Java的内存分配机制导致Java对象在物理内存的地址不一定是连续的,而C对内存的处理是很严格的,因此两端交互时一定要注意这方面的问题。
Java调用原生函数时,会把传递给原生函数的Java 数据固定在内存中,这样原生函数才可以访问这些Java数据。对于没有固定住的Java对象,GC可以删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原生函数访问没有被固定住的Java对象,就会导致调用失败。固定住哪些java对象,是JVM根据原生函数调用自动判断的。
4、java传参都是值传递,如果想传给C并改变参数值的话,C一般都是传递参数的指针,那么Java端这面就要用到JNA的Pointer类。