项目中遇到需要java调用c++动态库的需求,所以就简单记录一下使用

网上查找了一下相关的资料,发现有两种通用的方式,一种是JNI(Java Native Interface)和JNA(Java Native Access),

比较了一下两者的优缺点,JNI性能比较好,但是实现起来较为复杂,JNA性能差一点,但都是封装好的工具类,使用非常方便友好,所以这边选择了JNA来实现。

这边的c++程序编译后,生成了dll文件和so文件,分别对应windows和linux,文章记录的是windows下的dll动态库调用,这边需要注意编译生成的dll文件位数,电脑操作系统是64就统一都是64位,java和c++不一致可能会出问题。

首先java需要引入pom依赖,可以查询最新的版本,https://search.maven.org/

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.11.0</version>
</dependency>

1、c++代码

c++部分代码如下,功能非常简单

a、GetMyVersion()返回一个字符串

b、PointTest(char* ret, char* source)是传入一个源字符串,反转后赋值给ret

c、根据传入的一个方法,进行java方法回调,回调了100次,并将回调次数作为参数传入java方法

#include "pch.h"
#include "Work.h"

char* GetMyVersion()
{
    char* version = (char *)"c++ version 11";
    return version;
}

void PointTest(char* ret, char* source)
{
    int n = strlen(source);

    while (0<n) {
        *ret = source[n-1];
        ret++;
        n--;
    }
}

void CallBackTest(ProgressCallback progressCallback)
{
    int counter = 0;

    for (; counter <= 100; counter++)
    {
        // do the work...
        if (progressCallback)
        {
            // send progress update
            progressCallback(counter);
        }
    }
}

2、java代码

对应的java相关的代码如下:

package com.example.jnatest;

import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public interface Clibrary extends Library {

    /**
     * 加载c++动态库
     */
    Clibrary instance = Native.load("CsharedDll", Clibrary.class);

    /**
     * 调用c++方法,获取C++的版本,当前方法名和c++代码中的方法名保持相同
     */
    String GetMyVersion();

    /**
     * 测试指针的传入,把一个指针当做入参传入,c++代码对该指针的数据修改后,
     * java端可以获取到修改后的数据,方法名和入参要和c++的代码保持相同
     */
    void PointTest(Pointer ret, String source);

    /**
     * 测试回调,将一个java方法当成参数传给c++侧,c++执行完成代码逻辑后,
     * 会根据传入的方法进行java方法的回调
     */
    interface voidCallBack extends Callback {

        void callback(int counter);
    }

    void CallBackTest(voidCallBack c);
}

创建一个接口去extends Library接口,目的是加载动态库,CsharedDll为c++编译成的dll或so文件名称,如下图:

jni c++调用java jna调用c++类_java方法

低版本的jna依赖可以使用Native.loadLibrary(),但是在高版本中已经不推荐使用了,可以使用Native.load(),本例中使用后者。

里面需要注意java和c/c++数据类型对应关系,可以百度搜索下

在Clibrary中定义一个方法 ,方法和c++中的方法名称要相同,具体可以看java接口中每个方法上面的注释

测试调用如下面代码中的注释1:

package com.example.jnatest;

import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JnatestApplication {

    public static void main(String[] args) {
        //1、java正常调用c++,并返回结果
        String response = Clibrary.instance.GetMyVersion();
        System.out.println(response);

        //2、java侧是否可以正常解析C++返回的指针变量
        String source = "hello world";
        Pointer ret = new Memory(source.length());
        Clibrary.instance.PointTest(ret, source);
        System.out.println(new String(ret.getByteArray(0, source.length())));

        //3、java触发c++回调
        Clibrary.voidCallBack voidCallBack = new Clibrary.voidCallBack() {
            @Override
            public void callback(int counter) {
                System.out.println("java counter:" + counter);
            }
        };
        Clibrary.instance.CallBackTest(voidCallBack);
    }
}

 3、dll文件存放位置

百度关于dll存放java项目位置,有三种回答,第一种方式没有试过,后面的两种方式均试过,可以正常加载

一:是放到windows的对应的系统文件夹下。

二:是随便放一个位置,然后在load方法里面指定绝对文件路径,如下

Clibrary instance = Native.load("D://test//CsharedDll.dll", Clibrary.class);

 三:将dll或者so文件放到java工程的resources下面,如下图

 

jni c++调用java jna调用c++类_c++_02

load方法里直接是相对路径,只需要指定文件名称即可,可以不需要路径和后缀名,如下:

Clibrary instance = Native.load("CsharedDll", Clibrary.class);

4、Pointer

在PointTest方法中,java代码里面第一个参数类型是Pointer,第二个参数类型是String,因为java里面参数都是值传递,但是c++里面存在引用传递和值传递(具体不太清楚,反正和java是不一样的),

所以java传递进去的参数,c++程序修改后,java如果想读取到修改后的参数,此时就需要传入Pointer类型,如果传入String,c++修改后,java是获取不到的,这里需要注意,有兴趣的可以查查其他资料。

5、JNA回调

有时候会存在c++回调java方法的需求,我自己理解,大致回调的逻辑是这样的,可能不够准确,JNA调用c++方法时,类似传入了一个java的方法引用,c++接口执行完自己的逻辑后,根据java的方法引用来回调java方法

在java中实现如下:

public interface Clibrary extends Library {

    /**
     * 加载c++动态库
     */
    Clibrary instance = Native.load("CsharedDll", Clibrary.class);

    /**
     * 测试回调,将一个java方法当成参数传给c++侧,c++执行完成代码逻辑后,
     * 会根据传入的方法进行java方法的回调
     */
    interface voidCallBack extends Callback {

        void callback(int counter);
    }

    void CallBackTest(voidCallBack c);
}

接口Clibrary extends Library,里面定义一个接口voidCallBack(接口名称随意)需要extends Callback,Callback里面 String METHOD_NAME = "callback",所以需要在voidCallBack中定义一个callback方法,callback方法的入参和c++保持一致就可以,这个方法里面后面实现就是回调后的具体逻辑。

然后定义一个CallBackTest方法,这个方法名要和c++代码中的回调方法名一致。

实际使用如下:

Clibrary.voidCallBack voidCallBack = new Clibrary.voidCallBack() {
    @Override
    public void callback(int counter) {
        //实现具体的回调逻辑
        System.out.println("java counter:" + counter);
    }
};
Clibrary.instance.CallBackTest(voidCallBack);