1. 程式人生 > >C#呼叫非託管程式碼(轉)

C#呼叫非託管程式碼(轉)

在.net 程式設計環境中,系統的資源分為託管資源和非託管資源。

  對於託管的資源的回收工作,是不需要人工干預回收的,而且你也無法干預他們的回收,所能夠做的

只是瞭解.net CLR如何做這些操作。也就是說對於您的應用程式建立的大多數物件,可以依靠 .NET

Framework 的垃圾回收器隱式地執行所有必要的記憶體管理任務。託管程式碼就是基於.net元資料格式的程式碼,
運行於
.net平臺之上,所有的與作業系統的交換有.net來完成,就像是把這些功能委託給.net,所以稱之為託管程式碼。

舉個例子l  

Vc.net還可以使用mfc,atl來編寫程式,他們基於MFC或者ATL,而不是.NET,所有是非託管程式碼,如果基於

.net比如C#VB.net則是託管程式碼 

非託管程式碼是指.NET解釋不了的  

簡單的說,託管程式碼的話,.net可以自動釋放資料,非託管程式碼需要手動釋放資料.   
對於非託管資源,您在應用程式中使用完這些非託管資源之後,必須顯示的釋放他們,例如

System.IO.StreamReader的一個檔案物件,必須顯示的呼叫物件的Close()方法關閉它,否則會佔用系統

的記憶體和資源,而且可能會出現意想不到的錯誤。

  清楚什麼是託管資源,什麼是非託管資源

  最常見的一類非託管資源就是包裝作業系統資源的物件,例如檔案,視窗或網路連線,對於這類資源

雖然垃圾回收器可以跟蹤封裝非託管資源的物件的生存期,但它不瞭解具體如何清理這些資源。還好.net

Framework提供了Finalize()方法,它允許在垃圾回收器回收該類資源時,適當的清理非託管資源。如果

在MSDN Library 中搜索Finalize將會發現很多類似的主題,這裡列舉幾種常見的非託管資源:

ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Fon

t,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Time

r,Tooltip 等等資源。可能在使用的時候很多都沒有注意到!

關於託管資源,就不用說了撒,像簡單的int,string,float,DateTime等等,.net中超過80%的資源都是託

管資源。

非託管資源如何釋放,.NET Framework 提供 Object.Finalize 方法,它允許物件在垃圾回收器回收該對

象使用的記憶體時適當清理其非託管資源。預設情況下,Finalize 方法不執行任何操作。預設情況下,

Finalize 方法不執行任何操作。如果您要讓垃圾回收器在回收物件的記憶體之前對物件執行清理操作,您

必須在類中重寫 Finalize 方法。然而大家都可以發現在實際的程式設計中根本無法override方法Finalize

(),在C#中,可以通過解構函式自動生成 Finalize 方法和對基類的 Finalize 方法的呼叫。

例如:
~MyClass()
{
  // Perform some cleanup operations here.
}
  該程式碼隱式翻譯為下面的程式碼。
protected override void Finalize()
{
  try
  {
    // Perform some cleanup operations here.
  }
  finally
  {
    base.Finalize();
  }
}

但是,在程式設計中,並不建議進行override方法Finalize(),因為,實現 Finalize 方法或解構函式對效能

可能會有負面影響。一個簡單的理由如下:用 Finalize 方法回收物件使用的記憶體需要至少兩次垃圾回收

,當垃圾回收器回收時,它只回收沒有終結器(Finalize方法)的不可訪問的記憶體,這時他不能回收具有終

結器(Finalize方法)的不可以訪問的記憶體。它改為將這些物件的項從終止佇列中移除並將他們放置在標記

為“準備終止”的物件列表中,該列表中的項指向託管堆中準備被呼叫其終止程式碼的物件,下次垃圾回收

器進行回收時,就回收並釋放了這些記憶體。
C#如何直接呼叫非託管程式碼,通常有2種方法:

1. 直接呼叫從 DLL 匯出的函式。 2. 呼叫 COM 物件上的介面方法 我主要討論從dll中匯出函式,基本步驟如下: 1.使用 C# 關鍵字 static extern 宣告方法。 2 DllImport 屬性附加到該方法。DllImport 屬性允許您指定包含該方法的 DLL 的名稱 3.如果需要,為方法的引數和返回值指定自定義封送處理資訊,這將重寫 .NET Framework 預設封送處理。 好,我們開始 1.首先我們查詢MSDN找到GetShortPathName的定義 The GetShortPathName function retrieves the short path form of the specified path. DWORD GetShortPathName(  LPCTSTR lpszLongPath,  LPTSTR lpszShortPath,  DWORD cchBuffer );
Win32 Types Specification CLR Type
char, INT8, SBYTE, CHAR† 8-bit signed integer System.SByte
short, short int, INT16, SHORT 16-bit signed integer System.Int16
int, long, long int, INT32, LONG32, BOOL†, INT 32-bit signed integer System.Int32
__int64, INT64, LONGLONG 64-bit signed integer System.Int64
unsigned char, UINT8, UCHAR†, BYTE 8-bit unsigned integer System.Byte
unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR†, __wchar_t 16-bit unsigned integer System.UInt16
unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT 32-bit unsigned integer System.UInt32
unsigned __int64, UINT64, DWORDLONG, ULONGLONG 64-bit unsigned integer System.UInt64
float, FLOAT Single-precision floating point System.Single
double, long double, DOUBLE Double-precision floating point System.Double
†In Win32 this type is an integer with a specially assigned meaning; in contrast, the CLR provides a specific type devoted to this meaning.
3.呼叫GetShortPathName這個API,簡單的寫法如下(編譯通過的話), using System; using System.Runtime.InteropServices;     public class MSSQL_ServerHandler     {         [DllImport("kernel32.dll")]         public static extern int GetShortPathName         (             string path,             StringBuilder shortPath,             int shortPathLength )      } 而我們之前的例子: using System; using System.Runtime.InteropServices;     public class MSSQL_ServerHandler     {         [DllImport("kernel32.dll", CharSet = CharSet.Auto)]         public static extern int GetShortPathName         (             [MarshalAs(UnmanagedType.LPTStr)] string path,             [MarshalAs(UnmanagedType.LPTStr)] StringBuilder shortPath,             int shortPathLength )      } 對比可知,其中DllImport staticextern基本上是必須有的,其他CharSetMarshalAs)是可選項,在這裡即使沒有,程式也是可以呼叫此API了。 說明: 1MSSQL_ServerHandler. GetShortPathName 方法用 static extern 修飾符宣告並且具有 DllImport 屬性,該屬性使用預設名稱GetShortPathName 通知編譯器此實現來自kernel32.dll。若要對 C# 方法使用不同的名稱(如 getShort),則必須在 DllImport 屬性中使用 EntryPoint 選項,如下所示: [DllImport("kernel32.dll", EntryPoint="getShort")] 2使用MarshalAs(UnmanagedType.LPTStr)保證了在任何平臺上都會得到LPTStr,否則預設的方式會把從C#中的字串作為BStr傳遞。 現在如果是僅含有簡單引數和返回值的WIN32 API,就都可以利用這種方法進行對照,簡單的改寫和呼叫了。 二.背後的原理 ―― 知其所以然,相關的知識 1.平臺呼叫詳原理 平臺呼叫依賴於元資料在執行時查詢匯出的函式並封送其引數。下圖顯示了這一過程。 對非託管 DLL 函式的平臺呼叫呼叫
平臺呼叫呼叫非託管函式時,它將依次執行以下操作: 查詢包含該函式的 DLL 將該 DLL 載入到記憶體中。 查詢函式在記憶體中的地址並將其引數推到堆疊上,以封送所需的資料。 注意只在第一次呼叫函式時,才會查詢和載入 DLL 並查詢函式在記憶體中的地址。 將控制權轉移給非託管函式。 平臺呼叫會向託管呼叫方引發由非託管函式生成的異常。  2.關於Attribute(屬性,注意藍色字) 屬性可以放置在幾乎所有宣告中(但特定的屬性可能限制它在其上有效的宣告型別)。在語法上,屬性的指定方法為:將括在方括號中的屬性名置於其適用的實體宣告之前。例如,具有 DllImport 屬性的類將宣告如下: [DllImport] public class MyDllimportClass { ... } 許多屬性都帶引數,而這些引數可以是定位(未命名)引數也可以是命名引數。任何定位引數都必須按特定順序指定並且不能省略,而命名引數是可選的且可以按任意順序指定。首先指定定位引數。例如,這三個屬性是等效的: [DllImport("user32.dll", SetLastError=false, ExactSpelling=false)] [DllImport("user32.dll", ExactSpelling=false, SetLastError=false)] [DllImport("user32.dll")] 第一個引數(DLL 名稱)是定位引數並且總是第一個出現,其他引數為命名引數。在此例中,兩個命名引數都預設為假,因此它們可以省略(有關預設引數值的資訊,請參見各個屬性的文件)。 在一個宣告中可以放置多個屬性,可分開放置,也可放在同一組括號中: bool AMethod([In][Out]ref double x); bool AMethod([Out][In]ref double x); bool AMethod([In,Out]ref double x); 某些屬性對於給定實體可以指定多次。此類可多次使用的屬性的一個示例是 [Conditional("DEBUG"), Conditional("TEST1")] void TraceMethod() {...} 注意根據約定,所有屬性名稱都以單詞“Attribute”結束,以便將它們與 .NET Framework 中的其他項區分。但是,在程式碼中使用屬性時不需要指定屬性字尾。例如,[DllImport] 雖等效於 [DllImportAttribute],但 DllImportAttribute 才是該屬性在 .NET Framework 中的實際名稱。 3MarshalAsAttribute 指示如何在託管程式碼和非託管程式碼之間封送資料。可將該屬性應用於引數、欄位或返回值。 該屬性為可選屬性,因為每個資料型別都有預設的封送處理行為。 大多數情況下,該屬性只是使用 列舉標識非託管資料的格式。 例如,預設情況下,公共語言執行庫將字串引數作為 BStr 封送到 COM 方法,但是可以通過制定MarshalAs屬性,
將字串作為
LPStr BStr 封送到非託管程式碼。某些 UnmanagedType 列舉成員需要附加資訊。 三:進階,如何處理含有複雜的引數和返回值型別的API的呼叫(To Be Continue…
Api 函式是構築Windws應用程式的基石,每一種Windows應用程式開發工具,它提供的底層函式都間接或直接地呼叫了Windows API函式,同時為了實現功能擴充套件,一般也都提供了呼叫WindowsAPI函式的介面, 也就是說具備呼叫動態連線庫的能力。Visual C#和其它開發工具一樣也能夠呼叫動態連結庫的API函式。.NET框架本身提供了這樣一種服務,允許受管轄的程式碼呼叫動態連結庫中實現的非受管轄函式, 包括作業系統提供的Windows API函式。它能夠定位和呼叫輸出函式,根據需要,組織其各個引數(整型、字串型別、陣列、和結構等等)跨越互操作邊界。

下面以C#為例簡單介紹呼叫API的基本過程:  
動態連結庫函式的宣告  
  動態連結庫函式使用前必須宣告,相對於VB,C#函式宣告顯得更加羅嗦,前者通過 Api Viewer貼上以後,可以直接使用,而後者則需要對引數作些額外的變化工作。

  動態連結庫函式宣告部分一般由下列兩部分組成,一是函式名或索引號,二是動態連結庫的檔名。  
  譬如,你想呼叫User32.DLL中的MessageBox函式,我們必須指明函式的名字MessageBoxA或MessageBoxW,以及庫名字 User32.dll,我們知道Win32 API對每一個涉及字串和字元的函式一般都存在兩個版本,單位元組字元的ANSI版本和雙位元組字元的UNICODE版本。

  下面是一個呼叫API函式的例子:  
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,  
CharSet=CharSet.Unicode, ExactSpelling=true,  
CallingConvention=CallingConvention.StdCall)]  
public static extern bool MoveFile(String src, String dst);  

  其中入口點EntryPoint標識函式在動態連結庫的入口位置,在一個受管轄的工程中,目標函式的原始名字和序號入口點不僅標識一個跨越互操作界限的函 數。而且,你還可以把這個入口點對映為一個不同的名字,也就是對函式進行重新命名。重新命名可以給呼叫函式帶來種種便利,通過重新命名,一方面我們不用為函式的 大小寫傷透腦筋,同時它也可以保證與已有的命名規則保持一致,允許帶有不同引數型別的函式共存,更重要的是它簡化了對ANSI和Unicode版本的調 用。CharSet用於標識函式呼叫所採用的是Unicode或是ANSI版本,ExactSpelling=false將告訴編譯器,讓編譯器決定使用 Unicode或者是Ansi版本。其它的引數請參考MSDN線上幫助.

  在C#中,你可以在EntryPoint域通過名字和序號宣告一個動態連結庫函式,如果在方法定義中使用的函式名與DLL入口點相同,你不需要在EntryPoint域顯示宣告函式。否則,你必須使用下列屬性格式指示一個名字和序號。

[DllImport("dllname", EntryPoint="Functionname")]  
[DllImport("dllname", EntryPoint="#123")]  
值得注意的是,你必須在數字序號前加“#”  
下面是一個用MsgBox替換MessageBox名字的例子:  
[C#]  
using System.Runtime.InteropServices;  

public class Win32 {  
[DllImport("user32.dll", EntryPoint="MessageBox")]  
public static extern int MsgBox(int hWnd, String text, String caption, uint type);  
}  
許多受管轄的動態連結庫函式期望你能夠傳遞一個複雜的引數型別給函式,譬如一個使用者定義的結構型別成員或者受管轄程式碼定義的一個類成員,這時你必須提供額外的資訊格式化這個型別,以保持引數原有的佈局和對齊。

C#提供了一個StructLayoutAttribute類,通過它你可以定義自己的格式化型別,在受管轄程式碼中,格式化型別是一個用StructLayoutAttribute說明的結構或類成員,通過它能夠保證其內部成員預期的佈局資訊。佈局的選項共有三種:

佈局選項  
描述  
LayoutKind.Automatic  
為了提高效率允許執行態對型別成員重新排序。  
注意:永遠不要使用這個選項來呼叫不受管轄的動態連結庫函式。  
LayoutKind.Explicit  
對每個域按照FieldOffset屬性對型別成員排序  
LayoutKind.Sequential  
對出現在受管轄型別定義地方的不受管轄記憶體中的型別成員進行排序。  
傳遞結構成員  
下面的例子說明如何在受管轄程式碼中定義一個點和矩形型別,並作為一個引數傳遞給User32.dll庫中的PtInRect函式,  
函式的不受管轄原型宣告如下:  
BOOL PtInRect(const RECT *lprc, POINT pt);  
注意你必須通過引用傳遞Rect結構引數,因為函式需要一個Rect的結構指標。  
[C#]  
using System.Runtime.InteropServices;  

[StructLayout(LayoutKind.Sequential)]  
public struct Point {  
public int x;  
public int y;  
}  

[StructLayout(LayoutKind.Explicit]  
public struct Rect {  
[FieldOffset(0)] public int left;  
[FieldOffset(4)] public int top;  
[FieldOffset(8)] public int right;  
[FieldOffset(12)] public int bottom;  
}  

class Win32API {  
[DllImport("User32.dll")]  
public static extern Bool PtInRect(ref Rect r, Point p);  
}  
類似你可以呼叫GetSystemInfo函式獲得系統資訊:  
? using System.Runtime.InteropServices;  
[StructLayout(LayoutKind.Sequential)]  
public struct SYSTEM_INFO {  
public uint dwOemId;  
public uint dwPageSize;  
public uint lpMinimumApplicationAddress;  
public uint lpMaximumApplicationAddress;  
public uint dwActiveProcessorMask;  
public uint dwNumberOfProcessors;  
public uint dwProcessorType;  
public uint dwAllocationGranularity;  
public uint dwProcessorLevel;  
public uint dwProcessorRevision;  
}  
[DllImport("kernel32")]  
static extern void GetSystemInfo(ref SYSTEM_INFO pSI);  

SYSTEM_INFO pSI = new SYSTEM_INFO();  
GetSystemInfo(ref pSI);  

類成員的傳遞  
同 樣只要類具有一個固定的類成員佈局,你也可以傳遞一個類成員給一個不受管轄的動態連結庫函式,下面的例子主要說明如何傳遞一個sequential順序定 義的MySystemTime類給User32.dll的GetSystemTime函式, 函式用C/C++呼叫規範如下:

void GetSystemTime(SYSTEMTIME* SystemTime);  
不像傳值型別,類總是通過引用傳遞引數.  
[C#]  
[StructLayout(LayoutKind.Sequential)]  
public class MySystemTime {  
public ushort wYear;  
public ushort wMonth;  
public ushort wDayOfWeek;  
public ushort wDay;  
public ushort wHour;  
public ushort wMinute;  
public ushort wSecond;  
public ushort wMilliseconds;  
}  
class Win32API {  
[DllImport("User32.dll")]  
public static extern void GetSystemTime(MySystemTime st);  
}  
回撥函式的傳遞:  
從受管轄的程式碼中呼叫大多數動態連結庫函式,你只需建立一個受管轄的函式定義,然後呼叫它即可,這個過程非常直接。  
如果一個動態連結庫函式需要一個函式指標作為引數,你還需要做以下幾步:  
首先,你必須參考有關這個函式的文件,確定這個函式是否需要一個回撥;第二,你必須在受管轄程式碼中建立一個回撥函式;最後,你可以把指向這個函式的指標作為一個引數創遞給DLL函式,.

回撥函式及其實現:  
回 調函式經常用在任務需要重複執行的場合,譬如用於列舉函式,譬如Win32 API 中的EnumFontFamilies(字型列舉), EnumPrinters(印表機), EnumWindows (視窗列舉)函式. 下面以視窗列舉為例,談談如何通過呼叫EnumWindow 函式遍歷系統中存在的所有視窗

分下面幾個步驟:  
1. 在實現呼叫前先參考函式的宣告  
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam)  
顯然這個函式需要一個回撥函式地址作為引數.  
2. 建立一個受管轄的回撥函式,這個例子宣告為代表型別(delegate),也就是我們所說的回撥,它帶有兩個引數hwnd和lparam,第一個引數是一個視窗控制代碼,第二個引數由應用程式定義,兩個引數均為整形。

   當這個回撥函式返回一個非零值時,標示執行成功,零則暗示失敗,這個例子總是返回True值,以便持續列舉。  
3. 最後建立以代表物件(delegate),並把它作為一個引數傳遞給EnumWindows 函式,平臺會自動地 把代表轉化成函式能夠識別的回撥格式。

[C#]  
using System;  
using System.Runtime.InteropServices;  

public delegate bool CallBack(int hwnd, int lParam);  

public class EnumReportApp {  

[DllImport("user32")]  
public static extern int EnumWindows(CallBack x, int y);  

public static void Main()  
{  
CallBack myCallBack = new CallBack(EnumReportApp.Report);  
EnumWindows(myCallBack, 0);  
}  

public static bool Report(int hwnd, int lParam) {  
Console.Write("視窗控制代碼為");  
Console.WriteLine(hwnd);  
return true;  
}  
}  
指標型別引數傳遞:  
  在Windows API函式呼叫時,大部分函式採用指標傳遞引數,對一個結構變數指標,我們除了使用上面的類和結構方法傳遞引數之外,我們有時還可以採用陣列傳遞引數。

  下面這個函式通過呼叫GetUserName獲得使用者名稱  
BOOL GetUserName(  
LPTSTR lpBuffer, // 使用者名稱緩衝區  
LPDWORD nSize // 存放緩衝區大小的地址指標  
);  
    
[DllImport("Advapi32.dll",  
EntryPoint="GetComputerName",  
ExactSpelling=false,  
SetLastError=true)]  
static extern bool GetComputerName (  
[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,  
  [MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );  
  這個函式接受兩個引數,char * 和int *,因為你必須分配一個字串緩衝區以接受字串指標,
你可以使用String類代替這個引數型別,當然你還可以宣告一個位元組陣列傳遞ANSI字串,同樣你也可以宣告一個只有一個元素的長整型陣列,使用陣列名作為第二個引數。上面的函式可以呼叫如下:

byte[] str=new byte[20];  
Int32[] len=new Int32[1];  
len[0]=20;  
GetComputerName (str,len);  
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));  
  最後需要提醒的是,每一種方法使用前必須在檔案頭加上:  
  using System.Runtime.InteropServices;
參考文章 1. Eric Gunnerson的《》(非常好!!!)