一,項目前提:
我們目前是想實作一個人臉識别考勤的項目,而廠商給我們所提供的是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 這四個 屬性分别代表什麼意思呢?歡迎大家在部落格下多多讨論交流這方面的經驗。