C#調用C/C++動態庫 封送結構體,結構體數組


因為實驗室圖像處理的算法都是在OpenCV下寫的,還有就是導航的算法也是用C++寫的,然后界面部分要求在C#下寫,所以不管是Socket通信,還是調用OpenCV的DLL模塊,都設計到了C#和C++數據類型的對應,還有結構體的封裝使用。在誇語言調用方面,Java和C#都只能調用C格式導出的動態庫,因為C數據類型比較單一,容易映射,兩者都是在本地端提供一套與之映射的C#或者Java的描述接口,通過底層處理這種映射關系達到調用的目的。

 一. 結構體的傳遞

#define JNAAPI extern "C" __declspec(dllexport) // C方式導出函數 
   
typedef struct     
{   
    int osVersion;   
    int majorVersion;   
    int minorVersion;   
    int buildNum;   
    int platFormId;   
    char szVersion[128];   
}OSINFO;  

//獲取版本信息(傳遞結構體指針)
JNAAPI bool SetVersionPtr(OSINFO *info)
{
	char * str = "Hello DLL";
	info->osVersion = 100;
	strcpy(info->szVersion, str);

	return true;
}
//獲取版本信息(傳遞結構體引用)
JNAAPI bool SetVersionRef(OSINFO &info)
{
	char * str = "Hello DLL";
	info.osVersion = 101;
	strcpy(info.szVersion, str);

	return true;
}

  

在C#下對應的結構體定義:

// OSINFO定義  
[StructLayout(LayoutKind.Sequential)]  
public struct OSINFO  
{  
    public int osVersion;  
    public int majorVersion;  
    public int minorVersion;  
    public int buildNum;  
    public int platFormId;  
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]  
    public string szVersion;  
}  

  

可以通過兩種方式來調用非托管的函數SetVersionPtr:

1. 方式一(傳入結構體引用),在C#中,結構體是以傳值的方式傳遞的,類才是一傳地址的方式傳遞的,加上關鍵字ref就可以了。C端傳遞了兩種不同類型的參數,都可以通過引用來解決。

[DllImport("jnalib.dll", EntryPoint = "GetVersionPtr")]  
public static extern bool GetVersionPtr(ref OSINFO info);  
public static extern bool GetVersionRef(ref OSINFO info);

  

2. 方式二(傳入IntPtr(平台通用指針))

 對應的函數在C#中的聲明:

[DllImport("CSharpInvokeCpp_CppDemo.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern bool SetVersionPtr(IntPtr pv);

    

測試代碼:

IntPtr pv = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(OSINFO)));
//對申請的非托管的內存賦值
//但是沒有對最后一個string賦值
for (int i = 0; i < 5; i++)
{
	Marshal.WriteInt32(pv, i * Marshal.SizeOf(typeof(Int32)), ((Int32)(i + 1)));
}
	
if (CppDLL.SetVersionPtr(pv))
{
	Console.WriteLine("--osVersion:{0}", Marshal.ReadInt32(pv, 0));
	Console.WriteLine("--Major:{0}", Marshal.ReadInt32(pv, 4));
	Console.WriteLine("--Minor:{0}", Marshal.ReadInt32(pv, 8));
	Console.WriteLine("--BuildNum:{0}", Marshal.ReadInt32(pv, 12));
	Console.WriteLine("--szVersion:{0}", Marshal.PtrToStringAnsi((IntPtr)(pv.ToInt32()+20)));
}
Marshal.FreeHGlobal(pv);

  

上面的兩種方法得區別是:

第一種方法是先創建一個等價的結構體,然后把這個結構體的引用傳遞給DLL中的接口函數。第二種方法是申請一塊結構體大小的非托管內存,然后向內存中寫入數據。調用的時候,只把這塊非托管的內存的指針傳遞給DLL中的接口函數。其實第二種方法可以不用事先定義結構體,只需要知道結構體的大小,申請相應的非托管內存就可以了。

二.結構體數組的傳遞

 DLL中非托管代碼:

//傳遞結構體數組指針
JNAAPI bool SetVersionArray(OSINFO *info, int nLen)
{
	char *str = "Hello DLL";

	for(int i = 0; i < nLen; i++)
	{
		info[i].osVersion = 100;
		strcpy(info[i].szVersion, str);
	}

	return true;
}

調用代碼接口:

//C#接口,對於包含數組類型,只能傳遞IntPtr
[DllImport("CSharpInvokeCpp_CppDemo.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern bool SetVersionArray(IntPtr pv, int nLen);

測試代碼:

//結構體數組的傳遞測試代碼
OSINFO[] infos = new OSINFO[2];
for (int i = 0; i < infos.Length; i++ )
{
	infos[i] = new OSINFO();
}
IntPtr[] ptrArr = new IntPtr[1]; 
ptrArr[0] = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(OSINFO)) * 2);
IntPtr pt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(OSINFO)));

Marshal.Copy(ptrArr, 0, pt, 1);
CppDLL.SetVersionArray(pt, 2);

for (int i = 0; i < infos.Length; i++)
{
	infos[i] = (OSINFO)Marshal.PtrToStructure((IntPtr)(pt.ToInt32() + i * Marshal.SizeOf(typeof(OSINFO))), typeof(OSINFO));
	Console.WriteLine("OsVersion:{0} szVersion:{1}", infos[i].osVersion, infos[i].szVersion);
}

Marshal.FreeHGlobal(ptrArr[0]);
Marshal.FreeHGlobal(pt);

  上面的代碼中有兩處需要說明一下:

(1)C#中的IntPtr指針做相應的運算的時候,要首先把IntPtr做相應的轉換,然后運算。運算結束之后還要再做相應的轉換,還原成原來的IntPtr。

(2)Marshal.Copy這個函數的功能是:將數據從托管數組復制到非托管內存指針,或從非托管內存指針復制到托管數組。

三. 復雜結構體的傳遞

1. 輸出參數,結構體作為指針傳出

 非托管部分代碼:

typedef struct 
{
	char name[20];
	int age;
	double scores[32];
}Student;

//Class中包含結構體數組類型
typedef struct
{
	int number;
	Student stedents[50];
}Class;

JNAAPI int GetClass(Class *pClass,int len)
{
	for(int i = 0; i < len; i++)
	{
		pClass[i].number = i;
		for(int j = 0; j< 50; j++)
		{
			//把name中的前20個字節用0代替
			memset(pClass[i].stedents[j].name, 0, 20);
			//給每個同學命名
			sprintf(pClass[i].stedents[j].name, "name_%d_%d", i, j);
			pClass[i].stedents[j].age = j % 2 == 0 ? 15:20;
		}//for
	}//for

	return 0;
}

  

上面DLL 的導出函數要求傳遞的參數為它自定義的Class結構體數組, 那么我們在C#調用它時也要自定義對應的結構體了,

我們可以定義為如下:

[StructLayout(LayoutKind.Sequential)]
struct Student
{
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
	public string name;
	public int age;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
	public double[] scores;
}
[StructLayout(LayoutKind.Sequential)]
struct Class
{
	public int number;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)]
	public Student[] students;

}

  需要注意的是,這2個結構體中的數組大小一定要跟C++中的限定一樣大小哦,接下來如何使用這個API來正確的獲取數據呢,大多數人可能想到像這樣的處理方式:

Class myclass = new Class();
IntPtr ptr=Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Class)));
GetClass(ptr);
Marshal.FreeHGlobal(ptr);

  

沒錯,這樣的處理是沒問題的,但是我們的API的參數是Class數組,這種處理方式只是傳遞一個Class結構體參數,所以這種方式在這里就不太合適了,!

 那大家就想到先Class[] myclass = new Class[MaxClass]; 然后在用Marshal.AllocHGlobal 來獲取myclass 數據的指針,

其實這樣也是錯的, 因為 Class結構中包含了,不能直接封送的Student結構,所以無論如何上面的想法是錯誤的!

那要怎么辦呢,其實很簡單,就是先分配一段非托管內存,並調用API后,再將非托管內容數據讀取到托管結構體數據中!

 

示例演示代碼如下:

// 接口定義   
[DllImport("CSharpInvokeCpp_CppDemo.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetClass(IntPtr pv, int len);
//復雜結構體傳遞測試代碼
int size = Marshal.SizeOf(typeof(Class)) * 50;
IntPtr pBuff = Marshal.AllocHGlobal(size);
CppDLL.GetClass(pBuff, 50);

Class[] pClass = new Class[50];
for (int i = 0; i < 50; i++)
{
	IntPtr pr = new IntPtr(pBuff.ToInt64() + Marshal.SizeOf(typeof(Class)) * i);
	pClass[i] = (Class)Marshal.PtrToStructure(pr, typeof(Class));
}
Marshal.FreeHGlobal(pBuff);

  

5月19日學習內容:

http://tcspecial.iteye.com/blog/1675621

http://www.cnblogs.com/naiking/archive/2013/01/17/2864132.html

http://blog.csdn.net/zhangj1012003_2007/article/details/6283032

http://blog.csdn.net/xiaowei_cqu/article/details/7693985


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM