一,项目前提:
我们目前是想实现一个人脸识别考勤的项目,而厂商给我们所提供的是c++封装好的jdk 。为了方便跟我们的Java平台对接,因此需要一些手段将C++项目融入到我们的Java 平台当中。我们最终选用JNA来对c++ sdk 来进行封装。项目使用jdk(1.8.0_201)、 idea(2018.3.3)、jna版本(3.4.0)
ps:小编在jna版本上曾经踩过坑,发现使用的jna(4.2.2), 运行代码始终不成功,类继承 Structure 类的时候 。重写
getFieldOrder() 方法写法也很繁琐, 后来发现很多人都用的是3.4.0版本的JNA 。并且这个版本的jna可以不用非要去重写getFieldOrder()这个方法。后面我们会提到为什么可以不用重写这个方法了。
二,项目方案选择:
JNI:是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。
1,JNI
1,jni的简单应用
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPn1UMZRkT3NGVPpHOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0QjN5QTOxATM4IDNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2,jni在java中的应用(jdk1.1 之后 才加入了JNI的概念:在 IO 、net 、lang包下面偏多)。下面我们去JDK中IO中去看下这些本地方法吧。
/**
* Reads a subarray as a sequence of bytes.
* @param b the data to be written
* @param off the start offset in the data
* @param len the number of bytes that are written
* @exception IOException If an I/O error has occurred.
*/
private native int readBytes(byte b[], int off, int len) throws IOException;
这个方法是出现在 FileInputStream 这个类中。
/**
* Reads up to <code>b.length</code> bytes of data from this input
* stream into an array of bytes. This method blocks until some input
* is available.
*
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the file has been reached.
* @exception IOException if an I/O error occurs.
*/
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
/**
* Reads up to <code>len</code> bytes of data from this input stream
* into an array of bytes. If <code>len</code> is not zero, the method
* blocks until some input is available; otherwise, no
* bytes are read and <code>0</code> is returned.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the file has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @exception IOException if an I/O error occurs.
*/
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
然后这个 readBytes(b, off, len) 方法就会调用上面的这个本地Native 方法。至于这些方法的作用,自己看一看应该也能看得懂,但至于Native内部怎么实现,也不必太过追究。
2,JNA的使用
1,JNA相比JNI有什么优势呢?
下面来看一看JNA的维基百科解释
PS:关键点在于:JNA的设计旨在以最少的努力以自然的方式提供原生访问。它能支持macOS,Microsoft Windows,FreeBSD / OpenBSD,Solaris,Linux,AIX,Windows Mobile和Android 这么多的平台。so, 我们选型JNA(3.4.0)
三、JNA爬坑历路
1,结构体和我们的Java类的数据类型的转换问题。
这里给老司机们推荐几篇文章,https://blog.csdn.net/ctwy291314/article/details/82895604 , https://blog.csdn.net/ctwy291314/article/details/84626829 等等文章。这里其实有一些我没那样用,但是我的程序运行没有问题的。 例如 c中 int 对应 java 中的 int 。还有附上JNA在GitHub(全球最大的同性交友网站)上的源码地址:https://github.com/java-native-access/jna 。
这里是在网上找的对数据类型转换的一张图表。我在使用过程中大体没有出现什么问题。
下面是我在程序中用到的一些数据类型转换的图表,还有就是我的demo。
( 查询人脸识别人员组信息,pstInParam与pstOutParam内存由用户申请释放)
// 查询人脸识别人员组信息,pstInParam与pstOutParam内存由用户申请释放
CLIENT_NET_API BOOL CALL_METHOD CLIENT_FindGroupInfo(LLONG lLoginID, const NET_IN_FIND_GROUP_INFO* pstInParam, NET_OUT_FIND_GROUP_INFO *pstOutParam, int nWaitTime = 1000);
PS:这里的LLONG是我们自己自定义的一个类型。目的是考虑兼容32bit的操作系统 和 64bit的操作系统。
下面是我对LLONG这种数据类型的封装,代码如下:
public static class LLong extends IntegerType {
private static final long serialVersionUID = 1L;
/** Size of a native long, in bytes. */
public static int size;
static {
size = Native.LONG_SIZE;
if (Utils.getOsPrefix().toLowerCase().equals("linux-amd64")
|| Utils.getOsPrefix().toLowerCase().equals("win32-amd64")) {
size = 8;
} else if (Utils.getOsPrefix().toLowerCase().equals("linux-i386")
|| Utils.getOsPrefix().toLowerCase().equals("win32-x86")) {
size = 4;
}
}
// 获取操作平台信息
public static String getOsPrefix() {
String arch = System.getProperty("os.arch").toLowerCase();
final String name = System.getProperty("os.name");
String osPrefix;
switch(Platform.getOSType()) {
case Platform.WINDOWS: {
if ("i386".equals(arch))
arch = "x86";
osPrefix = "win32-" + arch;
}
break;
case Platform.LINUX: {
if ("x86".equals(arch)) {
arch = "i386";
}
else if ("x86_64".equals(arch)) {
arch = "amd64";
}
osPrefix = "linux-" + arch;
}
break;
default: {
osPrefix = name.toLowerCase();
if ("x86".equals(arch)) {
arch = "i386";
}
if ("x86_64".equals(arch)) {
arch = "amd64";
}
int space = osPrefix.indexOf(" ");
if (space != -1) {
osPrefix = osPrefix.substring(0, space);
}
osPrefix += "-" + arch;
}
break;
}
return osPrefix;
}
public static String getOsName() {
String osName = "";
String osPrefix = getOsPrefix();
if(osPrefix.toLowerCase().startsWith("win32-x86")
||osPrefix.toLowerCase().startsWith("win32-amd64") ) {
osName = "win";
} else if(osPrefix.toLowerCase().startsWith("linux-i386")
|| osPrefix.toLowerCase().startsWith("linux-amd64")) {
osName = "linux";
}
return osName;
}
上面写的一些Util来的这一些方法,就是对操作系统long类型的判断,32位系统的 long 类型占4字节 , 64位系统的long类型占 8字节。
下面我们来看这个接口的C++中的封装。
void CDispatchGroupDlg::RefreshDispatchInfo(const char *szGroupId, BOOL bRefreshShow)
{
int i = 0;
BOOL bRet = FALSE;
NET_IN_FIND_GROUP_INFO stuInParam = {sizeof(stuInParam)};
NET_OUT_FIND_GROUP_INFO stuOutParam = {sizeof(stuOutParam)};
stuOutParam.nMaxGroupNum = 50;
NET_FACERECONGNITION_GROUP_INFO *pGroupInfo = NULL;
stuOutParam.pGroupInfos = new NET_FACERECONGNITION_GROUP_INFO[stuOutParam.nMaxGroupNum];
if (NULL == stuOutParam.pGroupInfos)
{
MessageBox(ConvertString("Memory error"), "");
bRet = FALSE;
goto e_clear;
}
memset(stuOutParam.pGroupInfos, 0, sizeof(NET_FACERECONGNITION_GROUP_INFO)*stuOutParam.nMaxGroupNum);
for (i = 0; i < stuOutParam.nMaxGroupNum; i++)
{
pGroupInfo = stuOutParam.pGroupInfos + i;
pGroupInfo->dwSize = sizeof(*pGroupInfo);
}
bRet = CLIENT_FindGroupInfo(m_lLoginID, &stuInParam, &stuOutParam, DEFAULT_WAIT_TIME);
if (!bRet)
{
MessageBox(ConvertString("Failed to find group infos!"), "");
bRet = FALSE;
goto e_clear;
}
for (i = 0; i < stuOutParam.nRetGroupNum; i++)
{
pGroupInfo = stuOutParam.pGroupInfos + i;
if (0 == strcmp(szGroupId, pGroupInfo->szGroupId))
{
memcpy(&m_stuDispatchGroupInfo, pGroupInfo, sizeof(m_stuDispatchGroupInfo));
}
}
if (bRefreshShow)
{
CleanDispatchList();
m_DispatchInfoList.ResetContent();
for (i = 0; i < m_stuDispatchGroupInfo.nRetChnCount; i++)
{
CString str;
str.Format("%4d %4d %s", m_stuDispatchGroupInfo.nChannel[i]+1, m_stuDispatchGroupInfo.nSimilarity[i], "已布控");
m_DispatchInfoList.AddString(str);
NET_DISPATCH_INFO *pstDispatchInfo = new NET_DISPATCH_INFO;
if (pstDispatchInfo)
{
memset(pstDispatchInfo, 0, sizeof(*pstDispatchInfo));
//pstDispatchInfo->nIndex = i;
pstDispatchInfo->nChannel = m_stuDispatchGroupInfo.nChannel[i];
pstDispatchInfo->nSimilarity = m_stuDispatchGroupInfo.nSimilarity[i];
pstDispatchInfo->bDispatch = TRUE;
m_lstDispatchChannelInfo.push_back(pstDispatchInfo);
}
}
}
首先 NET_IN_FIND_GROUP_INFO stuInParam = {sizeof(stuInParam)}; sizeof() 库函数在c++中返回结构体对象返回的字节数。
// CLIENT_FindGroupInfo接口输入参数
typedef struct tagNET_IN_FIND_GROUP_INFO
{
DWORD dwSize;
char szGroupId[ST_COMMON_STRING_64];// 人员组ID,唯一标识一组人员,为空表示查询全部人员组信息
}NET_IN_FIND_GROUP_INFO;
// CLIENT_FindGroupInfo接口输出参数
typedef struct tagNET_OUT_FIND_GROUP_INFO
{
DWORD dwSize;
NET_FACERECONGNITION_GROUP_INFO *pGroupInfos; // 人员组信息,由用户申请空间,大小为sizeof(NET_FACERECONGNITION_GROUP_INFO)*nMaxGroupNum
int nMaxGroupNum; // 当前申请的数组大小
int nRetGroupNum; // 设备返回的人员组个数
}NET_OUT_FIND_GROUP_INFO;
下面先来看看这两个结构体:
demo | java | c++ |
---|---|---|
int | DWORD | |
short | WORD | |
LLONG | LONG |
public class NET_IN_FIND_GROUP_INFO extends Structure implements Common{
public int dwSize;
public byte[] szGroupId = new byte[NET_COMMON_STRING_64];//人员组ID,唯一标识一组人员,为空表示查询全部人员组信息
public NET_IN_FIND_GROUP_INFO()
{
this.dwSize = this.size();
}
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "dwSize", "szGroupId" ,
});
}
}
public class NET_FACERECONGNITION_GROUP_INFO extends Structure implements Common {
public int dwSize;
public int emFaceDBType; // 人员组类型,详见EM_FACE_DB_TYPE, 取值为EM_FACE_DB_TYPE中的值
public byte[] szGroupId = new byte[NET_COMMON_STRING_64]; // 人员组ID,唯一标识一组人员(不可修改,添加操作时无效)
public byte[] szGroupName = new byte[NET_COMMON_STRING_128]; // 人员组名称
public byte[] szGroupRemarks = new byte[NET_COMMON_STRING_256]; // 人员组备注信息
public int nGroupSize; // 当前组内人员数
public int nRetSimilarityCount; // 实际返回的库相似度阈值个数
public int[] nSimilarity = new int[MAX_SIMILARITY_COUNT]; // 库相似度阈值,人脸比对高于阈值认为匹配成功
public int nRetChnCount; // 实际返回的通道号个数
public int[] nChannel = new int[NET_MAX_CAMERA_CHANNEL_NUM]; // 当前组绑定到的视频通道号列表
public int[] nFeatureState = new int[MAX_FEATURESTATE_NUM]; // 人脸组建模状态信息:
// [0]-准备建模的人员数量,不保证一定建模成功
// [1]-建模失败的人员数量,图片不符合算法要求,需要更换图片
// [2]-已建模成功人员数量,数据可用于算法进行人脸识别
// [3]-曾经建模成功,但因算法升级变得不可用的数量,重新建模就可用
public NET_FACERECONGNITION_GROUP_INFO()
{
this.dwSize = this.size();
}
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "dwSize", "emFaceDBType","szGroupId","szGroupName", "szGroupRemarks" ,
"nGroupSize", "nRetSimilarityCount","nSimilarity","nRetChnCount", "nChannel" ,
"nFeatureState",
});
}
}
我的Common 接口 :都是放一些常量的接口,读者可不必在意。
this.dwSize = this.size(); 通过撸Structure 这个类的源码才知道 这个类里面的Size,所以我们用构造方法来初始化结构体的dwSize;
下面我们来看一下Structure这个类的calculateSize() 方法 的具体的实现方法,有兴趣的可以去撸一撸这个地方的实现原理。这里解决了 sizeof() 这个c++ 中的类库函数了。
int calculateSize(boolean force, boolean avoidFFIType) {
boolean needsInit = true;
Structure.LayoutInfo info;
synchronized(layoutInfo) {
info = (Structure.LayoutInfo)layoutInfo.get(this.getClass());
}
if (info == null || this.alignType != info.alignType || this.typeMapper != info.typeMapper || !this.fieldOrderMatch(info.fieldOrder)) {
info = this.deriveLayout(force, avoidFFIType);
needsInit = false;
}
if (info != null) {
this.structAlignment = info.alignment;
this.structFields = info.fields;
info.alignType = this.alignType;
info.typeMapper = this.typeMapper;
info.fieldOrder = this.fieldOrder;
if (!info.variable) {
synchronized(layoutInfo) {
layoutInfo.put(this.getClass(), info);
}
}
if (needsInit) {
this.initializeFields();
}
return info.size;
} else {
return -1;
}
}
PS:这个地方遇到的坑:1,刚开始我么还没有收到厂家的关于C++ 的代码,刚开始都不知道怎么入参合适,都不知道dwsize这个字段是个啥意思。后来他们给了厂家的c++的代码,于是马上装了一个vs 2015的社区版,在大佬的帮助下才艰难的把这个C++ 的程序跑起来。(因为我也是一直做的Java开发,对C++也不是很熟悉,全靠大学的那一点儿基础,还好大学学的C,C++ 还勉强算学的可以的类型,不然人都要炸掉。)
在调用这个接口的时候遇到了以下的问题:
1,NET_IN_FIND_GROUP_INFO stuIn = new NET_IN_FIND_GROUP_INFO();
stuIn = 1000;的这个时候完全不知道这里该怎么赋值, 但是和厂家那边沟通他们那边感觉也不是开发人员,只知道这里是一定要赋值的,然后我就在这里赋值1000。 发现调用这个CLIENT_FindGroupInfo()接口的时候接口始终返回false;
后来给 NET_OUT_FIND_GROUP_INFO stuOut = new NET_OUT_FIND_GROUP_INFO();
后来给 stuOut.dwSize = 1000; 发现调用这个CLIENT_FindGroupInfo()接口的时候接口始终返回true 了;
解决方案: 后来发现 Structure 这个类中有一个size 参数,就是对应的是dwSize 这个参数的值:所以在构造函数中就将这个数值赋值进去。
2,我先把这个java部分代码贴出来:
public NET_FACERECONGNITION_GROUP_INFO[] findGroupInfo(LLong loginHandle , String groupId) {
NET_FACERECONGNITION_GROUP_INFO[] groupInfoRet = null;
/*
* 入参
*/
NET_IN_FIND_GROUP_INFO stuIn = new NET_IN_FIND_GROUP_INFO();
System.arraycopy(groupId.getBytes(), 0, stuIn.szGroupId, 0, groupId.getBytes().length);
/*
* 出参
*/
int max = 20;
NET_FACERECONGNITION_GROUP_INFO[] groupInfo = new NET_FACERECONGNITION_GROUP_INFO[max];
for(int i = 0; i < max; i++) {
groupInfo[i] = new NET_FACERECONGNITION_GROUP_INFO();
}
NET_OUT_FIND_GROUP_INFO stuOut = new NET_OUT_FIND_GROUP_INFO();
stuOut.pGroupInfos = new Memory(groupInfo[0].size() * groupInfo.length); // Pointer初始化
stuOut.pGroupInfos.clear(groupInfo[0].size() * groupInfo.length);
stuOut.nMaxGroupNum = groupInfo.length;
ToolKits.SetStructArrToPointerData(groupInfo, stuOut.pGroupInfos); // 将数组内存拷贝给Pointer
if(Clibrary.INSTANCE.CLIENT_FindGroupInfo(loginHandle , stuIn, stuOut, 4000)) {
// 将Pointer的值输出到 数组 NET_FACERECONGNITION_GROUP_INFO
ToolKits.GetPointerDataToStructArr(stuOut.pGroupInfos, groupInfo);
if(stuOut.nRetGroupNum > 0) {
// 根据设备返回的,将有效的人脸库信息返回
groupInfoRet = new NET_FACERECONGNITION_GROUP_INFO[stuOut.nRetGroupNum];
for(int i = 0; i < stuOut.nRetGroupNum; i++) {
groupInfoRet[i] = groupInfo[i];
}
}
} else {
log.info("查询人员信息失败");
return null;
}
return groupInfoRet;
}
如下是ToolKits 这个类的部分代码:
/**
* 将结构体数组拷贝到内存
*
* @param pNativeData
* @param pJavaStuArr
*/
public static void SetStructArrToPointerData(Structure[] pJavaStuArr, Pointer pNativeData) {
long offset = 0;
for (int i = 0; i < pJavaStuArr.length; ++i) {
SetStructDataToPointer(pJavaStuArr[i], pNativeData, offset);
offset += pJavaStuArr[i].size();
}
}
public static void SetStructDataToPointer(Structure pJavaStu, Pointer pNativeData, long OffsetOfpNativeData) {
pJavaStu.write();
Pointer pJavaMem = pJavaStu.getPointer();
pNativeData.write(OffsetOfpNativeData, pJavaMem.getByteArray(0, pJavaStu.size()), 0, pJavaStu.size());
}
头文件的说明:
NET_FACERECONGNITION_GROUP_INFO *pGroupInfos; // 人员组信息,由用户申请空间,大小为sizeof(NET_FACERECONGNITION_GROUP_INFO)*nMaxGroupNum。
C++中的代码:
memset(stuOutParam.pGroupInfos, 0, sizeof(NET_FACERECONGNITION_GROUP_INFO)*stuOutParam.nMaxGroupNum);
Java中的代码:
stuOut.pGroupInfos = new Memory(groupInfo[0].size() * groupInfo.length); // Pointer初始化
stuOut.pGroupInfos.clear(groupInfo[0].size() * groupInfo.length);
stuOut.nMaxGroupNum = groupInfo.length;
ToolKits.SetStructArrToPointerData(groupInfo, stuOut.pGroupInfos); // 将数组内存拷贝给Pointer
对于这种在 NET_OUT_FIND_GROUP_INFO 类中 有一个属性是 pGroupInfos 是 Pointer 类型的。
public Pointer pGroupInfos; // 人员组信息,由用户申请空间, 指向 NET_FACERECONGNITION_GROUP_INFO 的指针
说明:这个指针空间需要自己去开辟,指针指向NET_FACERECONGNITION_GROUP_INFO这个结构体。
1,首先用 Memory 开辟内存空间,然后调用java.sun.jna.Memory 类中的 clear()方法,目的是锁定内存,
本质是调用了jni里面的setMemory 方法。
2,结构体和Pointer之间的关系:
比如上面,我们会出现很多将Pointer 指向 结构体,
public static void SetStructDataToPointer(Structure pJavaStu, Pointer pNativeData, long OffsetOfpNativeData) {
pJavaStu.write();
Pointer pJavaMem = pJavaStu.getPointer();
pNativeData.write(OffsetOfpNativeData, pJavaMem.getByteArray(0, pJavaStu.size()), 0, pJavaStu.size());
}
pJavaStu.getPointer();便可以得到一个Pointer类型了
pJavaStu.write(); Writes the fields of the struct to native memory
pJavaStu.read(); Reads the fields of the struct from native memory
https://java-native-access.github.io/jna/4.2.0/com/sun/jna/Structure.html 上面截图的来源;
2,数据类型转换问题:
下面是我自己整理的 java 和 C++ 平台数据类型转换的一个表格
Java | C++ | 额外可以 |
---|---|---|
LLONG(自己定义)64bit 就是 LONG 类型 | LLONG | |
int | DWORD | |
int | BOOL | |
LONG | LDWORD | |
Pointer | void* | |
short | WORD | |
String / Pointer | char* | |
Structure | struct*/struct | |
String | const char * chDVRIP | |
| int * error | |
Pointer stuUIDS | Structure *stuUIDs | |
Pointer pBuffer | char * pBuffer |
四、总结
至此我在JNA这一块儿爬坑目前看似是告一段落,终于把接入商汤封装的SDK摄像头进行了封装。但是对 Structure 这个类的很多属性和 方法 仍然还不知道 是怎么设计和 运行的。比如 ALIGN_DEFAULT 、 ALIGN_NONE 、 ALIGN_GNUC 、ALIGN_MSVC 这四个 属性分别代表什么意思呢?欢迎大家在博客下多多讨论交流这方面的经验。