平台叫用服務 (PInvoke) 允許Managed 程式碼調用在 DLL 中實現的非託管函數。
本教程說明使用什麼方法才能從 C# 調用非託管 DLL 函數。該教程所討論的屬性允許您調用這些函數並使資料類型得到正確封送。
教程
C# 代碼有以下兩種可以直接調用Unmanaged 程式碼的方法:
直接調用從 DLL 匯出的函數。
調用 COM 對象上的介面方法(有關更多資訊,請參見 COM Interop 第一部分:C# 用戶端教程)。
對於這兩種技術,都必須向 C# 編譯器提供非託管函數的聲明,並且還可能需要向 C# 編譯器提供如何封送與Unmanaged 程式碼之間傳遞的參數和傳回值的說明。
該教程由下列主題組成:
直接從 C# 調用 DLL 匯出
預設封送處理和為非託管方法的參數指定自訂封送處理
為使用者定義的結構指定自訂封送處理
註冊回調方法
該教程包括下列樣本:
樣本 1 使用 DllImport
樣本 2 重寫預設封送處理
樣本 3 指定自訂封送處理
直接從 C# 調用 DLL 匯出
若要聲明一個方法使其具有來自 DLL 匯出的實現,請執行下列操作:
使用 C# 關鍵字 static 和 extern 聲明方法。
將 DllImport 屬性附加到該方法。DllImport 屬性允許您指定包含該方法的 DLL 的名稱。通常的做法是用與匯出的方法相同的名稱命名 C# 方法,但也可以對 C# 方法使用不同的名稱。
還可以為方法的參數和傳回值指定自訂封送處理資訊,這將重寫 .NET Framework 的預設封送處理。
樣本 1
本樣本顯示如何使用 DllImport 屬性通過調用 msvcrt.dll 中的 puts 輸出訊息。
// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
public static void Main()
{
puts("Test");
_flushall();
}
}
輸出
Test
代碼討論
前面的樣本顯示了聲明在非託管 DLL 中實現的 C# 方法的最低要求。PlatformInvokeTest.puts 方法用 static 和 extern 修飾符聲明並且具有 DllImport 屬性,該屬性使用預設名稱 puts 通知編譯器此實現來自 msvcrt.dll。若要對 C# 方法使用不同的名稱(如 putstring),則必須在 DllImport 屬性中使用 EntryPoint 選項,如下所示:
[DllImport("msvcrt.dll", EntryPoint="puts")]
有關 DllImport 屬性的文法的更多資訊,請參見 DllImportAttribute 類。
預設封送處理和為非託管方法的參數指定自訂封送處理
當從 C# 代碼中調用非託管函數時,公用語言運行庫必須封送參數和傳回值。
對於每個 .NET Framework 類型均有一個預設非託管類型,公用語言運行庫將使用此非託管類型在託管到非託管的函數調用中封送資料。例如,C# 字串值的預設封送處理是封送為 LPTSTR(指向 TCHAR 字元緩衝區的指標)類型。可以在非託管函數的 C# 聲明中使用 MarshalAs 屬性重寫預設封送處理。
樣本 2
本樣本使用 DllImport 屬性輸出一個字串。它還顯示如何通過使用 MarshalAs 屬性重寫函數參數的預設封送處理。
// Marshal.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(
[MarshalAs(UnmanagedType.LPStr)]
string m);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
public static void Main()
{
puts("Hello World!");
_flushall();
}
}
輸出
運行此樣本時,字串
Hello World!
將顯示在控制台上。
代碼討論
在前面的樣本中,puts 函數的參數的預設封送處理已從預設值 LPTSTR 重寫為 LPSTR。
MarshalAs 屬性可以放置在方法參數、方法傳回值以及結構和類的欄位上。若要設定方法傳回值的封送處理,請將 MarshalAs 屬性與返回屬性位置重寫一起放置在方法上的屬性塊中。例如,若要顯式設定 puts 方法傳回值的封送處理:
...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
...
有關 MarshalAs 屬性的文法的更多資訊,請參見 MarshalAsAttribute 類。
注意 In 和 Out 屬性可用於批註非託管方法的參數。它們與 MIDL 源檔案中的 in 和 out 修飾符的工作方式類似。請注意,Out 屬性與 C# 參數修飾符 out 不同。有關 In 和 Out 屬性的更多資訊,請參見 InAttribute 類和 OutAttribute 類。
為使用者定義的結構指定自訂封送處理
可以為傳遞到非託管函數或從非託管函數返回的結構和類的欄位指定自訂封送處理屬性。通過向結構或類的欄位中添加 MarshalAs 屬性可以做到這一點。還必須使用 StructLayout 屬性設定結構的布局,還可以控制字元串成員的預設封送處理,並設定預設封裝大小。
樣本 3
本樣本說明如何為結構指定自訂封送處理屬性。
請考慮下面的 C 結構:
typedef struct tagLOGFONT
{
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
在 C# 中,可以使用 StructLayout 和 MarshalAs 屬性描述前面的結構,如下所示:
// logfont.cs
// compile with: /target:module
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public class LOGFONT
{
public const int LF_FACESIZE = 32;
public int lfHeight;
public int lfWidth;
public int lfEscapement;
public int lfOrientation;
public int lfWeight;
public byte lfItalic;
public byte lfUnderline;
public byte lfStrikeOut;
public byte lfCharSet;
public byte lfOutPrecision;
public byte lfClipPrecision;
public byte lfQuality;
public byte lfPitchAndFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=LF_FACESIZE)]
public string lfFaceName;
}
有關 StructLayout 屬性的文法的更多資訊,請參見 StructLayoutAttribute 類。
然後即可將該結構用在 C# 代碼中,如下所示:
// pinvoke.cs
// compile with: /addmodule:logfont.netmodule
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("gdi32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr CreateFontIndirect(
[In, MarshalAs(UnmanagedType.LPStruct)]
LOGFONT lplf // characteristics
);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(
IntPtr handle
);
public static void Main()
{
LOGFONT lf = new LOGFONT();
lf.lfHeight = 9;
lf.lfFaceName = "Arial";
IntPtr handle = CreateFontIndirect(lf);
if (IntPtr.Zero == handle)
{
Console.WriteLine("Can't creates a logical font.");
}
else
{
if (IntPtr.Size == 4)
Console.WriteLine("{0:X}", handle.ToInt32());
else
Console.WriteLine("{0:X}", handle.ToInt64());
// Delete the logical font created.
if (!DeleteObject(handle))
Console.WriteLine("Can't delete the logical font");
}
}
}
運行樣本
C30A0AE5
代碼討論
在前面的樣本中,CreateFontIndirect 方法使用了一個 LOGFONT 類型的參數。MarshalAs 和 In 屬性用於限定此參數。程式將由此方法返回的數值顯示為十六進位大寫字串。
註冊回調方法
若要註冊調用非託管函數的託管回調,請用相同的參數列表聲明一個委託並通過 PInvoke 傳遞它的一個執行個體。在非託管端,它將顯示為一個函數指標。有關 PInvoke 和回調的更多資訊,請參見平台叫用詳解。
例如,考慮以下非託管函數 MyFunction,此函數要求 callback 作為其參數之一:
typedef void (__stdcall *PFN_MYCALLBACK)();
int __stdcall MyFunction(PFN_ MYCALLBACK callback);
若要從Managed 程式碼調用 MyFunction,請聲明該委託,將 DllImport 附加到函式宣告,並根據需要封送任何參數或傳回值:
public delegate void MyCallback();
[DllImport("MYDLL.DLL")]
public static extern void MyFunction(MyCallback callback);
同時,請確保委託執行個體的生存期覆蓋Unmanaged 程式碼的生存期;否則,委託在經過記憶體回收後將不再可用。