zoukankan      html  css  js  c++  java
  • Net调用非托管代码(P/Invoke与C++InterOP) [转]

    将 System::String 转换为 wchar_t* 或 char*

    PtrToStringChars将String转换为本机wchar_t *或char *
    。由于 CLR 字符串为内部 Unicode,因此这样通常会返回一个 Unicode 宽字符串指针。然后可以将其转换为宽字符串

    1 .Net互操作
    .Net不能直接操作非托管代码,这时就需要互操作了。
    1.1 P/Invoke
    许多常用Windows操作都有托管接口,但是还有许多完整的 Win32 部分没有托管接口。如何操作呢?平台调用 (P/Invoke) 就是完成这一任务的最常用方法。要使用 P/Invoke,您可以编写一个描述如何调用函数的原型,然后运行时将使用此信息进行调用。

    1.1.1 枚举和常量
    以MessageBeep()为例。MSDN 给出了以下原型:
    BOOL MessageBeep(
     UINT uType // 声音类型
    );
    这看起来很简单,但是从注释中可以发现两个有趣的事实。
        首先,uType 参数实际上接受一组预先定义的常量。
        其次,可能的参数值包括 -1,这意味着尽管它被定义为 uint 类型,但 int 会更加适合。对于 uType 参数,使用 enum 类型是合乎情理的。
    public enum BeepType
    {
      SimpleBeep = -1,
      IconAsterisk = 0x00000040,
      IconExclamation = 0x00000030,
      IconHand = 0x00000010,
      IconQuestion = 0x00000020,
      Ok = 0x00000000,
    }
    [DllImport("user32.dll")]
    public static extern bool MessageBeep(BeepType beepType);   
    现在我可以用下面的语句来调用它: MessageBeep(BeepType.IconQuestion);
    如果常量为其他类型(非int),则需要修改枚举类型的基本类型
    enum Name : Type {…}
    1.1.2 处理普通结构体
    有时我需要确定我笔记本的电池状况。Win32 为此提供了电源管理函数。
    BOOL GetSystemPowerStatus(
     LPSYSTEM_POWER_STATUS lpSystemPowerStatus
    );   
    此函数包含指向某个结构的指针,我们尚未对此进行过处理。要处理结构,我们需要用 C# 定义结构。我们从非托管的定义开始:
    typedef struct _SYSTEM_POWER_STATUS {
      BYTE  ACLineStatus;
      BYTE  BatteryFlag;
      BYTE  BatteryLifePercent;
      BYTE  Reserved1;
      DWORD BatteryLifeTime;
      DWORD BatteryFullLifeTime;
    } SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;
    然后,通过用 C# 类型代替 C 类型来得到 C# 版本。
    struct SystemPowerStatus
    {
      byte ACLineStatus;
      byte batteryFlag;
      byte batteryLifePercent;
      byte reserved1;
      int batteryLifeTime;
      int batteryFullLifeTime;
    }
    这样,就可以方便地编写出 C# 原型:
    [DllImport("kernel32.dll")]
    public static extern bool GetSystemPowerStatus( ref SystemPowerStatus systemPowerStatus);   
    在此原型中,我们用“ref”指明将传递结构指针而不是结构值。这是处理通过指针传递的结构的一般方法。
    此函数运行良好,但是最好将 ACLineStatus 和 batteryFlag 字段定义为 enum:
    enum ACLineStatus: byte
    {
     Offline = 0,
     Online = 1,
     Unknown = 255,
    }
    enum BatteryFlag: byte
    { ...}   
    请注意,由于结构的字段是一些字节,因此我们使用 byte 作为该 enum 的基本类型。

    1.1.3 处理内嵌指针的结构体
    有时我们要调用的函数的参数为包含指针的结构体,对于这样的参数,如何处理呢?
    struct CXTest
    {
    LPBYTE pData;     // 一个指向byte数组的指针
    int nLen;         // 数组的长度
    }
    BOOL WINAPI XFunction(const CXTest &inData_, CXTest &outData_);
    在C#中我们如何去调用呢
    struct CXTest
    {
    public IntPrt pData;
    public int nLen;
    }
    static extern bool XFunction(ref [In] CXTest inData_, ref CXTest outData_);
    下面就来看一下具体调用了,设数组长度为nDataLen
    CXTest stIn = new CXTest(), stOut = new CXTest();
    byte[] pIn = new byte[nDataLen];
    // 为数组赋值
    stIn.pData = Marshal.AllocHGlobal(nDataLen);
    Marshal.Copy(pIn, 0, stIn.pData, nDataLen);
    stIn.nLen = nDataLen;
    stOut.pData = Marshal.AllocHGlobal(nDataLen);
    stOut.nLen = nDataLen;
    XFunction(ref stIn, ref stOut);
    byte[] pOut = new byte[nDataLen];
    Marshal.Copy(stOut.pData, pOut, 0, nDataLen);
    // ....
    Marshal.FreeHGlobal(stIn.pData);
    Marshal.FreeHGlobal(stOut.pData);
    此处最重要的是要注意,pData的内存要先申请,再向里copy数据;还有最后要记得释放申请的内存。
     
    1.1.4 处理内嵌数组与字符串的结构体
    C/C++下的定义与实现:
    struct CXTest
    {
    WCHAR wzName[64];
    int nLen;
    byte byData[100];
    }
    bool SetTest(const CXTest &stTest_);
    在C#下,为了方便初始化byte数组,我们使用类来代替结构
    [StructLayout(LayoutKind.Sequential, Pack=2, CharSet=CharSet.Unicode)]
    class CXTest
    {
     public void Init()
    {
     strName = "";
    nLen = 0;
    byData = new byte[100];
    }
     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64))]
    public string strName;
     public int nLen;
     [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
    public byte[] byData;
    }
    stataic extern bool SetTest(CXTest stTest_);
    定义后,虽然为byData预留的空间,但是其指向null,不能为其复制。由于结构体不能自定义缺省参数,所以增加一个Init函数或通过类来替换来初始化byData。
    从底层接口中获取数据一定要使用struct,且从底层接口中(out)获取数据后,byData就自动指向了实际的内容了。向底层接口中设定数据时,如 果使用struct一定要先调用init,并且通过ref方式;如果是类,则不能使用ref修饰(C#中:类默认放在堆中,结构体默认放在栈中的)。
     
    1.1.5 字符串与字符串缓冲区
    在 Win32 中还有两种不同的字符串表示:ANSI、Unicode。由于 P/Invoke 的设计者不想让您为所在的平台操心,因此他们提供了内置的支持来自动使用 A 或 W 版本。如果您调用的函数不存在,互操作层将为您查找并使用 A 或 W 版本。但是互操作的默认字符类型是 Ansi 或单字节,如果非托管代码为宽字符,则需要明确的把CharSet设为CharSet.Unicode。
    .NET 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。对于要将字符串值复制到字符串缓冲区的函数,字符串将无效。这样做至少会破坏由封送拆收 器在转换字符串时创建的临时缓冲区;严重时会破坏托管堆,而这通常会导致错误的发生。无论哪种情况都不可能获得正确的返回值。
    要解决此问题,我们需要使用其他类型。StringBuilder 类型就是被设计为用作缓冲区的,我们将使用它来代替字符串。下面是一个示例:
    C格式函数声明:
    DWORD GetShortPathName(
      LPCTSTR lpszLongPath,
      LPTSTR lpszShortPath,
      DWORD cchBuffer
    );
    C#中封装
    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    public static extern int GetShortPathName(
      [MarshalAs(UnmanagedType.LPTStr)]
      string path,
      [MarshalAs(UnmanagedType.LPTStr)]
      StringBuilder shortPath,
      int shortPathLength);   

    使用此函数很简单:
    StringBuilder shortPath = new StringBuilder(80);
    int result = GetShortPathName(
    @"d:dest.jpg", shortPath, shortPath.Capacity);
    string s = shortPath.ToString();
    请注意,StringBuilder 的 Capacity 传递的是缓冲区大小。

    1.1.6 指针参数
    许多 Windows API 函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有了指 针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多 种。

    封送不透明 (Opaque) 指针:一种特殊情况
    有时在 Windows API 中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给 Windows 以便随后进行重用。一个非常常见的例子就是句柄的概念。
    当一个不透明指针返回给您的应用程序(或者您的应用程序期望得到一个不透明指针)时,您应该将参数或返回值封送为 CLR 中的一种特殊类型 — System.IntPtr。当您使用 IntPtr 类型时,通常不使用 out 或 ref 参数,因为 IntPtr 意为直接持有指针。不过,如果您将一个指针封送为一个指针,则对 IntPtr 使用 by-ref 参数是合适的。
    在 CLR 类型系统中,System.IntPtr 类型有一个特殊的属性。不像系统中的其他基类型,IntPtr 并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在 32 位的 Windows 中,IntPtr 变量的宽度是 32 位的,而在 64 位的 Windows 中,实时编译器编译的代码会将 IntPtr 值看作 64 位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。
    您可以在托管代码中将 IntPtr 值强制转换为 32 位或 64 位的整数值,或将后者强制转换为前者。然而,当使用 Windows API 函数时,因为指针应是不透明的,所以除了存储和传递给外部方法外,不能将它们另做它用。这种“只限存储和传递”规则的两个特例是当您需要向外部方法传递 null 指针值和需要比较 IntPtr 值与 null 值的情况。为了做到这一点,您不能将零强制转换为 System.IntPtr,而应该在 IntPtr 类型上使用 Int32.Zero 静态公共字段。

    1.1.7 回调函数
    当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。
    在 C# 中没有函数指针,而是使用“委托”,在调用 Win32 函数时使用委托来代替函数指针。EnumDesktops() 函数就是这类函数的一个示例:
    BOOL EnumDesktops(
     HWINSTA hwinsta, // 窗口实例的句柄
     DESKTOPENUMPROC lpEnumFunc, // 回调函数
     LPARAM lParam// 用于回调函数的值
    );   
    HWINSTA 类型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC 所需的工作要多一些。下面是 MSDN 中的定义:
    BOOL CALLBACK EnumDesktopProc(
     LPTSTR lpszDesktop, // 桌面名称
     LPARAM lParam// 用户定义的值
    );   
    我们可以将它转换为以下委托:
    delegate bool EnumDesktopProc(
     [MarshalAs(UnmanagedType.LPTStr)]
     string desktopName,
     int lParam);   

    完成该定义后,我们可以为 EnumDesktops() 编写以下定义:
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    static extern bool EnumDesktops(
      IntPtr windowStation,
      EnumDesktopProc callback,
      int lParam);   
    这样该函数就可以正常运行了。

    在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给非托管函数。但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,因此它假定函数指针只需在调用该函数时有效即可。
    因此,如果委托是通过诸如 SetCallback() 这样的函数调用后,底层保存以便以后使用,则托管代码需要保证在使用委托时,委托引用还是有效的(没有把回收掉),此中情况下,一般要设为全局。

    1.1.8 属性的其他选项
    DLLImport 和 StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。另外返回值可以Return属性进行修饰。
    DLL Import 属性
    除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
        EntryPoint:在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。
        CharSet:如果 DLL 函数不以任何方式处理文本,则可以忽略 DllImportAttribute 的 CharSet 属性。然而,当 Char 或 String 数据是等式的一部分时,应该将 CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。
        SetLastError:设为true后,会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误。然后,在包装方法中,可以通过调用 System.Runtime.InteropServices.Marshal.GetLastWin32Error 方法来获取缓存的错误值。然后检查这些期望来自 API 函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在 System.ComponentModel.Win32Exception异常,并将 Marshal.GetLastWin32Error 返回的值传递给它。
        CallingConvention :通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。

    StructLayout 属性
        LayoutKind:结构在默认情况下按顺序布局,并且在多数情况下都适用。如果需要完全控制结构成员所放置的位置,可以使用 LayoutKind.Explicit,然后为每个结构成员添加 FieldOffset 属性。当您需要创建 union 时,通常需要这样做。
        CharSet:控制 ByValTStr 成员的默认字符类型。
        Pack:设置结构的压缩大小。它控制结构的排列方式。如果 C 结构采用了其他压缩方式,您可能需要设置此属性。
        Size:设置结构大小。不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。

    返回值
    返回值可修改返回的类型,一般都是bool类型需要处理。
    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetLastInputInfo(ref XLastInputInfo stInfo_)

    1.1.9 其他问题
    从不同位置加载
    您无法指定希望 DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一目的。
    DllImport 调用 LoadLibrary() 来完成它的工作。如果进程中已经加载了特定的 DLL,那么即使指定的加载路径不同,LoadLibrary() 也会成功。
    这意味着如果直接调用 LoadLibrary(),您就可以从任何位置加载 DLL,然后 DllImport LoadLibrary() 将使用该 DLL。
    由于这种行为,我们可以提前调用 LoadLibrary(),从而将您的调用指向其他 DLL。如果您在编写库,可以通过调用 GetModuleHandle() 来防止出现这种情况,以确保在首次调用 P/Invoke 之前没有加载该库。

    P/Invoke 疑难解答
    如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:
        long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。
        字符串类型设置不正确。

    对于非常复杂的结构,通过P/Invoke还是很难处理的,这是可考虑使用C++ Inerop来处理。

    1.2 C++ Interop
    使用P/Invoke可以封送大部分的操作,但是对于复杂的操作处理起来就非常麻烦,同时无法处理异常(无法获取原来异常的真实信息)。同时,一般来说Interop性能比较好。
    1.2.1 托管类型
    C++下的类、结构体、枚举等,不能在托管C++下直接使用,需要使用托管的类、结构体与枚举类型:ref class、ref struct与enum class。
    C++下的指针与引用也不能在托管C++下,需要分别替换为跟踪句柄(^)与跟踪引用(%)。同样,数组与字符串也需要替换为:String^与array<type>^。
    托管C++下的常量需要使用literal来修饰。
    String^ strVerb=nullptr;    //不能直接使用NULL
    array<String^>^ strNames={“Jill”, “Tes”};
    array<int>^ nWeight = {130, 168};
    int nValue = 10;
    int% nTrackValue=nValue;
    literal int NameMaxlen = 64;

    定义结构体时,需要使用StructLayout 与Marshal属性进行修改,以如下C++结构体为例:
    #pragma pack(push, MyPack_H, 4)
    struct CPPStruct
    {
    public:
        BOOL bValid;
    DWORD nCount;
    LARGE_INTEGER liNumber;
    WCHAR wzName[10];
    BYTE byBuff[100];
    CPPSubStruct stSub;
    }
    #pragma pack(pop, MyPack_H)
    对应的.Net定义
    [StructLayout(LayoutKind::Sequential, Pack = 4, CharSet = CharSet::Unicode)]
    ref struct MyStruct
    {
    public:
        MyStruct()
        {
            // 必须先使用gcnew为数组与结构体分配空间,字符串不需要
            byBuff = gcnew array<unsigned char>(100)
            stSub = gcnew MySubStruct();
        }
         [MarshalAs(UnmanagedType::Bool)]
        bool bValid;
        int nCount;
        long long llNumber;
        [MarshalAs(UnmanagedType::ByValTStr, SizeConst = 10)]
        String^ strName;
        [MarshalAs(UnmanagedType::ByValArray, SizeConst = 100)]
        array<unsigned char>^ byBuff;
        [MarshalAs(UnmanagedType::Struct)]
        MySubStruct    ^ stSub;
    };


    1.2.2 字符串与数组转换
    可通过<vcclr.h>中的pin_ptr把托管字符串与数组转换为非托管的字符串与数组:
    pin_ptr<const wchar_t> pKeySN = PtrToStringChars(strKeySN_)
    wchar_t     wzUser[CLen::CKeySNLen+1];
    GetNameBySN(pKeySN, wzUser);
    return gcnew String(wzUser);
    转换字符串时,需要用到PtrToStringChars来获取指针;如果是数组,直接使用第一个元素的地址即可(&Elments[0]),但 是如果如果数组指针为空需要先判断,设_xPtr为托管数组指针(如array<unsigned char>^ byBuffer):
     ( ((nullptr == _xPtr) || (0 == _xPtr->Length)) ? nullptr : &_xPtr[0] )
    数组操作:
    int GetInfo(IntPtr hHandle, [Out] array<unsigned char>^ %byInfo)
    {    
    int nLen = 100;
    array<unsigned char>^ byKey = gcnew array<unsigned char>(100);
    pin_ptr<unsigned char> pBuff = &byKey[0];
    int nCount = CPPGetInfo(hHandle.ToPointer(),pBuff, nLen);

    byInfo = gcnew array<unsigned char>(nLen);
    Array::Copy(byKey, byInfo, nLen);
    return nCount;
    }
    为了能回传byMySubStruct,必须使用跟踪引用(%)。
    托管内存使用gcnew来申请(不需要手动释放),然后使用pin_ptr转换为非托管的指针(当然,此处也完全可以使用pBuffer[100]来代替),通过Copy把非托管内容复制到托管数字钟;通过ToPointer()来获取非托管指针。

    1.2.3 回调函数
    声明
    [UnmanagedFunctionPointer(CallingConvention::StdCall)]
    delegate int CallbackFun(…);
    设定(设CPPCallbackFun为CallbackFun的C++对应声明)
    void SetCallback(CallbackFun^ delFun_)
    {
    IntPtr ptrCallback = Marshal::GetFunctionPointerForDelegate(delFun_);
    CPPSetCallback(static_cast<CPPCallbackFun>(ptrCallback.ToPointer()));
    }

    1.2.4 异常处理
    非托管的异常无法在托管程序中使用,必须先捕获非托管的异常,然后再转换为托管的异常。
    设CPPException为C++下的异常,DotNetException(需要继承标准异常,如ApplicationException、Exception等)为托管异常
    try
    {
    ……
    }
    catch(CPPException &ex)
    {
        throw gcnew DotNetException(gcnew String(ex.GetMsg()), ex.GetCode());
    }
    捕获C++异常时,需要使用引用,防止出现截断现象;新抛出的托管异常需要gcnew出来。

  • 相关阅读:
    菜鸟学IT之豆瓣爬取初体验
    菜鸟学IT之python网页爬取多页爬取
    菜鸟学IT之python网页爬取初体验
    菜鸟学IT之python词云初体验
    菜鸟学IT之python3关于列表,元组,字典,集合浅认识!
    整合ssm框架
    MyBatis
    服务出现服务名无效的原因及解决方法
    数据库(三)
    数据库(二)
  • 原文地址:https://www.cnblogs.com/mazhenyu/p/4995204.html
Copyright © 2011-2022 走看看