记录学习内容

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目录结构:

java 动态库 目录 java jna 加载动态库_内存对齐

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 动态库 目录 java jna 加载动态库_内存对齐_02

 本例可见 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 动态库 目录 java jna 加载动态库_开发语言_03

上例中可见,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());
    }
}

结果:

java 动态库 目录 java jna 加载动态库_内存对齐_04

 上例可见,JNA的回调,只需要创建继承Callback接口,然后实现该接口,然后传递给C即可。

五、总结

1、C的基本数据类型与Java基本数据类型对照表(来自JNA官方

java 动态库 目录 java jna 加载动态库_内存对齐_05

2、JNA调用Native时,不管是继承Library还是Structure或者Callback,方法名,变量类型、变量名、参数类型和参数名等尽量一致。

java 动态库 目录 java jna 加载动态库_java_06

这个错误将是使用JNA最常见的”非法访问内存“错误,这时就要检查上面说的一致性的情况。

3、由于Java的内存分配机制导致Java对象在物理内存的地址不一定是连续的,而C对内存的处理是很严格的,因此两端交互时一定要注意这方面的问题。

Java调用原生函数时,会把传递给原生函数的Java 数据固定在内存中,这样原生函数才可以访问这些Java数据。对于没有固定住的Java对象,GC可以删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原生函数访问没有被固定住的Java对象,就会导致调用失败。固定住哪些java对象,是JVM根据原生函数调用自动判断的。

 4、java传参都是值传递,如果想传给C并改变参数值的话,C一般都是传递参数的指针,那么Java端这面就要用到JNA的Pointer类。