java调用ddl动态链接库,以及遇到的一些坑

java调用ddl动态链接库,以及遇到的一些坑

大家好,我是不会java的java废材生。某一天突然接到个小需求,要求用c++写一个函数并用java来调用,而且特地强调不允许通过网络请求的方式调用(当然作者比较废也不会写c++的服务接口),这就需要本地调用ddl动态链接库来实现了。作者以前没有做过类似是场景,不过java有性能瓶颈,所以一些偏复杂的功能可能就是用其他性能较好的语言来实现的(比如c、c++等)再在java里面调用那些实现好的功能,以下是作者做的案例并踩的一些坑,希望能帮助到大家。

什么是ddl

我们在实现类似上面需求之前,要大致了解一下什么是ddl。

ddl是一种文件的类型,在Windows开启文件后缀名展示的话能看到它是应用程序扩展文件,标准点叫动态链接库文件(Dynamic Link Library)。在Linux上,动态链接库的文件后缀为.so(有兴趣的朋友可以自行搜索)。

ddl文件是实现资源共享的一种方式,本质就是编译好的机器指令文件,直接打开就会看到一些乱码;动态是指只有在程序运行时才会去调用某个ddl,因此ddl性能开销就会小很多(静态链接的性能开销会大不少,具体有兴趣也可以百度)。

C++生成ddl的两种方式(使用clion工具)

在生成ddl之前,需要注意设置好编译环境,clion工具只提供了cmake项目构建工具。编译器的话一般选择用MinGW,MinGW要和jdk位数一样,64位的jdk对应64位的MinGW,MinGW下载链接

编译环境配置

设置->构建、执行、部署->工具链,按照下图配置好确定即可。

新建一个c++项目

选择c++库,按下图勾选即可

其实选择可执行文件那个也是可以的,但我们简单打印个helloworld不需要执行测试。项目创建成功后会默认生成一个library.cpp文件和library.h文件。

方式一(适用于c和c++之间的调用)

1.代码编写

使用 __declspec(dllexport)标识符

新建一个cpp文件,直接在函数前面加上这个标识符就可以,该关键字可以将该函数标识为要导出的函数,如下面void hello()函数

c++ 复制代码
__declspec(dllexport) void hello(){
    printf("helloworld\n");
}

注意事项:这种方式导出函数得到ddl之后,导出的函数名会改变,在编译链接的时候,c++会按照自己的规则篡改函数名称。

解决方式:在定义导出函数时加上extern"C"限定符

可以这样

c++ 复制代码
extern"C" __declspec(dllexport) void hello(){
    printf("helloworld\n");
}

也可以这样将多个要导出了函数放一个块里

c++ 复制代码
extern"C" {
    __declspec(dllexport) void hello(){
       printf("helloworld\n");
   }
}

完整代码附图如下

2.配置CMakeList.txt文件

在编写完上面代码之后就可以去生成ddl文件了。

找到项目根目录下的CMakeList.txt配置文件(创建项目的话会默认生成),加入如下内容

scss 复制代码
cmake_minimum_required(VERSION 3.19) #创建项目默认就有
project(dlltest) #ddltest是项目名称,也是创建项目默认就有

set(CMAKE_CXX_STANDARD 11) #版本,有默认就默认,没默认选11

add_library(dlltest SHARED  mytest.cpp) 

因为创建的是c++库项目,所以配置里有add_library,如果是创建的是c++可执行文件的项目就要自己手动加入。

dlltest就是生成的ddl文件名称,mytest.cpp就要导出为dll的代码源文件

配置好之后如下图

3.构建项目生成dll文件

点击最上方那栏构建->构建项目

项目构建成功之后会在cmake-build-debug目录下生成ddl文件,不过默认加上了lib的前缀。

将这个dll文件复制到程序可以访问到的地方就可以调用那个void hello()函数了,如果访问不到会报错文件找不到。

注意:这一种方式少一些流程,但依然存在问题,只能解决c和c++之间调用函数命名的问题,生成了ddl给java调用的时候就会报错java.lang.UnsatisfiedLinkError,想用java调用就用方式二吧。

方式二(生成java可调用的ddl)

了解java调用ddl常见的三种方法

java调用dll常用有三种方法,分别是用jni、jna和jnative。

jni是jdk自带的,性能最好(推荐),其他两种是基于jni的封装。

本文使用jni的方法来调用。

了解java中native关键字

它用于声明一个方法是由本地代码实现的,也就是说这个方法的实现并不是用 Java 编写的,而是使用其他语言(比如 c/c++)编写的,并通过jni调用。

如果希望在 java 中调用dll中的函数,并且不希望函数名被改变,你可以使用 native 关键字声明一个本地方法,并在本地代码中实现这个方法。

1.在java中创建一个类声明native方法

java 复制代码
public class MylibTest {
    public native void hello(); //方法名要和c++里的函数名一一致
}

2.生成native方法对应的c++头文件

该头文件的作用是定义c++能够识别的java中方法对应的c++函数。

生成头文件需要用到命令,不同版本jdk所需命令不一样。

jdk1.8版本

javah命令,在MylibTest.java所在的目录下执行命令。

javah MylibTest.java

1.8版本以上

方式一

这个地方有坑,网上查的时候说是java8之后的版本用javac -h命令(但没补充,作者一执行就报错),这个命令是没问题但是如上面命令一样直接执行就会出问题。

我们能看到下面执行报错说无源文件。

那怎么解决能,指定一个输出源目录就可以了

-h 指定头文件的输出目录,.代表当前目录,test则是在当前目录下生成一个test目录

javac -h test MylibTest.java

执行之后就会在MylibTest.java所在目录生成一个test目录

执行成功

生成test目录

test目录下会有一个头文件。

方式二

在与方式一同样的目录位置执行如下命令,encoding指定编码为uft8

javac -encoding utf8 -h . MylibTest.java

作者jdk17这两种命令都用过了,实测没问题。

3.将生成的头文件引入c++项目

之前创建好了c++项目这里就不再创建了。

将生成的头文件复制粘贴到项目跟目录,生成的头文件里面用到了jni.h和jni_md.h头文件(jni.h和jni_md.h头文件在jdk目录下的include目录里面,找一找很快就能找到),也一并复制粘贴过来。

生成的头文件类容如下

arduino 复制代码
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_stx_MylibTest */

#ifndef _Included_com_stx_MylibTest
#define _Included_com_stx_MylibTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_stx_MylibTest
 * Method:    hello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_stx_MylibTest_hello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

4.在当前目录新建一个cpp文件并实现函数

新建的cpp需要引入生成的头文件和jni.h头文件,并实现生成头文件里面的函数

arduino 复制代码
#include "jni.h"
#include "com_stx_LibTest.h"

JNIEXPORT void JNICALL Java_com_stx_LibTest_hello (JNIEnv *, jobject){
    printf("helloworld\n");
}

5.配置CMakeList.txt

在里面的add_library添加cpp和引用到生成的那个头文件(同前面的一样)

scss 复制代码
add_library(dlltest SHARED com_stx_LibTest.h xxx.cpp) 

6.构建生成dll

原理同方式一提到的一样。

7.将生成的ddl直接粘贴到jdk的bin目录下

bin目录是java在加载库时最优先访问的目录。

8.编写java测试类

java 复制代码
public class LibTest {
    static {
        System.loadLibrary("libdlltest"); //这里的名字要和ddl的文件名一致
    }
    public native void hello();
    public static void main(String[] args) {
        new LibTest().hello();
    }
}

9.运行测试

可以看到测试成功

总结

在第一次弄的时候,踩了不少坑,弄了一个下午没弄出来(可能是作者比较菜的原因)。作者没有因此而放弃,查了诸多文章之后给实现出来了,总的来说多试试、多看看文章、多看看书,总会有收获的。

相关推荐
阿伟*rui29 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端