本帖最后由 bcrun 于 2013-07-16 08:39:07 編輯
提到腳本,腦海里馬上閃過一大堆:Python,Perl,Ruby,PHP,JS,VBS,LUA。。。 不過你有沒聽說過,用經(jīng)典的C++做腳本語言嗎?先不多說,上個(gè)圖。
或許你在想這一定是瘋了,用世界上最復(fù)雜的語言做腳本,寫的人累不說,腳本引擎先累壞了。各種復(fù)雜的模板庫,要邊解釋邊運(yùn)行,得有多強(qiáng)大的虛擬機(jī)才撐得住。
好吧,那么我們退一步,不強(qiáng)求解釋執(zhí)行,回歸到原始的編譯后執(zhí)行?!?nbsp;不過那還算腳本嗎?
編譯速度
事實(shí)上如今高性能的腳本都是先編譯后運(yùn)行的,大名鼎鼎的JavaScript V8引擎,號(hào)稱速度最快的LUA-Jit,以及眾所周知的ActionScript。。。預(yù)先編譯不僅能大幅提高運(yùn)行速度,更重要的是能夠提前發(fā)現(xiàn)腳本中顯式的錯(cuò)誤。
但腳本中所謂的編譯,和傳統(tǒng)語言的編譯,還是很大區(qū)別的。腳本的編譯,不過是代碼上的深度優(yōu)化,很快就可以完成。相比復(fù)雜了多的C++來說,似乎是望塵莫及的。提到C++的編譯速度,大家的映象莫過于在VC里按下F5之后,看著輸出框內(nèi)一條一條的“Compiling...”緩緩出現(xiàn)。有時(shí)僅僅測試一個(gè)微小的修改,也要等上好幾秒的時(shí)間。緩慢的編譯速度備受煎熬,以至于簡單的程序往往選擇VB或C#這樣可以快速調(diào)試的語言。
對(duì)于龐大的MFC程序來說,緩慢的編譯是理所當(dāng)然的。但簡單的小程序出現(xiàn)過長的編譯時(shí)間,那一定是頭文件引用的不合理了。事實(shí)上,使用預(yù)處理頭文件的小程序,編譯僅僅是一瞬間的,之后的各種停頓往往是IDE引起的。
那么我們就來測試下,不用IDE,僅用純命令編譯個(gè)C++小程序。我們使用VC6.0的編譯器:CL.exe
為了確保純凈的編譯環(huán)境,我們把CL.exe必須依賴的文件復(fù)制到新建的文件夾里。對(duì)于VC6的版本,只要有如下5個(gè)文件,就可以完成.cpp到.exe的編譯了。
1 2 3 4 5 | CL.exe
C1XX.DLL
C2.DLL
MSPDB60.DLL
Link.exe
|
打開cmd,設(shè)置好環(huán)境變量,對(duì)應(yīng)到VC6的頭目錄和庫目錄
1 2 | SET INCLUDE=C:\Program Files (x86)\Microsoft Visual Studio\VC98\Include
SET LIB=C:\Program Files (x86)\Microsoft Visual Studio\VC98\ Lib
|
就可以調(diào)用命令編譯了:
cl test.cpp
一眨眼的工夫,編譯和鏈接完成,生成了test.exe,一切正常。而這還是在沒有使用預(yù)編譯頭的情況下編譯的。
由此可見,即使語言本身很復(fù)雜,但只要用它寫的代碼不復(fù)雜,編譯還是非常快的。
仔細(xì)想想也應(yīng)如此。以如今的硬件配置,運(yùn)行98年的編譯器,編譯一個(gè)才幾行代碼的程序,自然是一瞬間的。
命令行編譯簡單的C++程序是如此的快速,利用這個(gè)優(yōu)勢(shì),繼續(xù)我們的腳本探索。。。
運(yùn)行環(huán)境
如果要寫一個(gè)生成100個(gè)隨機(jī)序列號(hào)的小程序,你會(huì)使用哪類語言?
相比傳統(tǒng)語言要先創(chuàng)建一個(gè)工程項(xiàng)目,我們直接在桌面新建個(gè)文本文件就可以寫腳本了。
雖然用文本編輯器寫代碼沒任何優(yōu)勢(shì),但對(duì)于簡單的程序足矣。之后程序交給其他人使用時(shí),腳本優(yōu)勢(shì)就淋漓盡致的體現(xiàn)出來了:當(dāng)他們自己想簡單修改一些邏輯規(guī)則時(shí),只需用記事本打開就可以,而記事本每臺(tái)電腦上都有。
相反,傳統(tǒng)語言寫的程序,即使有源代碼,用戶想簡單的修改下也無法生效,還需安裝并配置好相應(yīng)的開發(fā)環(huán)境才行,這對(duì)不熟悉的人來說頗費(fèi)周折。
所以腳本必須足夠簡單 —— 簡單到用戶只管修改和運(yùn)行就可以,其他步驟都交給腳本宿主自動(dòng)完成了。
如果想用C++寫腳本,那么代碼的編譯和鏈接當(dāng)然必須是全自動(dòng)的,這并不復(fù)雜。
但僅僅依靠CL.exe等幾個(gè)命令還是不夠的,因?yàn)樵谄渌碾娔X上并沒有相應(yīng)的開發(fā)環(huán)境 —— Include和Lib文件夾,因此就無法通過編譯和鏈接了。
而這些頭文件庫文件,一共多達(dá)上千個(gè),全都帶上則有近百兆!顯然,我們的腳本只用到幾個(gè)基本功能就可以了,那些復(fù)雜的windows頭文件就沒必要了。
事實(shí)上,程序的頭文件只是函數(shù)和結(jié)構(gòu)的定義,僅僅用來給編譯器分析而已,最終并不生成實(shí)際的指令。所以,我們把常用的頭文件,事先生成一個(gè).pch預(yù)編譯頭文件就可以。以后編譯時(shí),將他對(duì)應(yīng)到某個(gè)頭文件就可以了,例如stdafx.h。這樣就無需使用任何頭文件了。即使stdafx.h也不在,編譯仍然能通過,因?yàn)檫@一切都打包在.pch里面了。并且大量的頭文件經(jīng)過事先的分析,編譯時(shí)就無需再編譯它們了,速度大幅提升。
至于Lib文件,里面都是庫函數(shù)的內(nèi)容。除非整個(gè)程序不使用任何C運(yùn)行時(shí)庫,那么我們可以不帶上任何lib,但那樣只能寫最基本的代碼了。對(duì)于一般的簡單腳本程序,只需幾個(gè)必要的lib即可:KERNEL32.LIB,LIBCMT.LIB,LIBCPMT.LIB,OLDNAMES.LIB??偣膊?M多。
我們把這幾個(gè)lib文件以及.pch文件,放在cl.exe同個(gè)目錄下,這樣就無需指定INCLUDE和LIB環(huán)境變量。
至此,我們有了一個(gè)精簡版的VC6編譯器。通過它們,我們可以不依賴任何環(huán)境,獨(dú)立編譯C++程序了。
實(shí)際運(yùn)行
現(xiàn)在,我們可以動(dòng)態(tài)產(chǎn)生C++代碼文件,并且自動(dòng)編譯的能力了。但是如何將最終的二進(jìn)制文件運(yùn)行在實(shí)際的腳本宿主里呢?
顯然,exe程序運(yùn)行在獨(dú)立的進(jìn)程里,數(shù)據(jù)交互只能通過匿名管道,要實(shí)現(xiàn)回調(diào)什么的就非常困難了。
但若換成dll就可以大顯身手了,不僅運(yùn)行在同一進(jìn)程空間內(nèi),更重要的是dll是可以動(dòng)態(tài)加載卸載的,這一點(diǎn)太符合腳本程序的特性了。當(dāng)我們更新了腳本之后,就可以把先前的dll釋放掉,換上最新的。而這一切都是動(dòng)態(tài)的,無需重啟宿主即可完成!
而且dll可以導(dǎo)出內(nèi)部的函數(shù),宿主用GetProcAddress()就可以獲得某個(gè)函數(shù)地址即可調(diào)用;至于回調(diào),傳遞一個(gè)宿主的函數(shù)地址給腳本就可以了。只要約定好函數(shù)聲明,雙方都可以用最簡單原始的方法互相調(diào)用,甚至共享同一塊內(nèi)存空間。
為了讓函數(shù)導(dǎo)出更簡潔,本例中定義了個(gè)叫function的宏:
1 | #define function extern "C" __declspec(dllexport) void
|
于是就可以簡單的定義一個(gè)導(dǎo)出函數(shù)了:
1 2 3 4 | function Test()
{
// some code here
}
|
是不是很有腳本的感覺呢:)
語法檢查
一個(gè)用文本編輯器編寫的代碼,拼寫錯(cuò)誤自然是很難避免的。所以一個(gè)好的腳本引擎,會(huì)在運(yùn)行前做一次全面的語法檢查,事先排除明顯的錯(cuò)誤。
C++就是將其做到了極限,不僅能查出致命的錯(cuò)誤,甚至不規(guī)范的代碼也會(huì)有警告提示。這是非常值得的,一個(gè)小bug浪費(fèi)的時(shí)間,足夠幾萬次編譯了。
想要在我們的C++腳本里實(shí)現(xiàn)這個(gè)功能,其實(shí)是非常簡單的。因?yàn)樵谡{(diào)用cl.exe編譯時(shí),要是有編譯錯(cuò)誤就會(huì)反饋出來。我們根據(jù)對(duì)應(yīng)的錯(cuò)誤行號(hào),提示用戶就可以了。
調(diào)試環(huán)境
一個(gè)強(qiáng)大的腳本引擎,往往帶有調(diào)試器。雖然編譯器能夠預(yù)先排除一些錯(cuò)誤,但是邏輯上的錯(cuò)誤只有在運(yùn)行時(shí)才能出現(xiàn)。
對(duì)于簡單的腳本程序,這項(xiàng)功能似乎不那么重要。畢竟在調(diào)試狀態(tài)下運(yùn)行,性能會(huì)有所影響。
在C++腳本里,我們可以通過宏來擴(kuò)展調(diào)試功能,決定是否輸出調(diào)試信息。不過對(duì)于異常錯(cuò)誤,處理就比較講究了。
由于我們最終運(yùn)行的是二進(jìn)制dll模塊,這和普通的腳本有著天壤之別。dll模塊是和宿主共用一個(gè)進(jìn)程的,所以一旦當(dāng)dll內(nèi)異常觸發(fā)時(shí),整個(gè)進(jìn)程包括宿主一塊進(jìn)入調(diào)試狀態(tài)了(系統(tǒng)裝有開發(fā)環(huán)境的話)。如果錯(cuò)誤過于嚴(yán)重,會(huì)導(dǎo)致整個(gè)進(jìn)程的崩潰。這是個(gè)非常值得注意的地方,也是C++作腳本在權(quán)限上的隱患。所以盡可能少用指針特性,使用更安全的代碼,讓代碼風(fēng)險(xiǎn)降到最少。
對(duì)于致命的錯(cuò)誤,宿主記錄下dump文件是非常重要的,方便調(diào)試。
不過出于簡單,本例的宿主是用VB寫的,也就無法在調(diào)用前使用__try{}進(jìn)行SEH捕捉。如果宿主也是C++實(shí)現(xiàn)的話,則盡可能捕捉dll內(nèi)的異常。
開發(fā)環(huán)境
有別于腳本語言,C++本身就是用于大型程序的開發(fā),所以開發(fā)環(huán)境是非常完善的。
但作為一個(gè)腳本,往往都是單個(gè)的文本文件,而不是一個(gè)項(xiàng)目組。任何版本的VC編輯單個(gè)cpp文件,和編輯純文本文件幾乎沒有區(qū)別。因此我們事先得建立一個(gè)模板項(xiàng)目,將需要編輯的cpp移到此項(xiàng)目內(nèi)開發(fā),這樣才會(huì)有下拉框智能提示等功能。
不過既然選擇它作為腳本來使用,那就應(yīng)該用來處理一些簡單的,經(jīng)常變更的邏輯事務(wù)。對(duì)于復(fù)雜的腳本程序,還不如直接寫在宿主里面了。
事實(shí)上,“程序”和“腳本”之間從沒一條固定的界限。用純粹的程序也可以寫一個(gè)復(fù)雜的游戲故事情節(jié),用純粹的腳本也可以開發(fā)一個(gè)大型項(xiàng)目。只不過太過死板,或太過靈活,都會(huì)增加額外的工作量。
總結(jié)
與其稱之為C++腳本,倒不如說是插件———可以根據(jù)需求,動(dòng)態(tài)產(chǎn)生指令的插件。
雖然可以玩轉(zhuǎn)出一些腳本的特征,然而C++終究是門嚴(yán)格的語言。相比腳本的靈活性,C++固然更為嚴(yán)謹(jǐn)和死板。當(dāng)然,憑借強(qiáng)大的宏、模版、運(yùn)算符重載,我們可以充分?jǐn)U展,為腳本提供豐富多樣的特征和語法糖。
當(dāng)然,它的優(yōu)勢(shì)也是顯而易見的:性能超高,交互簡單。
事實(shí)上,不僅僅是C++,任何一門高級(jí)語言都可以當(dāng)“腳本”使用,只要調(diào)用它們的編譯器即可。如果喜歡C#,或者Java風(fēng)格,只需稍作修改就可以。
為了簡單演示,本例使用VB寫了個(gè)簡單的宿主程序,包括基本的編譯,鏈接,加載,語法檢查功能。
宿主提供了一個(gè)叫“Print”的接口,可以輸出字符串。要實(shí)現(xiàn)更多接口和擴(kuò)展功能,修改cl文件夾內(nèi)的T.h即可。
源碼可以在這里下載:
http://files.cnblogs.com/index-html/CppScript.rar
其中有一個(gè)DLLTmpl的工程,沒有任何用處,僅僅為了生成一個(gè).pch預(yù)編譯頭文件而已。如果想在腳本里使用更多的頭文件,就得在StdAfx.h內(nèi)添加。編譯之后的release/MyDll.pch復(fù)制到cl文件夾,覆蓋原有的即可。