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.运行测试

可以看到测试成功

总结

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

相关推荐
Charles Ray27 分钟前
C++学习笔记 —— 内存分配 new
c++·笔记·学习
重生之我在20年代敲代码27 分钟前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
爱上语文29 分钟前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people32 分钟前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
qmx_071 小时前
HTB-Jerry(tomcat war文件、msfvenom)
java·web安全·网络安全·tomcat
为风而战2 小时前
IIS+Ngnix+Tomcat 部署网站 用IIS实现反向代理
java·tomcat
技术无疆4 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
迷迭所归处6 小时前
C++ —— 关于vector
开发语言·c++·算法
架构文摘JGWZ7 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
CV工程师小林7 小时前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先