天天看点

Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、JNA爬坑历路 四、总结

一,项目前提:

        我们目前是想实现一个人脸识别考勤的项目,而厂商给我们所提供的是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的简单应用

Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、JNA爬坑历路 四、总结

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的维基百科解释

Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、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 。

Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、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()方法,目的是锁定内存,

Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、JNA爬坑历路 四、总结

 本质是调用了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
           
Jna调用C++使用心得分享一,项目前提:二,项目方案选择: 三、JNA爬坑历路 四、总结

 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
IntByReference
           
int * error
Pointer stuUIDS Structure *stuUIDs
Pointer pBuffer char *  pBuffer

四、总结

至此我在JNA这一块儿爬坑目前看似是告一段落,终于把接入商汤封装的SDK摄像头进行了封装。但是对 Structure 这个类的很多属性和 方法 仍然还不知道 是怎么设计和 运行的。比如 ALIGN_DEFAULT 、 ALIGN_NONE 、 ALIGN_GNUC 、ALIGN_MSVC 这四个 属性分别代表什么意思呢?欢迎大家在博客下多多讨论交流这方面的经验。

JNA

继续阅读