上一篇文章简单介绍了.Net平台互操作技术的面临的主要问题,以及主要的解决方案。本文将重点介绍使用相对较多的P/Invoke技术的实现:C#通过P/Invoke调用Native C++ Dll技术、C#调用Native C++代码示例、非托管内存的释放和平台调用性能提升技巧。

1 C# 通过P/Invoke调用Native C++ Dll技术

1.1 C# 中安全代码与不安全代码

通常,公共语言运行时(CLR)负责检查 Microsoft 中间语言(MSIL)代码的行为,防止任何有问题的操作。但是,有时您希望直接访问低级功能(如:Native C++模块、Win32 API调用等),表现为通过指针操作内存。为此,C#提供了对不安全(不安全指的是内存不会被管理)类型代码的支持。不安全代码必须放在源代码中的不安全代码块内。

1.1.1 unsafe 关键字

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
}

1.1.2 fixed 关键字

在安全代码中,垃圾回收器在对象的生命周期内可以自由地移动对象,以组织和压缩可用资源。但是,如果代码使用了指针,则此行为可能很容易造成意外的结果,因此您可以使用fixed语句来指示垃圾回收器不要移动某些对象。下面的代码演示了使用fixed关键字以确保在执行方法中的不安全代码块时系统不会移动数组。注意:fixed 只能用于不安全的代码中:

unsafe
{
   fixed (char *p = array)
   {
       for (int i=0; i<array.Length; i++) {//logic}
   }
}

在我们的实际开发中,较少用到这两个关键字。

1.2 C#中的DllImport详细介绍

1.2.1命名空间:

using System.Runtime.InteropServices;

1.2.2 DllImport说明

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.2.3 DllImport的用法:

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; }

1.3 数据封送处理

在托管代码对非托管函数进行平台调用时,会进行数据封送处理。封送指的是在托管内存和非托管内存之间传递数据的过程。它是一个双向的过程,不仅在托管代码向非托管代码传递参数时发生,在非托管代码向托管代码返回结果时也发生。封送过程由封送拆收器完成,主要有三项任务:首先将数据从非托管类型转换为托管类型,或者由托管类型转换为非托管类型。然后,再将经过类型转换的数据从非托管内存复制到托管内存,或者从托管内存复制到非托管内存。最后,在调用完成后,释放掉封送过程中分配的内存。

1.3.1封送字符串

由于不同的编程语言对字符串的实现机制不同,因此导致在托管代码中平台调用C/C++函数时,必须对字符串进行特殊的封送处理。主要注意一下几点:

1. 字符是ANSI格式还是Unicode格式,需要设置相应的DllImport的CharSet参数。

2. 在托管代码中使用相应的字符类型与非托管字符类型对应。

3. 注意释放非托管内存,避免内存泄漏。

4. 注意字符参数的方向属性。如果需要将非托管代码对字符串的修改返回托管代码,则必须使用StringBuilder。

1.3.2封送结构体

无论是对作为参数的结构体,还是对作为返回值的结构体进行封送,主要注意以下几点。

1. 必须在托管代码中定义一个与非托管结构体等价的托管结构体。

2. 善于使用StructLayout属性及其参数来指定结构体的内存布局和对齐方式。

1.3.3封送类

对类的封送和对结构体的封送的方式类似。他们之间的区别在于结构体是值传递,类是传递引用。对于非blittable引用类型,非托管代码对它的修改不会反应到托管代码中,除非显示地使用[In, Out]或者ref / out标识。

1.3.4封送数组

数组传递的是引用。在传递的时候适用封送类的情况。

2 C#调用Native C++代码示例

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;
}

2.2参数是int*类型,返回值是int*类型的示例

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;
}

2.3参数是char*、wchar_t*类型的示例

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;
}

2.4返回值是char*类型的示例

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;
}

2.5参数是char数组,同时也是返回值

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参数是Structure\Class类型,返回Structure\Class指针类型的示例

由于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 平台调用性能提升技巧

4.1显示地指定要调用的非托管函数的名称

通过将关键字ExactSpelling设置为true显示指定非托管函数的名称,缩短CLR寻找非托管函数的时间。否则,他将会按照一定的规则模糊的搜索非托管函数。

4.2对数据封送处理进行优化

a. 尽量使用blittable数据类型:CLR对数据封送时,有两种选择:锁定数据和复制数据。第二种方式会耗费多一些时间,因为他有一个数据转换的过程。而blittable数据类型采用的是第一种方式。

b. 尽可能的减少数据封送的次数:如:将一些循环逻辑转移到非托管代码中,而不要循环的调用非托管函数。

4.3尽量避免字符串编码转换

.Net采用的是Unicode编码,如果要调用的非托管函数采用的是ANSI编码,那么就会有类型转换的过程,耗费性能。所以,在非托管代码中应该尽可能的采用Unicode编码方式。

4.4 Native C++和托管平台基本类型位宽比较

在托管平台和非托管平台,即使是相同的数据类型。也会占用不同的位宽。了解他们之间的差异,对在数据封送和类型转换中选择合适的数据类型非常重要。例如,在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

无此类型

posted on 2013-08-17 16:21  Maxwell Zhou  阅读(494)  评论(0编辑  收藏  举报