平臺呼叫P/Invoke進階 -- 初階:認識平臺呼叫P/Invoke
.NET互動操作服務
前言
.NET是微軟最新推出的程式設計平臺,它通過公共語言執行庫將基於.NET Framework的託管程式碼(Managed code)承載執行,以簡化Internet 環境中的應用程式開發和部署。
Microsoft .NET最核心的特徵是互動性,包括多種程式語言的互動、與非託管程式碼的互動。其中與非託管程式碼又包括與現有原生程式碼(Native code)的互動、與COM的互相互動操作。與非託管程式碼互動使得.NET能夠與作業系統服務,應用開發商現有程式庫、Microsoft COM元件技術等進行無縫的整合,從而促進.NET程式設計平臺的推廣。
本人在此把全文分為《平臺呼叫
-
平臺呼叫P/Invoke進階
平臺呼叫P/Invoke用於從.NET託管程式碼中呼叫在動態連結庫(DLL)中的函式。這裡的動態連結庫是指原生的非託管動態連結庫,如作業系統的DLL、由C/C++生成的DLL等,而並非.NET的託管程式庫。在公共語言執行時中它的主要作用是:
1.提供一種更便利的方式與開發商現存程式碼互動。
2.呼叫系統API來實現公共語言執行時庫目前沒有提供的功能。
可見,利用平臺呼叫這種服務,我們可以再利用本公司現有的程式庫,以及直接使用作業系統API
初階:認識平臺呼叫P/Invoke
一、從Hello world例子開始
我們從”Hello world”這個簡單例子開始我們的平臺呼叫之旅。這裡我們從自己編寫的一個動態連結庫mylib.dll中呼叫Dll函式Print()輸出”Hello world!”。這個函式的宣告如下:
void WINAPI Print (
const char* str // 輸出的字串
);
我們可以用以下一個用C#寫的方法來將這個函式引進以供在託管程式碼中使用。這個方法就是.NET中的平臺呼叫方法。
[DllImport("mylib.dll")]
public static extern void Print(String text);
這樣聲明後,我們就能如用其它任何方法一樣的使用這個靜態方法。所有如載入動態連結庫、查詢函式地址,資料封送排程等工作均由.NET在執行時自動完成。我們所要做的僅僅就是正確地宣告一個平臺呼叫函式。
public static void Main(String[] args)
{
Print ("Hello world!");
}
我們編譯後並執行,就可得到如下結果:
二、解析平臺呼叫P/Invoke方法
我們從剛剛這個例子可以看到,平臺呼叫在.NET中呼叫非託管函式程式碼是如此的簡單。它只需要我們正確地宣告一個平臺呼叫方法就完成了整個呼叫包裝過程。從.NET內部機制來看,平臺呼叫方法其實只是一個讓公共語言執行時去尋找真正的非託管函式的元資料,因而它不需要方法體定義。
現在我們來仔細分析它的結構。它可分為兩個部分。方法頭宣告和DllImport屬性宣告。我們再來看Helloworld這個例子中的平臺呼叫函式。
[DllImport("mylib.dll")]
public static extern void Print(String text);
方法頭
在C#中,平臺呼叫方法必須帶有修飾符static和extern, static是因為非託管函式並沒有物件例項,而extern指示這是一個外部實現方法,而一個外部方法是沒有方法體,它通常與DllImport屬性一起使用,來說明一個方法為平臺呼叫方法。
其次,大家注意到,這個方法的原型結構對應於動態連結庫裡的函式宣告,它返回型別,引數型別是一一相對應的,不同的是,這裡的字串用的是通用型別系統的String型別。從String型別到C格式的字串轉換是由.NET在執行時自動完成的。(這裡有一個術語叫排程(Marshal),就是專門來描述不同資料格式模型的封送、轉換的,在本文的後面,將有詳細講解)下面這個表格列出了平臺呼叫中C#託管型別、通用型別系統型別與C式樣的非託管資料型別的對應關係。
表1.非託管型別與託管型別的對應關係
非託管型別 |
C#型別 |
通用型別系統型別 |
說明 |
unsigned char |
byte |
System.Byte |
無符號 8 位整數 |
short |
short |
System.Int16 |
有符號 16 位整數 |
unsigned short |
ushort |
System.UInt16 |
無符號 16 位整數 |
int, long |
int |
System.Int32 |
有符號 32 位整數 |
unsigned int, unsigned long |
uint |
System.UInt32 |
無符號 32 位整數 |
__int64 或 long long |
long |
System.Int64 |
有符號 64 位整數 |
unsigned __int64 或 unsigned long long |
Ulong |
System.UInt64 |
無符號 64 位整數 |
char |
char |
System.Char |
8 位 ANSI 字元 |
wchar_t |
Char |
System.Char |
16 位 Unicode 字元 |
char*, const char* |
string |
System.String |
ANSI字串 |
wchar_t*, const wchar_t* |
String |
System.String |
Unicode字串 |
float |
float |
System.Single |
32位浮點數 |
double |
double |
System.Double |
64位浮點數 |
void*,const void*,其它指標 |
System.IntPtr |
System.IntPtr |
指標型別 |
DllImport屬性
我們曾說過,平臺呼叫方法只是一個讓公共語言執行時去尋找真正的非託管程式碼的元資料,這些元資料資訊就是來自DllImport這個自定義屬性。由此可見,DllImport屬性在平臺呼叫中起著非常重要的作用。我們來看它的C#版本定義(原始碼來自Mono):
namespace System.Runtime.InteropServices {
[AttributeUsage (AttributeTargets.Method)]
public sealed class DllImportAttribute: Attribute {
public CallingConvention CallingConvention;
public CharSet CharSet;
public string EntryPoint;
public bool ExactSpelling;
public bool PreserveSig;
public bool SetLastError;
private string Dll;
public string Value {
get {return Dll;}
}
public DllImportAttribute (string dllName) {
Dll = dllName;
}
}
}
由上面的程式碼我們可以得出,一個DllImport屬性的最小化形式就是我們在Helloworld中使用形式,它必須指明我們的非託管函式所在有動態連結庫名。你所指定的動態連結庫可以為絕對路徑,如DllImport("D://pinvoke sample//mylib.dll"),也可以為相對路徑,這時公共語言執行時沿以下路徑順序搜尋這個庫:
-
當前路徑。
-
Windows系統目錄。
-
PATH環境變數所指定的目錄。
如果所指定的動態連結庫沒有在可搜尋路徑範圍內,公共語言執行時就會引發DllNotFoundException.
而其它六個屬性成員都是可選的,在我們未指明它們的時候,公共語言執行時為我們加上一些預設的值。接下來我們一一來認識其它屬性成員。
CallingConvention 指定非託管函式的呼叫規則,它指示公共語言執行時如何進行引數堆疊的清理。比較常用的CallingConvention.StdCall和CallingConvention.Cdecl,在C/C++中它分別對應於__stdcall和__cdecl。該屬性成員的預設值是CallingConvention.WinAPI,它實際上就是CallingConvention.StdCall,它表示由被呼叫方清理堆疊。通常我們用預設值都能很好的工作,即使我們的非託管函式是__cdecl,但是這種情況並不總是有效,如果呼叫規則不一致,可能導致堆疊不能正確清理而影響應用程式的穩定性,甚至可能直接導致應用程式無法執行。所以,在編寫平臺呼叫方法時,我們一定要檢查是否與非託管函式的呼叫規則保持一致。
CharSet 指定封送的字串的字符集型別。我們熟悉最常見的字符集是Unicode和ANSI,而.NET的字串所用的字符集是Unicode,從表1中我們看到無論非託管函式中所用的字符集是ANSI還是Unicode,在平臺呼叫方法中我們都用String型別。顯然我們只通過指定CharSet屬性來區分我們的非託管函式所用的字符集。它的預設值是CharSet.Ansi,也就是ANSI字符集。我們還可以使用CharSet.Auto值,它表示根據作業系統自動識別所用的字符集,即公共語言執行時在WinNT, Win2000等系列作業系統用CharSet.Unicode字符集,在Win98,WinMe等系列作業系統就用CharSet.ANSI.
EntryPoint 指定非託管函式名稱。如果我們沒有指定在DllImport屬性中指定EntryPoint值,公共語言執行時就會以我們的平臺呼叫方法名為名稱,如Hello world例子中的Print.如果公共語言執行時在動態連結庫中沒有找到非託管函式,就會引發EntryPointNotFoundException。但是公共語言執行時對名字進行解析時做一些精緻的動作,我們在ExactSpelling中可以看到詳細的分析。
ExactSpelling 指定是否通過修改我們所指定的非託管函式名來尋找非託管函式。我們知道,在Windows系統API中,如果API的引數或返回型別中有字串型別,Windows就會提供兩個API,一個用於Uncode字符集,一個用於ANSI字符集。如MessageBox API,它實際上只是MessageBoxA或MessageBoxW的巨集。而在動態連結庫user32.dl中也只提供了MessageBoxA和MessageBoxW兩個函式。公共語言執行時為了我們平臺呼叫的方便,就提供了一個自動新增尾綴的操作,它根據我們所指定的CharSet屬性值來新增。當ExactSpelling為假值false時,如果我們在平臺呼叫方法中用的字符集為CharSet.ANSI, 就會在我們所指定的非託管函式名尾部新增一個A字元;如果我們在平臺呼叫方法中用的字符集為CharSet.Unicode, 就會在我們所指定的非託管函式名尾部新增一個W字元。如果在添加了尾綴後從動態連結庫中沒有找到非託管函式,就會以非新增尾綴的名稱再找一次,如果仍然沒有找到,就引發EntryPointNotFoundException. 當ExactSpelling為真值true時,公共語言執行時不會做任何操作,它直接用我們指定的非託管函式名稱。如果沒有找到,就會引發EntryPointNotFoundException.
ExactSpelling的預設值因編譯器不同而不同,微軟的C#編譯器的預設值為false.
PreserveSig 指定是否保留託管函式的簽名不作變化。這個屬性值是針對COM方法來的。在COM規範中,所有的方法都必須返回HRESULT型別值,以指示這個呼叫是否成功,而一個方法欲返回的其它值則在引數中以[out,retval]標出。公共語言執行時為我們又提供一種便利,可以自動轉換函式返回值,在我們的平臺呼叫方法中直接返回[out,retval]引數值而不是HRESULT值。如果PreserveSig為假值false,則作出這種轉換,但是必須以這個非託管函式是返回HRESULT值為前提。它的預設值為true
SetLastError 指定是否保留平臺呼叫的錯誤值,這裡的錯誤值是指Windows API GetLastError所返回的值。如是保留,則我們可以通過呼叫Marshal.GetLastWin32Error獲得此值。該屬性值的預設值是假值false.
我們來看一個對DllImport屬性的綜合運用,它用MSOLE API LoabTypeLib裝載一個COM型別庫。這裡用到了兩到了兩個系統API的原型為:
HRESULT CoInitialize(
LPVOIDpvReserved//保留
);
我們用以下平臺呼叫方法來引進它,因為它沒有[out,ret]引數,我們就不能變化它的函式簽名:
[DllImport("ole32.dll")]
public static extern int CoInitialize (IntPtr v);
HRESULT LoadTypeLib(
const OLECHAR FAR* szFile, //型別庫檔名
ITypeLib FAR* FAR* pptlib //裝載後得到的ITypeLib介面指標
);
我們用以下平臺呼叫方法來引進它,注意OLECHAR的為Unicode字符集
[DllImport("oleaut32.dll", PreserveSig = false, ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadTypeLib (string szFile);
宣告之後,在我們的.NET應用程式中,我們就可以直接使用這些API:
int err = CoInitialize (IntPtr.Zero);
if (err == 0) {
pTypeLib = Test.LoadTypeLib (argv[0]);
if (pTypeLib == IntPtr.Zero) {
Console.WriteLine ("Failed call LoadTypeLib with " + err);
err = Marshal.GetLastWin32Error();
Marshal.ThrowExceptionForHR (err);
} else {
Console.WriteLine ("Success");
}
} else {
Console.WriteLine ("Failed CoInitialize with " + err);
}
現在,我們已經一一瞭解了DllImport各成員的作用,並對公共語言執行時的一些操作進行了分析。下面我們來進一步來認識平臺呼叫的資料封送排程。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=725253