天天看點

C#調用Win32API的常見問題點

使用.net做系統整合是一個很有技術含量的活,從代碼到架構都有很多值得考慮的問題和一大堆陷阱。 最近看了一些關于這個方面的文章,現在整理一些筆記希望對大家有所幫助。 今天是第一篇,編碼級别的C#調用WinAPI(可以是Windows提供的,也可以是其他系統提供的,比如另一個應用程式)的常見問題點。在VS的安裝目錄下的frameworksdk\samples\technologies\interop\platforminvoke\winapis\cs 目錄中有大量的調用api的例子。

一、聲明函數時的Attrbutes 在使用API的函數時,我們需要先聲明這個函數,在以後的代碼中這個函數将可以像這個class的一個正常的static method一樣進行使用了。 1. 最基礎的聲明 

[dllimport("SampleWinApi.dll")]

public static extern int SampleApi1();

2. 進一步說明特性,用逗号隔開,如:

[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )] public static extern int SampleApiFriendName();

dllimport的attribute有如下幾個,最常用的是charset,setlasterror和entrypoint  :

a callingconvention 訓示向非托管實作傳遞方法參數時所用的 callingconvention 值。

callingconvention.cdecl : 調用方清理堆棧。它使您能夠調用具有 varargs 的函數。

callingconvention.stdcall : 被調用方清理堆棧。它是從托管代碼調用非托管函數的預設約定。

b charset 控制調用函數的名稱版本及訓示如何向方法封送 string 參數。

此字段被設定為 charset 值之一。如果 charset 字段設定為 unicode,則所有字元串參數在傳遞到非托管實作之前都轉換成 unicode 字元;如果此字段設定為 ansi,則字元串将轉換成 ansi 字元串;如果 charset 設定為 auto,則這種轉換就是與平台有關的(在 windows nt 上為 unicode,在 windows 98 上為 ansi)。charset 的預設值為 ansi。charset 字段也用于确定将從指定的 dll 導入哪個版本的函數。charset.ansi 和 charset.unicode 的名稱比對規則大不相同。對于 ansi 來說,如果将 entrypoint 設定為“mymethod”且它存在的話,則傳回“mymethod”。如果 dll 中沒有“mymethod”,但存在“mymethoda”,則傳回“mymethoda”。對于 unicode 來說則正好相反。如果将 entrypoint 設定為“mymethod”且它存在的話,則傳回“mymethodw”。如果 dll 中不存在“mymethodw”,但存在“mymethod”,則傳回“mymethod”。如果使用的是 auto,則比對規則與平台有關(在 windows nt 上為 unicode,在 windows 98 上為 ansi)。如果 exactspelling 設定為 true,則隻有當 dll 中存在“mymethod”時才傳回“mymethod”。

c entrypoint 訓示要調用的 dll 入口點的名稱或序号。

如果你的方法名不想與api函數同名的話,一定要指定此參數,例如:

[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )] public static extern int SampleApiFriendName();

d exactspelling 訓示是否應修改非托管 dll 中的入口點的名稱,以與 charset 字段中指定的 charset 值相對應。如果為 true,則當 dllimportattribute.charset 字段設定為 charset 的 ansi 值時,向方法名稱中追加字母 a,當 dllimportattribute.charset 字段設定為 charset 的 unicode 值時,向方法的名稱中追加字母 w。此字段的預設值是 false。

e preservesig 訓示托管方法簽名不應轉換成傳回 hresult、并且可能有一個對應于傳回值的附加 [out, retval] 參數的非托管簽名。

f setlasterror 訓示被調用方在從屬性化方法傳回之前将調用 win32 api setlasterror。 true 訓示調用方将調用 setlasterror,預設為 false。運作時封送拆收器将調用 getlasterror 并緩存傳回的值,以防其被其他 api 調用重寫。使用者可通過調用 getlastwin32error 來檢索錯誤代碼。

二、參數類型: WinApi使用的資料類型和.net的資料類型相比是完全不同的體系,是以我們需要在聲明API函數時将API的資料類型轉換為.net的類型。以下是常見的資料類型對應表 http://msdn.microsoft.com/zh-cn/library/aa720411(en-us,VS.71).aspx 

值得特殊說明的有以下兩個,StringBuilder和Struct 1、stringbuilder,不要使用ref或out,否則會被當作CHAR**

2、api中結構 -> .net中結構或者類。注意這種情況下,要先用structlayout特性限定聲明結構或類

公共語言運作庫利用structlayoutattribute控制類或結構的資料字段在托管記憶體中的實體布局,即類或結構需要按某種方式排列。如果要将類傳遞給需要指定布局的非托管代碼,則顯式控制類布局是重要的。它的構造函數中用layoutkind值初始化 structlayoutattribute 類的新執行個體。 layoutkind.sequential 用于強制将成員按其出現的順序進行順序布局。layoutkind.explicit 用于控制每個資料成員的精确位置。利用 explicit, 每個成員必須使用 fieldoffsetattribute 訓示此字段在類型中的位置。這樣做往往需要用到mashalas特性,它用于描述字段、方法或參數的封送處理格式。用它作為參數字首并指定目标需要的資料類型。例如,以下代碼将兩個參數作為資料類型長指針封送給 windows api 函數的字元串 (lpstr):

[marshalas(unmanagedtype.lpstr)]

string existingfile;

[marshalas(unmanagedtype.lpstr)]

string newfile;

注意結構作為參數時候,一般前面要加上ref修飾符,否則會出現錯誤:對象的引用沒有指定對象的執行個體。

[ dllimport( "kernel32", entrypoint="getversionex" )]

public static extern bool getversionex2( ref osversioninfo2 osvi );

三、防止被不恰當的回收

如果在調用平台 invoke 後的任何位置都未引用托管對象,則垃圾回收器可能将完成該托管對象。這将釋放資源并使句柄無效,進而導緻平台invoke 調用失敗。用 handleref 包裝句柄可保證在平台 invoke 調用完成前,不對托管對象進行垃圾回收。

例如下面:

filestream fs = new filestream( "a.txt", filemode.open );

stringbuilder buffer = new stringbuilder( 5 );

int read = 0;

readfile(fs.handle, buffer, 5, out read, 0 ); //調用win api中的readfile函數

由于fs是托管對象,是以有可能在平台調用還未完成時候被垃圾資源回收筒回收。将檔案流的句柄用handleref包裝後,就能避免被垃圾站回收:

[ dllimport( "kernel32.dll" )]

public static extern bool readfile(

handleref hndref,

stringbuilder buffer,

int numberofbytestoread,

out int numberofbytesread,

ref overlapped flag );

......

......

filestream fs = new filestream( "handleref.txt", filemode.open );

handleref hr = new handleref( fs, fs.handle );

stringbuilder buffer = new stringbuilder( 5 );

int read = 0;

// platform invoke will hold reference to handleref until call ends

readfile( hr, buffer, 5, out read, 0 );

繼續閱讀