C#如何直接調用非托管代碼,通常有2種方法:
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,
DWORD cchBuffer
);
2.查找對照表進行數據類型的轉換(出處:http://msdn.microsoft.com/msdnmag/issues/03/07/NET/default.aspx?fig=true )Data Types
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 ,static,extern基本上是必須有的,其他CharSet,MarshalAs(…)是可選項,在這里即使沒有,程序也是可以調用此API了。
說明:
1.MSSQL_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 { ... }
有關更多信息,請參見 DllImportAttribute 類。
許多屬性都帶參數,而這些參數可以是定位(未命名)參數也可以是命名參數。任何定位參數都必須按特定順序指定並且不能省略,而命名參數是可選的且可以按任意順序指定。首先指定定位參數。例如,這三個屬性是等效的:
[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:
[Conditional("DEBUG"), Conditional("TEST1")] void TraceMethod() {...}
注意 根據約定,所有屬性名稱都以單詞“Attribute”結束,以便將它們與 .NET Framework 中的其他項區分。但是,在代碼中使用屬性時不需要指定屬性后綴。例如,[DllImport] 雖等效於 [DllImportAttribute],但DllImportAttribute才是該屬性在 .NET Framework 中的實際名稱。
指示如何在托管代碼和非托管代碼之間封送數據。可將該屬性應用於參數、字段或返回值。
該屬性為可選屬性,因為每個數據類型都有默認的封送處理行為。
大多數情況下,該屬性只是使用 UnmanagedType 枚舉標識非托管數據的格式。
例如,默認情況下,公共語言運行庫將字符串參數作為 BStr 封送到 COM 方法,但是可以通過制定MarshalAs屬性,
將字符串作為 LPStr、LPWStr、LPTStr 或 BStr 封送到非托管代碼。某些 UnmanagedType 枚舉成員需要附加信息。
三:進階,如何處理含有復雜的參數和返回值類型的API的調用(To Be Continue…)
Api函數是構築Windws應用程序的基石,每一種Windows應用程序開發工具,它提供的底層函數都間接或直接地調用了Windows API函數,同時為了實現功能擴展,一般也都提供了調用WindowsAPI函數的接口, 也就是說具備調用動態連接庫的能力。VisualC#和其它開發工具一樣也能夠調用動態鏈接庫的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;