zoukankan      html  css  js  c++  java
  • .Net平台互操作技术:02. 技术介绍

    上一篇文章简单介绍了.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参数是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 平台调用性能提升技巧

    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

    无此类型

  • 相关阅读:
    日期操作
    sanchi
    502 Server dropped connection
    把项目挂载到composer上
    从composer上在本地创建一个项目
    初始化后,composer安装
    在项目目录初始化composer
    Linux安装composer
    linux网络编程之TCP/IP基础
    grep的用法
  • 原文地址:https://www.cnblogs.com/zhouwei0213/p/3264752.html
Copyright © 2011-2022 走看看