上一篇文章简单介绍了.Net平台互操作技术的面临的主要问题,以及主要的解决方案。本文将重点介绍使用相对较多的P/Invoke技术的实现:C#通过P/Invoke调用Native C++ Dll技术、C#调用Native C++代码示例、非托管内存的释放和平台调用性能提升技巧。
1 C# 通过P/Invoke调用Native C++ Dll技术
通常,公共语言运行时(CLR)负责检查 Microsoft 中间语言(MSIL)代码的行为,防止任何有问题的操作。但是,有时您希望直接访问低级功能(如:Native C++模块、Win32 API调用等),表现为通过指针操作内存。为此,C#提供了对不安全(不安全指的是内存不会被管理)类型代码的支持。不安全代码必须放在源代码中的不安全代码块内。
C#中不安全的代码必须用unsafe关键字标识出来。unsafe可以标识整个方法、大括号内的代码块和单个语句。下面代码演示如何使用unsafe关键字:
unsafe static void PointyMethod() { //unsafe function } static void StillPointy() { unsafe { //unsafe code block } int i = 10; unsafe int* p = &i; //unsafe statement }
在安全代码中,垃圾回收器在对象的生命周期内可以自由地移动对象,以组织和压缩可用资源。但是,如果代码使用了指针,则此行为可能很容易造成意外的结果,因此您可以使用fixed语句来指示垃圾回收器不要移动某些对象。下面的代码演示了使用fixed关键字以确保在执行方法中的不安全代码块时系统不会移动数组。注意:fixed 只能用于不安全的代码中:
unsafe { fixed (char *p = array) { for (int i=0; i<array.Length; i++) {//logic} } }
在我们的实际开发中,较少用到这两个关键字。
using System.Runtime.InteropServices;
1) DllImport只能放置在方法声明上。
2) DllImport具有单个定位参数:指定导入Dll的地址。
3) DllImport具有五个命名参数:
a) CallingConvention:指示入口点的调用约定。默认值:CallingConvention.Winapi
b) CharSet :指示用在入口点中的字符集。默认值:CharSet.Auto
c) EntryPoint:指示Dll中入口点的名称。默认值:方法本身的名称
d) ExactSpelling:指示EntryPoint是否必须与指示的入口点拼写完全匹配。默认值:False
e) PreserveSig:指示方法的签名应当被保留还是被转换。默认值:True
f) SetLastError:指示方法是否保留Win 32“上一错误”。默认值:False
4) DllImport是一次性属性类
5) 用DllImport属性修饰的方法必须具有extern修饰符。
1) 静态调用Native C++ Dll
[DllImport("myDll.dll", EntryPoint = "fun")] public static extern int fun(int a, int b);
2) 动态调用Native C++ Dll
[DllImport("kernel32.dll", EntryPoint = "LoadLibrary")] public static extern int LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName); [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")] public static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName); [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")] public static extern bool FreeLibrary(int hModule); int hModule = NativeMethod.LoadLibrary("myDll.dll");//动态读取 if (hModule == 0) { return; } IntPtr add = NativeMethod.GetProcAddress(hModule, "Add"); if (add == IntPtr.Zero) { return; }
在托管代码对非托管函数进行平台调用时,会进行数据封送处理。封送指的是在托管内存和非托管内存之间传递数据的过程。它是一个双向的过程,不仅在托管代码向非托管代码传递参数时发生,在非托管代码向托管代码返回结果时也发生。封送过程由封送拆收器完成,主要有三项任务:首先将数据从非托管类型转换为托管类型,或者由托管类型转换为非托管类型。然后,再将经过类型转换的数据从非托管内存复制到托管内存,或者从托管内存复制到非托管内存。最后,在调用完成后,释放掉封送过程中分配的内存。
由于不同的编程语言对字符串的实现机制不同,因此导致在托管代码中平台调用C/C++函数时,必须对字符串进行特殊的封送处理。主要注意一下几点:
1. 字符是ANSI格式还是Unicode格式,需要设置相应的DllImport的CharSet参数。
2. 在托管代码中使用相应的字符类型与非托管字符类型对应。
3. 注意释放非托管内存,避免内存泄漏。
4. 注意字符参数的方向属性。如果需要将非托管代码对字符串的修改返回托管代码,则必须使用StringBuilder。
无论是对作为参数的结构体,还是对作为返回值的结构体进行封送,主要注意以下几点。
1. 必须在托管代码中定义一个与非托管结构体等价的托管结构体。
2. 善于使用StructLayout属性及其参数来指定结构体的内存布局和对齐方式。
对类的封送和对结构体的封送的方式类似。他们之间的区别在于结构体是值传递,类是传递引用。对于非blittable引用类型,非托管代码对它的修改不会反应到托管代码中,除非显示地使用[In, Out]或者ref / out标识。
数组传递的是引用。在传递的时候适用封送类的情况。
2.1参数是int类型的示例
C# Code [DllImport("myDll.dll", EntryPoint = "fun")] public static extern int fun(int a, int b); C++ Code extern "C" __declspec(dllexport) int fun(int a, int b) { return a+b; }
C# Code [DllImport("myDll.dll", EntryPoint = "fun")] unsafe public static extern IntPtr fun(ref int a, ref int b, ref int result); C++ Code extern "C" __declspec(dllexport) int* fun(int* a, int* b, int* result) { *result = (*a+*b); return result; }
C# Code [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)] public extern static void TestStringMarshalArguments( [MarshalAs(UnmanagedType.LPStr)] string inAnsiString, [MarshalAs(UnmanagedType.LPWStr)] string inUnicodeString, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder outStringBuffer, int outBufferSize); C++ Code extern "C" __declspec(dllexport) void __cdecl TestStringMarshalArguments(const char* inAnsiString, const wchar_t* inUnicodeString, wchar_t* outUnicodeString, int outBufferSize) { size_t ansiStrLength = strlen(inAnsiString); size_t uniStrLength = wcslen(inUnicodeString); size_t totalSize = ansiStrLength + uniStrLength + 2; wchar_t* tempBuffer = new(std::nothrow) wchar_t[totalSize]; if(NULL == tempBuffer) { return; } wmemset(tempBuffer, 0, totalSize); mbstowcs(tempBuffer, inAnsiString, totalSize); wcscat_s(tempBuffer, totalSize, L" "); wcscat_s(tempBuffer, totalSize, inUnicodeString); wcscpy_s(outUnicodeString, outBufferSize, tempBuffer); delete[] tempBuffer; }
C# Code [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public extern static IntPtr TestStringAsResult(int id); C++ Code extern "C" __declspec(dllexport) char* __cdecl TestStringAsResult(int id) { int size = 64; char* result = (char*)CoTaskMemAlloc(size); sprintf_s(result, size/sizeof(char), "Result of ID: %d", id); return result; }
C# Code [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public extern static uint TestArrayOfChar([In, Out] char[] charArray, int arraySize); C++ Code extern "C" __declspec(dllexport) UINT __cdecl TestArrayOfChar(char charArray[], int arraySize) { int result = 0; for(int i = 0; i < arraySize; i++) { if (isdigit(charArray[i])) { result++; charArray[i] = '@'; } } return result; }
2.6参数是StructureClass类型,返回StructureClass指针类型的示例
由于C++自定义类型在C#中不会被识别,解决方法是在C#中定义等价的结构体,然后传递内存地址给Native C++ Dll。需要注意的是,相同类型在C++中和C#中所占的字节是不一样的。在处理布局内存的时候,需要具体考虑。在C#中定义对应的结构时,LayoutKind可以选择:Auto、Sequential和Explicit。Explicit模式下,可以通过FieldOffset(num)显示的指定占用的字节数。示例如下:
C# Code //Structure definition [StructLayout(LayoutKind.Sequential)] public struct CPerson { public int Age; public double Height; [MarshalAS(UnmanagedType.LPStr)] public string Name; } //funtion in C# [DllImport("myDll.dll", EntryPoint = "fun")] unsafe public static extern IntPtr fun(ref CPerson a, ref CPerson b, ref CPerson result); C++ code //Class definition #pragma once class __declspec(dllexport) CPerson { public: CPerson(); void SetAge(int iAge); int GetAge(); public: int Age; double Height; char* Name; }; CPerson::CPerson() { } void CPerson::SetAge(int iAge){ Age = iAge; } int CPerson::GetAge(){ return Age; } //function impletmentation in C++ extern "C" __declspec(dllexport) CPerson* fun(CPerson* cPerson1,CPerson* cPerson2,CPerson* result) { if(cPerson1==NULL||cPerson2==NULL){return NULL;} result->SetAge(cPerson1->GetAge()+cPerson2->GetAge()); return result; }
3 非托管内存的释放
在前面的例子中,为了力求简洁的说明问题,没有考虑非托管内存的释放。实际编程中,需要及时的释放非托管程序中动态申请的内存空间,否则会造成内存泄漏。
要想成功的释放非托管内存,首先要清楚它生产的方式(malloc, new etc),然后用相应的方式(free, delete etc)释放内存。在非托管环境中主要有三种申请内存的方法:new(使用delete释放内存), malloc(使用free释放内存)和CoTaskMemAlloc(使用CoTaskMemFree释放内存)。如果是前两种方式申请的内存,在托管代码中无法直接对其进行释放,必须在非托管代码中实现一个能够释放此非托管内存的方法,然后在托管代码中调用该方法对非托管内存进行释放。示例程序如下:
C# Code [DllImport(dllPath, CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern void FreeMallocMemory(IntPtr buffer); [DllImport(dllPath, CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern IntPtr GetStringMalloc(); private static void TestGetString() { try { IntPtr stringPtr = NativeMethod.GetStringMalloc(); string str = Marshal.PtrToStringUni(stringPtr); NativeMethod.FreeMallocMemory(stringPtr); } catch (Exception ex) { } } C++ code extern "C" __declspec(dllexport) void FreeMallocMemory(void* buffer) { if(NULL != buffer) { free(buffer); buffer = NULL; } } extern "C" __declspec(dllexport) wchar_t* GetStringMalloc() { int size = 128; wchar_t* buffer = (wchar_t*)malloc(size); if(NULL != buffer) { wcscpy_s(buffer, size / sizeof(wchar_t), L"String from Malloc"); } return buffer; }
如果是采用CoTaskMemAlloc方式申请内存,那么封送拆收器是能够将其释放掉的。原因在于封送拆收器在对非托管内存进行处理时,会将CoTaskMemAlloc作为分配内存的默认方式。因此,当封送拆收器将一个非托管内存指针封送成.Net对象时,封送拆收器会使用非托管数据的一个复制创建一个.Net对象。由于非托管数据已经被封送拆收器获取,因此封送拆收器就会使用相应的释放内存的方式CoTaskMemFree来释放掉这块已经被封送过的非托管内存。所以,在编程托管代码与非托管代码交互的程序时,推荐使用CoTaskMemAlloc方法申请内存。
4 平台调用性能提升技巧
通过将关键字ExactSpelling设置为true显示指定非托管函数的名称,缩短CLR寻找非托管函数的时间。否则,他将会按照一定的规则模糊的搜索非托管函数。
a. 尽量使用blittable数据类型:CLR对数据封送时,有两种选择:锁定数据和复制数据。第二种方式会耗费多一些时间,因为他有一个数据转换的过程。而blittable数据类型采用的是第一种方式。
b. 尽可能的减少数据封送的次数:如:将一些循环逻辑转移到非托管代码中,而不要循环的调用非托管函数。
.Net采用的是Unicode编码,如果要调用的非托管函数采用的是ANSI编码,那么就会有类型转换的过程,耗费性能。所以,在非托管代码中应该尽可能的采用Unicode编码方式。
在托管平台和非托管平台,即使是相同的数据类型。也会占用不同的位宽。了解他们之间的差异,对在数据封送和类型转换中选择合适的数据类型非常重要。例如,在Native C++中,void*类型被强制转换为char*类型,那么在C#环境中应该将void*类型强制转换为byte*类型才能达到和Native环境等价的效果。下表列出了Native环境和托管环境各基本数据类型的位宽:
数据类型 平台类型 |
Native C++ |
托管环境(C#) |
int |
4 byte |
4 byte |
long |
4 byte |
8 byte |
short |
2 byte |
2 byte |
byte |
1 byte |
1 byte |
char |
1 byte |
2 byte |
wchar_t |
2 byte |
无此类型 |
double |
8 byte |
8 byte |
float |
4 byte |
4 byte |
void |
0 byte |
无此类型 |