前言
最近在刚好碰到了在跨平台下调用代码的需求,特别是Java下调用C++的的核心代码,是比较常见的一种情况。在不断的摸索下,目前发现想实现上诉功能发手段主要有:JNI、JNA和JNR三种。
本文以最为简单的JNA入手,记录从C++类库的生成到Java下成功调用的整个过程。笔者本身主要以 C++ 的开发为主,以下过程也主要以 C 开发者的角度展开。
一、JNA简介
JNA (Java native access) 在 JNI(Java native interface) 的基础上,提供了从 Java 到 C++ 下函数接口的动态映射,从而简化了对本地共享库调用过程。
二、环境准备
1、软件环境
- Visual Studio 2022 Community
- IntelliJ IDEA Community Edition 2023.1
- JDK-8u333
- Maven 3.6.3
2、引入库
主要是对 JNA Jar包的导入。
新建 Maven工程(配置不在此处赘述,详见参考文献),在 “pom.xml” 中添加依赖项 com.sun.jna ,这里用的是较旧的 3.0.9 版本。
<dependencies>
<dependency>
<groupId>com.sun.jna</groupId>
<artifactId>jna</artifactId>
<version>3.0.9</version>
</dependency>
</dependencies>
编译结束后,如本地环境中生成了我们所需的依赖库,说明库引入成功。
二、C++ 生成标准动态库(.dll)
如果只是 C++ 下动态库的生成其实很简单,这里详细记录一点。
首先在VS2022中创建新项目,选择 动态链接库(Dll) ,名称为 CTest 。
新建完成后,向项目中再添加新的3个代码文件,分别命名为:
- CTESTInterface.h
- CTESTImpl.h
- CTESTImpl.cpp
CTESTInterface 负责定义对外公开的类型及函数接口,CTESTImpl 为对函数接口的具体实现。
在 CTESTInterface.h 通过宏定义需要导出的函数接口。
#define CTEST_API extern "C" __declspec(dllexport)
extern “C” 关键字用于确保在编译过程中函数名不发生改变。
同时定义用于测试的函数接口:求一个数组中的最大值。
CTEST_API int CTEST_Max(int vec[], int size);
在 CTESTImpl.cpp 实现已定义的函数接口。
#include "CTESTInterface.h"
#include "CTESTImpl.h"
int CTEST_Max(int vec[], int size)
{
int maxVal = 0;
if (size > 1)
{
vec[0] = -10;
maxVal = vec[0];
}
for (int i = 0; i < size; i++)
{
if (vec[i] > maxVal)
{
maxVal = vec[i];
}
}
return maxVal;
}
此时对工程进行编译,在 Debug 目录下,出现了三个编译后文件:CTest.dll , CTest.lib, CTest.exp ,说明简单的一个动态库已经构建完成。
其中 .dll 是包含实际的函数实现,.lib 用于编译过程中对 .dll 中的函数进行定位,.exp 除非相互链接,一般用不上。
三、C++下动态库调用
同解决方案下新建一个名为 TestConsoleApp 控制台应用,对上文的动态库进行测试。
在 TestConsoleApp 工程上右键->属性->连接器->输入->附加依赖项,编辑后添加一个新的库引用。
..\x64\Debug\CTest.lib
这里需要确保两个项目的输出目录保持一致,我这里都在 x64下的Debug目录下。
在 TestConsoleApp.cpp 中添加如下代码:
#include <iostream>
#include "..\CTest\CTESTInterface.h"
int main()
{
std::cout << "Hello World!\n";
//测试封装库内函数
int nums [] = {1,2,3,4,5};
int max = CTEST_Max(nums, 5);
//输出
std::cout << "最大值为:" << max << "\n";
char wait;
std::cin >> wait;
return 0;
}
运行后输出为:
Hello World!
最大值为:5
总结
- 以 __declspec(dllexport) 定义导出函数接口
- .lib 文件为编译过程中使用,编译完成后,运行仅需要将对应的 .dll 文件放入运行目录下
- 对库内函数的调用也可以使用隐式的方式(不需要 Interface.h )