Contents
C与C#的交互
在C#执行C编写的DLL时可能会出现很多问题,而绝大部分问题都出自数据封装传送上,接下来总结了本次交互实验。
环境使用MSVC2019,使用__declspec(dllexport)作为DLL导出关键字,使用__stdcall作为调用协议。
为了更方便使用,声明两个宏:(WINAPI就是__stdcall,来自于windows.h)
#define DLLEXPORT __declspec(dllexport)
#define CALLAPI WINAPI
C#中使用char*和sbyte*等指针使用new string(指针)的方式来构建。
C中可能会出现一些Windows定义的类型,如LPCWSTR,代表着(long pointer const wstring),实际类型为const wchar_t*,像这样的类型还有很多,但他们都是通用的。
实验1:返回一个字符串
char str[30] = "hello世界!"; DLLEXPORT char* WINAPI GetString() { return str; }
C#端代码:
[DllImport(DLLNAME)] private extern static string GetString();
使用string接受返回char*,执行结果:崩溃
[DllImport(DLLNAME)] private extern static char* GetString();
使用char*接受返回char*,执行结果:乱码。
应该是大小不符导致的,C#的char是2字节的,因此尝试sbyte。
[DllImport(DLLNAME)] private extern static sbyte* GetString();
sbyte有符号字节和C的char一样,执行结果:正确。
总结:
因为C#侧没有预分配内存空间,所以只能使用指针来接受后在构建字符串。
实验2:返回一个宽字符串
wchar_t wstr[30] = L"hello世界!"; DLLEXPORT wchar_t* WINAPI GetWString() { return wstr; }
C#端代码:
[DllImport(DLLNAME, CharSet = CharSet.Unicode)] private extern static string GetWString();
执行结果:崩溃
[DllImport(DLLNAME, CharSet = CharSet.Unicode)] private extern static char* GetWString();
执行结果:正确
实验3:通过参数指针传出字符串
char str[30] = "hello世界!"; DLLEXPORT void WINAPI GetOutString(char* out_str) { strcpy(out_str, str); }
C#端代码:
[DllImport(DLLNAME)] private extern static void GetOutString(ref string str);
string内存不可写,执行结果:异常。
[DllImport(DLLNAME)] private extern static void GetOutString(StringBuilder sb);
执行结果:正确
[DllImport(DLLNAME)] private extern static void GetOutString(char* str);
执行结果:乱码
[DllImport(DLLNAME)] private extern static void GetOutString(sbyte* str);
执行结果:正确
同样是数据大小的差异问题
实验4:通过参数传出一个字符串数组
char str1[30] = "hello世界!"; char str2[30] = "第二个str"; DLLEXPORT void WINAPI GetOutString(char** out_str, int* count) { *count = 2; strcpy(out_str[0], str1); strcpy(out_str[1], str2); }
C#代码:
[DllImport(DLLNAME)] private extern static void GetOutString(sbyte** str, ref int count); //因实现复杂,代码发上来,分配的大小要足够大,否则会出现内存不可写异常 sbyte** bytes = stackalloc sbyte*[8]; for (int i = 0; i < 8; i++) { sbyte* t = stackalloc sbyte[64]; bytes[i] = t; } int count = 0; GetOutString(bytes, ref count); string[] strs = new string[count]; for (int i = 0; i < count; i++) { strs[0] = new string(bytes[i]); Console.WriteLine(strs[0]); }
实验结果:正确
先分配一个指针数组,在让数组里的指针指向字符数组(字符串),就是字符串数组了
c中使用char则C#使用sbyte,同样的,c中使用wchar_t则C#中使用char
[DllImport(DLLNAME)] private extern static void GetOutString(StringBuilder[] sbs, ref int count); //实现 StringBuilder[] sbs = new StringBuilder[8]; for (int i = 0; i < 8; i++) { sbs[i] = new StringBuilder(64); } int count = 0; GetOutString(sbs, ref count); for (int i = 0; i < count; i++) { Console.WriteLine(sbs[i].ToString()); }
实验结果:意料之外的结果
实验5:结构体的传递
typedef struct Email { int id; wchar_t name[16]; } Email; DLLEXPORT void GetEmail(Email* out_email) { out_email->id = 3; wcscpy(out_email->name, L"jomixedyu"); }
C#端
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Email { public int id; public StringBuilder name; } [DllImport(DLLNAME)] private extern static void GetEmail(ref Email email);
无法封送Stringbuilder类型,执行结果:异常
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Email { public int id; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public char[] name; } [DllImport(DLLNAME)] private extern static void GetEmail(ref Email email);
声明封送非托管类型为字符数组,执行结果:正确
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Email { public int id; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string name; } [DllImport(DLLNAME)] private extern static void GetEmail(ref Email email);
声明封送非托管字符串,执行结果:正确
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Email { public int id; public fixed char name[16]; } [DllImport(DLLNAME)] private extern static void GetEmail(ref Email email);
固定的数组大小,执行结果:正确
实验6:函数指针与委托传递
typedef wchar_t* (*CallBack)(wchar_t*); CallBack g_cb; DLLEXPORT void CALLAPI Register(CallBack cb) { g_cb = cb; } DLLEXPORT void CALLAPI Trigger(wchar_t* str) { wprintf(L"c recived: %s\n", str); wchar_t* _str = g_cb(str); wprintf(L"c call result: %s\n", _str); }
C#代码:
public delegate string CallBack(string str); [DllImport(DLLNAME, CharSet = CharSet.Unicode)] private extern static void Register(CallBack cb); [DllImport(DLLNAME, CharSet = CharSet.Unicode)] private extern static void Trigger(string str); //实现 Register(str => { Console.WriteLine("收到str: " + str); return str; }); Trigger("hello世界!");
执行结果:
c recived: hello??! 收到str: h c call result: h??????????????????????????????
需要给委托的传参和返回值标示为宽字符的字符串
[return: MarshalAs(UnmanagedType.LPWStr)] public delegate string CallBack([MarshalAs(UnmanagedType.LPWStr)] string str);
再次执行结果:正确。
也可以直接使用char*(对应c的wchar_t*)来声明委托
public delegate char* CallBack(char* str);
实现:
Register(str => { string r = new string(str); Console.WriteLine("收到str: " + r); char[] ca = r.ToArray(); fixed(char* cp = ca) { return cp; } }); Trigger("hello世界!");
执行结果:正确
虽然此段代码正确但并不安全,数组是托管对象,可能会在返回后被移动。但返回str指针是安全的。
问题与实验总结
当编写C时,指针最好使用参数传出的方式让C#调用。如果只是返回全局或静态指针还可以,如果是局部变量会造成悬垂,如果分配堆内存的话C#侧是不知道的,内存不好释放。所以让C#来准备对象,C侧把数据填入C#侧准备好的内存中,然后在C#侧管理内存。
在程序编写的过程中一定要清楚哪些数据是C侧的,哪些是C#侧的,指向C侧的指针数据不会移动,而指向C#侧的指针可能会被GC移动,要格外小心。
踩坑
在传递数组的时候需要注意个问题,就是在C侧的数组有可能只写了部分内存,比如固定的8长度数组,但是只写了4长度,剩下的内存没写并且没有在写数据之前将内存清0,则剩下的内存中乱七八糟的数据在C#中构造则会出现不可预知的后果。
当C#传递给C++委托时,应该使用指向静态方法的委托,否则可能会因堆对象移动导致this失效问题。
文章评论