2017年8月17日 星期四

C#中調用Windows API的要點

C#中調用Windows API的要點
在.Net Framework SDK文檔中,關於調用Windows API的指示比較零散,並且其中稍全面一點的是針對Visual Basic .net講述的。本文將C#中調用API的要點彙集如下,希望給未在C#中使用過API的朋友一點幫助。另外如果安裝了Visual Studio .net的話,在C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Samples\Technologies\Interop\PlatformInvoke\WinAPIs\CS目錄下有大量的調用API的例子。
  一、調用格式
using System.Runtime.InteropServices; //引用此名稱空間,簡化後面的代碼
...
//使用DllImportAttribute特性來引入api函數,注意聲明的是空方法,即方法體為空。
[DllImport("user32.dll")]
public static extern ReturnType FunctionName(type arg1,type arg2,...);
//調用時與調用其他方法並無區別
  可以使用欄位進一步說明特性,用逗號隔開,如:
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )]
  DllImportAttribute特性的公共欄位如下:
  1、CallingConvention 指示向非託管實現傳遞方法參數時所用的 CallingConvention 值。
  CallingConvention.Cdecl : 調用方清理堆疊。它使您能夠調用具有 varargs 的函數。
  CallingConvention.StdCall : 被調用方清理堆疊。它是從託管代碼調用非託管函數的默認約定。
  2、CharSet 控制調用函數的名稱版本及指示如何向方法封送 String 參數。
  此欄位被設置為 CharSet 值之一。如果 CharSet 欄位設置為 Unicode,則所有字串參數在傳遞到非託管實現之前都轉換成 Unicode 字元。這還導致向 DLL EntryPoint 的名稱中追加字母“W”。如果此欄位設置為 Ansi,則字串將轉換成 ANSI 字串,同時向 DLL EntryPoint 的名稱中追加字母“A”。大多數 Win32 API 使用這種追加“W”或“A”的約定。如果 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”。
  3、EntryPoint 指示要調用的 DLL 入口點的名稱或序號。
  如果你的方法名不想與api函數同名的話,一定要指定此參數,例如:
[DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")]
public static extern int MsgBox(IntPtr hWnd,string txt,string caption, int type);
  4、ExactSpelling 指示是否應修改非託管 DLL 中的入口點的名稱,以與 CharSet 欄位中指定的 CharSet 值相對應。如果為 true,則當 DllImportAttribute.CharSet 欄位設置為 CharSet 的 Ansi 值時,向方法名稱中追加字母 A,當 DllImportAttribute.CharSet 欄位設置為 CharSet 的 Unicode 值時,向方法的名稱中追加字母 W。此欄位的預設值是 false。
  5、PreserveSig 指示託管方法簽名不應轉換成返回 HRESULT、並且可能有一個對應於返回值的附加 [out, retval] 參數的非託管簽名。
  6、SetLastError 指示被調用方在從屬性化方法返回之前將調用 Win32 API SetLastError。 true 指示調用方將調用 SetLastError,默認為 false。運行時封送拆收器將調用 GetLastError 並緩存返回的值,以防其被其他 API 調用重寫。用戶可通過調用 GetLastWin32Error 來檢索錯誤代碼。
  二、參數類型:
  1、數值型直接用對應的就可。(DWORD -> int , WORD -> Int16)
  2、API中字串指標類型 -> .net中string
  3、API中控制碼 (dWord) -> .net中IntPtr
  4、API中結構 -> .net中結構或者類。注意這種情況下,要先用StructLayout特性限定聲明結構或類
  公共語言運行庫利用StructLayoutAttribute控制類或結構的資料欄位在託管記憶體中的物理佈局,即類或結構需要按某種方式排列。如果要將類傳遞給需要指定佈局的非託管代碼,則顯式控制類佈局是重要的。它的構造函數中用LayoutKind值初始化 StructLayoutAttribute 類的新實例。 LayoutKind.Sequential 用於強制將成員按其出現的順序進行順序佈局。
  LayoutKind.Explicit 用於控制每個資料成員的精確位置。利用 Explicit, 每個成員必須使用 FieldOffsetAttribute 指示此欄位在類型中的位置。如:
[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)]
public class MySystemTime 
{
[FieldOffset(0)]public ushort wYear; 
[FieldOffset(2)]public ushort wMonth;
[FieldOffset(4)]public ushort wDayOfWeek; 
[FieldOffset(6)]public ushort wDay; 
[FieldOffset(8)]public ushort wHour; 
[FieldOffset(10)]public ushort wMinute; 
[FieldOffset(12)]public ushort wSecond; 
[FieldOffset(14)]public ushort wMilliseconds; 
}
  下面是針對API中OSVERSIONINFO結構,在.net中定義對應類或結構的例子:
/**********************************************
* API中定義原結構聲明
* OSVERSIONINFOA STRUCT
* dwOSVersionInfoSize DWORD ?
* dwMajorVersion DWORD ?
* dwMinorVersion DWORD ?
* dwBuildNumber DWORD ?
* dwPlatformId DWORD ?
* szCSDVersion BYTE 128 dup (?)
* OSVERSIONINFOA ENDS
*
* OSVERSIONINFO equ <OSVERSIONINFOA>
*********************************************/
//.net中聲明為類
[ StructLayout( LayoutKind.Sequential )] 
public class OSVersionInfo 

public int OSVersionInfoSize;
public int majorVersion; 
public int minorVersion;
public int buildNumber;
public int platformId;
[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] 
public String versionString;
}
//或者
//.net中聲明為結構
[ StructLayout( LayoutKind.Sequential )] 
public struct OSVersionInfo2 
{
public int OSVersionInfoSize;
public int majorVersion; 
public int minorVersion;
public int buildNumber;
public int platformId;
[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] 
public String versionString;
}
  此例中用到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 );
C# 託管資源和非託管資源

託管資源指的是.NET可以自動進行回收的資源,主要是指託管堆上分配的內存資源。託管資源的回收工作是不需要人工干預的,有.NET運行庫在合適調用垃圾回收器進行回收。

非託管資源指的是.NET不知道如何回收的資源,最常見的一類非託管資源是包裝作業系統資源的對象,例如文件,窗口,網絡連接,資料庫連接,畫刷,圖標等。這類資源,垃圾回收器在清理的時候會調用Object.Finalize方法。默認情況下,方法是空的,對於非託管對象,需要在此方法中編寫回收非託管資源的代碼,以便垃圾回收器正確回收資源。

在.NET中,Object.Finalize方法是無法重載的,編譯器是根據類的析構函數來自動生成Object.Finalize方法的,所以對於包含非託管資源的類,可以將釋放非託管資源的代碼放在析構函數。

注意,不能在析構函數中釋放託管資源,因為析構函數是有垃圾回收器調用的,可能在析構函數調用之前,類包含的託管資源已經被回收了,從而導致無法預知的結果。
本來如果按照上面做法,非託管資源也能夠由垃圾回收器進行回收,但是非託管資源一般是有限的,比較寶貴的,而垃圾回收器是由CRL自動調用的,這樣就無法保證及時的釋放掉非託管資源,因此定義了一個Dispose方法,讓使用者能夠手動的釋放非託管資源。Dispose方法釋放類的託管資源和非託管資源,使用者手動調用此方法後,垃圾回收器不會對此類實例再次進行回收。Dispose方法是由使用者調用的,在調用時,類的託管資源和非託管資源肯定都未被回收,所以可以同時回收兩種資源。

Microsoft為非託管資源的回收專門定義了一個接口:IDisposable,接口中只包含一個Dispose方法。任何包含非託管資源的類,都應該繼承此接口。
在一個包含非託管資源的類中,關於資源釋放的標準做法是:
(1) 繼承IDisposable接口;
(2) 實現Dispose方法,在其中釋放託管資源和非託管資源,並將對象本身從垃圾回收器中移除(垃圾回收器不在回收此資源);
(3) 實現類析構函數,在其中釋放非託管資源。
在使用時,顯示調用Dispose方法,可以及時的釋放資源,同時通過移除Finalize方法的執行,提高了性能;如果沒有顯示調用Dispose方法,垃圾回收器也可以通過析構函數來釋放非託管資源,垃圾回收器本身就具有回收託管資源的功能,從而保證資源的正常釋放,只不過由垃圾回收器回收會導致非託管資源的未及時釋放的浪費。

在.NET中應該儘可能的少用析構函數釋放資源。在沒有析構函數的對象在垃圾處理器一次處理中從內存刪除,但有析構函數的對象,需要兩次,第一次調用析構函數,第二次刪除對象。而且在析構函數中包含大量的釋放資原始碼,會降低垃圾回收器的工作效率,影響性能。所以對於包含非託管資源的對象,最好及時的調用Dispose方法來回收資源,而不是依賴垃圾回收器。

上面就是.NET中對包含非託管資源的類的資源釋放機制,只要按照上面要求的步驟編寫代碼,類就屬於資源安全的類。
下面用一個例子來總結一下.NET非託管資源回收機制:

Public class BaseResource:IDisposable

{

PrivateIntPtr handle; // 句柄,屬於非託管資源

PrivateComponet comp; // 組件,託管資源

Privateboo isDisposed = false; // 是否已釋放資源的標誌

PublicBaseResource

{

}

//實現接口方法

//由類的使用者,在外部顯示調用,釋放類資源

Publicvoid Dispose

{

Dispose(true);// 釋放託管和非託管資源

//將對象從垃圾回收器鍊表中移除,

// 從而在垃圾回收器工作時,只釋放託管資源,而不執行此對象的析構函數

GC.SuppressFinalize(this);

}

//由垃圾回收器調用,釋放非託管資源
~BaseResource
{

Dispose(false);// 釋放非託管資源

}

//參數為true表示釋放所有資源,只能由使用者調用

//參數為false表示釋放非託管資源,只能由垃圾回收器自動調用

//如果子類有自己的非託管資源,可以重載這個函數,添加自己的非託管資源的釋放

//但是要記住,重載此函數必須保證調用基類的版本,以保證基類的資源正常釋放

Protectedvirtual void Dispose(bool disposing)

{

If(!this.disposed)// 如果資源未釋放 這個判斷主要用了防止對象被多次釋放

{

If(disposing)

{

Comp.Dispose;// 釋放託管資源

}

closeHandle(handle);// 釋放非託管資源

handle= IntPtr.Zero;

}

this.disposed= true; // 標識此對象已釋放

}

}

析構函數只能由垃圾回收器調用。

Despose方法只能由類的使用者調用。

在C#中,凡是繼承了IDisposable接口的類,都可以使用using語句,從而在超出作用域後,讓系統自動調用Dispose方法。 一個資源安全的類,都實現了IDisposable接口和析構函數。提供手動釋放資源和系統自動釋放資源的雙保險。

原文網址:https://read01.com/xmA7D.html

MarshalAs的使用

參考:http://blog.sina.com.cn/s/blog_4e4ee8ed0100elou.html

作用:

MarshalAs屬性指示如何在託管代碼和非託管代碼之間封送數據。

使用方法:

[MarshalAs(UnmanagedType unmanagedType, 命名參數)]

實際上相當於構造一個MarshalAsAttribute類的對象

常用的UnmanagedType枚舉值:(詳細內容查MSDN)

BStr 長度前綴為雙字節的Unicode 字符串;

LPStr 單字節、空終止的ANSI 字符串。;

LPWStr 一個2 字節、空終止的Unicode 字符串;

ByValArray 用於在結構中出現的內聯定長字符數組,應始終使用MarshalAsAttribute的SizeConst字段來指示數組的大小。

注意:

在用Marshal.SizeOf(),即獲取對象的非託管大小時,獲得的是自己定義的大小;

但在實際處理的時候,是按照實際的大小來獲取的

示例:

定義一個固定大小的結構體,代碼如下:

結構的聲明:
結構的使用:
注意:
size=16+16+256
可見,獲取到的非託管大小為288
但是,查看myInfo對象可以看到其實際大小如下所示:
http://hi.csdn.net/attachment/201111/30/0_1322620745Hgu1.gif
問題:
這種實際大小和固定大小的不一致性,導致了在用Marshal類進行託管對象和非託管對象的轉換時,會有如下錯誤提示:“未能封送類型,因為嵌入數組實例的長度與佈局中聲明的長度不匹配。”
解決辦法還沒想到......
小結:
MarshalAs這個屬性很難用,很容易用錯,用好需要對C#、C++和COM數據的佈局方式有一定的了解才能做。所以做好使用一些工具來幫你,可以參照我下面的文章:
http://blog.csdn.net/Donjuan/archive/2009/02/05/3865026.aspx
如果你只是感興趣的話,那就忘了這個屬性吧,在.NET 4.0以後,微軟會盡量解決掉這個屬性。

沒有留言:

張貼留言