第 27 講:指針(四):和底層的互操作
另外,這篇文章可以考慮在以后學(xué)習(xí)了其它的知識(shí)點(diǎn)后,再來回頭看。反正也不是很重要。
Part 1 互操作性
互操作性(也叫交互性,Interoperability),指的是 C# 的代碼上可以跑 C 和 C++ 的程序的代碼。從另外一個(gè)角度來說,由于我們?cè)试S使用這樣的機(jī)制來執(zhí)行程序,因此我們可以允許 C# 跑 Linux 上的 C 語言程序,因此平臺(tái)不相同了。所以,這個(gè)情況一般也可以記作 P/Invoke。
其中 P/Invoke 的 P 是 Platform(平臺(tái))的縮寫。所以 P/Invoke 也就叫做平臺(tái)調(diào)用。
我們來舉個(gè)例子。由于我們是黑框程序(控制臺(tái)),因此我們想要用彈窗告知用戶一些信息。但是,C# 的控制臺(tái)又不是平時(shí)肉眼所見的彈框,因此我們需要借助別的方法來產(chǎn)生這個(gè)彈框。
我們這里要用到 C 語言和 C++ 里用的函數(shù):MessageBox
。這個(gè)函數(shù)被存放在 user32.dll
這個(gè)文件里。
[DllImport("user32")]
。以方括號(hào)記號(hào)標(biāo)記在方法上方的模式叫做特性(Attribute)。
接著,因?yàn)閷戇M(jìn) C# 代碼的方法是外來導(dǎo)入的,因此這樣的方法稱為外部方法(External Method)。這樣的方法需要在簽名上添加 extern
關(guān)鍵字以表示方法是外來的。這個(gè)方法帶有四個(gè)參數(shù)分別是 void*
、string
、string
和 uint
,并返回一個(gè) int
類型結(jié)果。
什么?你不知道這個(gè)參數(shù)為啥是這樣?互操作性是從 C/C++ 引入的函數(shù),所以這個(gè)得網(wǎng)上搜資料才知道。這個(gè)函數(shù)的聲明并不是拿給你背的。
另外,但凡有一個(gè)參數(shù)的類型,還有返回值類型寫錯(cuò),整個(gè)程序都會(huì)崩潰,因?yàn)檫@樣的函數(shù)在文件里因?yàn)閰?shù)和返回值無法對(duì)應(yīng)起來,就會(huì)導(dǎo)致傳參失敗。
然后我們?cè)囍\(yùn)行下這個(gè)程序。你在看到控制臺(tái)打開的時(shí)候,立刻彈出一個(gè)新的白色框,提示一段文字;這些文字都是在剛才 C# 代碼里寫的這些字符串。

這就是運(yùn)行結(jié)果了。
然后提一句,這里引用 C/C++ 的函數(shù)的過程叫做調(diào)用(Invoke)。這里的調(diào)用(invoke)和之前介紹方法的調(diào)用(call)是不一樣的英語單詞,只是中文里用的是同一個(gè)詞語,在英文環(huán)境下,它們并不是一個(gè)意思。這里的調(diào)用(invoke)指的是一種“回調(diào)”過程:方法本身并不是我們控制的調(diào)用,因?yàn)榈讓拥拇a并非由我們自己實(shí)現(xiàn),而過程是自動(dòng)調(diào)用的;而相反地,方法里的調(diào)用(call)過程,是我們自己控制的,我想這么調(diào)用就這么調(diào)用。
Part 2 MarshalAs
標(biāo)記
這樣的代碼是不嚴(yán)謹(jǐn)?shù)摹R驗(yàn)榈讓訉?shí)現(xiàn)的關(guān)系,C 語言里的 int
類型大小并不是完全和 C# 語言的 int
類型正確對(duì)應(yīng)起來的,因此,我們需要指定參數(shù)在交互的時(shí)候,和 C 語言里真正對(duì)應(yīng)起來的轉(zhuǎn)換類型。
看一下這個(gè)例子。我們除了用上方的 [DllImport]
以外,還需要為參數(shù)添加 [MarshalAs]
修飾。
'\0'
作為終止字符的。因此,我們?cè)谥付ǖ臅r(shí)候,為了嚴(yán)謹(jǐn)需要添加 [MarshalAs(UnmanagedType.LPStr)]
。這個(gè)寫法專門表示和指明這個(gè)參數(shù)在調(diào)用的時(shí)候會(huì)自動(dòng)轉(zhuǎn)換成 C 語言和 C++ 的這種字符串形式。另外,最后一個(gè)參數(shù)我們固定指定 [MarshalAs(UnmanagedType.U4)]
表示傳入的是 unsigned int
類型,大小是 4 個(gè)字節(jié)。
然后,第 4 行的 [return: MarshalAs(UnmanagedType.I4)]
實(shí)際上指的是,這個(gè)函數(shù)的返回值在底層是什么類型的。這里寫成這樣表示,底層是 C/C++ 里的 int
類型。
Part 3 調(diào)用變長(zhǎng)參數(shù)函數(shù)
在 C 語言和 C++ 里,擁有一個(gè)特殊的函數(shù)類型,叫做變長(zhǎng)參數(shù)函數(shù)。變長(zhǎng)參數(shù)使用三個(gè)小數(shù)點(diǎn)來表達(dá)。這種函數(shù)我們?cè)趺磳?C# 代碼呢?難道是 params
參數(shù)修飾嗎?
實(shí)際上不是。因?yàn)?C 語言和 C++ 里的變長(zhǎng)參數(shù)實(shí)現(xiàn)模型和 C# 的是不一樣的,因此我們需要借助一個(gè)特殊的關(guān)鍵字來完成:__arglist
。這個(gè)關(guān)鍵字比較特殊的地方在于,它是以雙下劃線開頭的關(guān)鍵字。
稍微注意一下的是,我們這里要用到一個(gè)地方的修改:[DllImport]
里要在后面追加一個(gè)叫 EntryPoint = "printf"
這個(gè)修改是為了指定執(zhí)行的方法在 DLL 文件里名字是什么。因?yàn)?C# 的方法約定是使用大寫字母開頭的單詞,因此我們寫大寫的話,可能會(huì)導(dǎo)致這個(gè)叫 Printf
的函數(shù)在文件里找不到。因此我們追加這個(gè)東西來告知程序在處理的時(shí)候自動(dòng)去找 printf
。
運(yùn)行程序。我們可以看到結(jié)果:

這就是 C# 里使用 C/C++ 里的變長(zhǎng)參數(shù)的辦法。
Part 4 總結(jié)
至此,我們就把 C# 的指針大致給大家說了一下。指針的內(nèi)容比較復(fù)雜,特別是本節(jié)的內(nèi)容可能讓你看得是一頭霧水。沒有關(guān)系。以后還有更難的東西(不是
好好學(xué)。這點(diǎn)內(nèi)容可以以后來看,至于重要不重要,我相信你自己應(yīng)該是知道的。