.Net調用Unmanaged 程式碼(P/Invoke與C++InterOP)

來源:互聯網
上載者:User

1 .Net互操作
.Net不能直接操作Unmanaged 程式碼,這時就需要互操作了。
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 或單位元組,如果Unmanaged 程式碼為寬字元,則需要明確的把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 函數將指標作為它們的一個或多個參數。指標增加了封送資料的複雜性,因為它們增加了一個間接層。如果沒有指標,您可以通過值線上程堆棧中傳遞資料。有了指標,則可以通過引用傳遞資料,方法是將該資料的記憶體位址推入線程堆棧中。然後,函數通過記憶體位址間接訪問資料。使用Managed 程式碼表示此附加間接層的方式有多種。

封送不透明 (Opaque) 指標:一種特殊情況
有時在 Windows API 中,方法傳遞或返回的指標是不透明的,這意味著該指標值從技術角度講是一個指標,但代碼卻不直接使用它。相反,代碼將該指標返回給 Windows 以便隨後進行重用。一個非常常見的例子就是控制代碼的概念。
當一個不透明指標返回給您的應用程式(或者您的應用程式期望得到一個不透明指標)時,您應該將參數或傳回值封送為 CLR 中的一種特殊類型 — System.IntPtr。當您使用 IntPtr 類型時,通常不使用 out 或 ref 參數,因為 IntPtr 意為直接持有指標。不過,如果您將一個指標封送為一個指標,則對 IntPtr 使用 by-ref 參數是合適的。
在 CLR 類型系統中,System.IntPtr 類型有一個特殊的屬性。不像系統中的其他基底類型,IntPtr 並沒有固定的大小。相反,它在運行時的大小是依底層作業系統的正常指標大小而定的。這意味著在 32 位的 Windows 中,IntPtr 變數的寬度是 32 位的,而在 64 位元的 Windows 中,Just-In-Time 編譯器編譯的代碼會將 IntPtr 值看作 64 位元的值。當在Managed 程式碼和Unmanaged 程式碼之間封送不透明指標時,這種自動調節大小的特點十分有用。
您可以在Managed 程式碼中將 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() 這樣的函數調用後,底層儲存以便以後使用,則Managed 程式碼需要保證在使用委託時,委託引用還是有效(沒有把回收掉),此中情況下,一般要設為全域。

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出來。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.