• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-341  評(píng)論-2670  文章-0  trackbacks-0

            手把手教你寫(xiě)腳本引擎(二)——命令腳本

             

            陳梓瀚

            華南理工大學(xué)軟件本科05級(jí)

            vczh@163.com

            http://www.shnenglu.com/vczh/

             

            這次要實(shí)現(xiàn)的是一個(gè)形式最簡(jiǎn)單的腳本。這種腳本僅有命令、標(biāo)號(hào)及跳轉(zhuǎn)構(gòu)成,看起來(lái)就跟匯編一樣,不過(guò)好是比較好讀的。雖然這種腳本語(yǔ)言的語(yǔ)法非常簡(jiǎn)單,但是最基本的要素還是要有的。

             

            作為一個(gè)腳本引擎,為了可以在各種各樣的合適的宿主程序中使用,腳本本身最好不要涉及到具體的領(lǐng)域。當(dāng)然,如果這個(gè)腳本被創(chuàng)建的目的僅僅是為了某個(gè)領(lǐng)域的話,那就無(wú)所謂了。因此,一個(gè)腳本引擎需要一個(gè)檢查和運(yùn)行代碼的機(jī)制、運(yùn)行時(shí)環(huán)境的維護(hù)以及一個(gè)功能足夠使用的插件系統(tǒng)。一個(gè)完整的腳本引擎至少需要如下部件:

             

            1、代碼數(shù)據(jù)結(jié)構(gòu)。代碼的數(shù)據(jù)結(jié)構(gòu)用來(lái)存放經(jīng)過(guò)分析的腳本代碼。事實(shí)上解釋型的腳本引擎,也就是邊執(zhí)行邊分析代碼字符串的腳本引擎是比較難做,而且效率也不高的。腳本代碼經(jīng)過(guò)事先分析,可以檢查一出一些在運(yùn)行之前就能夠檢查的錯(cuò)誤。而且我們把腳本的代碼重新處理成一個(gè)數(shù)據(jù)結(jié)構(gòu)之后,執(zhí)行也變得更加容易控制。

             

            2、運(yùn)行時(shí)環(huán)境。運(yùn)行時(shí)環(huán)境用于存放腳本在運(yùn)行的過(guò)程中產(chǎn)生的數(shù)據(jù),譬如堆棧、變量和狀態(tài)信息等。對(duì)于一個(gè)已知的代碼,不同的運(yùn)行時(shí)環(huán)境代表不同的腳本執(zhí)行流程。為了讓腳本可以同時(shí)(但不一定是并發(fā))執(zhí)行,將運(yùn)行時(shí)環(huán)境獨(dú)立出來(lái)也就顯得必要了。

             

            3、語(yǔ)法分析器。語(yǔ)法分析器用于將代碼轉(zhuǎn)換成等價(jià)的代碼數(shù)據(jù)結(jié)構(gòu),并在發(fā)現(xiàn)代碼出錯(cuò)的時(shí)候輸出合適的錯(cuò)誤信息。

             

            4、插件。插件是腳本與外部環(huán)境交互的途徑之一。有了插件系統(tǒng),我們可以為腳本引擎添加額外的、跟腳本引擎無(wú)關(guān)的功能,譬如文件操作、屏幕輸入輸出等。如果必要的話,插件系統(tǒng)可以將腳本引擎與領(lǐng)域信息互相隔離,系統(tǒng)將變得更加容易使用。

             

            5、虛擬機(jī)。虛擬機(jī)用于執(zhí)行代碼并返回相應(yīng)的結(jié)果。我們?cè)谑褂媚_本引擎時(shí)直接跟虛擬機(jī)進(jìn)行交互,虛擬機(jī)則協(xié)調(diào)上述4個(gè)部件的相互協(xié)作。

             

            在知道了這些之后,我們就可以開(kāi)始開(kāi)發(fā)一個(gè)基于命令的腳本引擎了。為了更加詳細(xì)以及明確地講述開(kāi)發(fā)過(guò)程以及原理,在這里將構(gòu)造一門(mén)簡(jiǎn)單的基于命令的語(yǔ)言。一門(mén)語(yǔ)言至少還是要有分支和循環(huán)的。但是為了簡(jiǎn)化,我們將分支和循環(huán)分解成判斷與跳轉(zhuǎn)。語(yǔ)言可以自由添加標(biāo)號(hào),標(biāo)號(hào)將作為跳轉(zhuǎn)的目標(biāo)而出現(xiàn)。這門(mén)語(yǔ)言使用如下語(yǔ)法:

             

            <>:值可以是整數(shù)、小數(shù)、字符串或名字。

            <>:名可以是變量名或者標(biāo)號(hào)等,使用字母與下劃線開(kāi)始,后接不定數(shù)量的字母、下劃線與數(shù)字。

            <>::名字后接冒號(hào)代表一個(gè)標(biāo)號(hào)。這個(gè)標(biāo)號(hào)代表著一個(gè)指令的位置,用于指定跳轉(zhuǎn)目標(biāo)。

            goto <>goto用于直接跳轉(zhuǎn)到一個(gè)位置繼續(xù)執(zhí)行。

            set <> <>set用于將一個(gè)值賦值給一個(gè)指定名字的變量。這個(gè)變量不存在則創(chuàng)建。

            opcode <> <> <>opcode可以是addminusmuldividivmod。這6個(gè)命令將兩個(gè)值進(jìn)行加、減、乘、除、整除及求余,并將結(jié)果賦值給一個(gè)指定名字的變量。這個(gè)變量不存在則創(chuàng)建。

            if <>[ opcode <>] goto <>if用于判斷一個(gè)條件并在條件滿足被滿足的時(shí)候跳轉(zhuǎn)到指定的地方。條件可以是一個(gè)值,這個(gè)值必須是整數(shù),并且在這個(gè)值不為0的時(shí)候條件被滿足。條件也可以是一個(gè)比較,這個(gè)時(shí)候opcode可以是isis_notless_thangreater_thanless_equalgreater_equal,分別在第一個(gè)值等于、不等于、小于、大于、小于或等于、大于或等于第二個(gè)值的時(shí)候滿足條件。

            exit:結(jié)束執(zhí)行

            <> <>*:如果命令名稱(chēng)不是上面的5種的其中一種的話,那么這個(gè)命令將被傳遞給插件進(jìn)行執(zhí)行。這個(gè)時(shí)候,命令可以有任意的參數(shù)。

             

            在這種語(yǔ)法下,我們可以假設(shè)宿主程序給了我們writewritelnread命令用于輸入輸出,并得到一個(gè)判斷輸入的數(shù)字是否質(zhì)數(shù)的程序:

              write "請(qǐng)輸入一個(gè)數(shù)字:"

              read Number

              if Number less_then 2 goto FAIL

              if Number is 2 goto SUCCESS

              set Divisor 2

            LOOP_BEGIN:

              if Number is Divisor goto SUCCESS

              mod Remainder Number Divisor

              if Remainder is 0 goto FAIL

              add Divisor Divisor 1

              goto LOOP_BEGIN

            SUCCESS:

              writeln Number "是質(zhì)數(shù)。"

              exit

            FAIL:

              writeln Number "不是質(zhì)數(shù)。"

            這個(gè)程序首先判斷輸入是不是小于等于2,如果不是的話則使用一種簡(jiǎn)單的方法來(lái)判斷輸入是不是質(zhì)數(shù)。假設(shè)輸入的數(shù)字為n,那么在n>2的時(shí)候,如果2n-1中的任何一個(gè)數(shù)字能夠整除n的話,那么n就不是質(zhì)數(shù)了。下圖是這個(gè)腳本的運(yùn)行結(jié)果:

             

            現(xiàn)在開(kāi)始實(shí)現(xiàn)它。

             

            在真正開(kāi)始讀腳本之前,我們需要一個(gè)在內(nèi)存中表達(dá)命令的方法。命令有兩種,一種是跳轉(zhuǎn)標(biāo)號(hào),另一種是普通的命令。于是我們可以大概給出一個(gè)數(shù)據(jù)結(jié)構(gòu)。跳轉(zhuǎn)標(biāo)號(hào)表用于查詢(xún)一個(gè)名字所指定的命令的位置,而一個(gè)命令就由一個(gè)名字和一個(gè)參數(shù)列表構(gòu)成。參數(shù)列表中的參數(shù)不僅有內(nèi)容,還有類(lèi)型。主要用于區(qū)分字符串和名字:

                 enum LexerType

                 {

                     ltString,

                     ltName

                 };

             

                 class LexerToken

                 {

                 public:

                     LexerType Type;

                     wstring Token;

            };

             

                 class Command

                 {

                 public:

                     wstring Name;

                     vector<LexerToken> Parameters;

            };

            至于命令與標(biāo)號(hào)的表示方法則用如下代碼:

                 vector<Command> FCommands;

            map<wstring , size_t> FLabels;

             

            好了,現(xiàn)在讓我們看看一行代碼應(yīng)該如何分析。由于腳本支持字符串,所以我們不能簡(jiǎn)單地使用空格來(lái)分割。如果我們遇到了“  writeln Number "是質(zhì)數(shù)。"”,那么我們期望的結(jié)果是這一行代碼被拆分成三個(gè)部分,分別是writelnNumber"是質(zhì)數(shù)。"。于是我們可以寫(xiě)一個(gè)函數(shù),一次取出一個(gè)部分。那么我們只要一直取道換行符或者字符串結(jié)束,就能獲得一行的所有部分了。

             

            腳本代碼由整數(shù)、小數(shù)、字符串、名字以及冒號(hào)組成。于是我們可以寫(xiě)很多類(lèi)似的代碼,然而格式都是int GetXXX(wchar_t*& Input);。這個(gè)函數(shù)檢查Input是否由XXX開(kāi)始,返回值代表XXX用掉了多少個(gè)字符,然后把Input參數(shù)往后推那么多個(gè)字符返回給你。舉個(gè)例子:

            wchar_t* Input=L”123vczh”;

            int Chars=GetInt(Input);

            這個(gè)時(shí)候Chars=3,而且Input已經(jīng)往后推了三個(gè)字符,指向了”vczh”

             

            于是經(jīng)過(guò)努力,我們就擁有了一些函數(shù):GetIntGetRealGetNameGetStringGetColonGetSpaceGetLineBreak。我們?nèi)绾问褂媚兀渴紫龋覀冊(cè)诿恳淮潍@得一個(gè)部分之前,我們都要調(diào)用GetSpace以過(guò)濾所有空格。然后就按如下順序調(diào)用上面的5個(gè)函數(shù):

            GetColon

            GetString

            GetName

            GetReal

            GetInt

            事實(shí)上只要GetIntGetReal之下就好了。因?yàn)槿绻?/span>123.456GetInt先吃掉了3個(gè)字符之后,剩下的就無(wú)法解釋了。

             

            如果全都失敗(函數(shù)返回0,代表什么都沒(méi)檢查到)了,那么我們可以GetLineBreak。如果再次失敗,那么證明這個(gè)輸入的腳本就有問(wèn)題了。那么報(bào)錯(cuò)吧。在示例代碼的Lexer.h/Lexer.cpp中有一個(gè)非常類(lèi)似的詞法分析器用于將一行代碼分段。

             

            讓我們把“  writeln Number "是質(zhì)數(shù)。"”分行吧。

             

            首先調(diào)用GetSpace,字符串指向了“writeln Number "是質(zhì)數(shù)。”,然后依次調(diào)用5個(gè)函數(shù)一直到GetName成功。GetName返回7,拿到了writeln,字符串指向了“” Number "是質(zhì)數(shù)。”。

            然后調(diào)用GetSpace,接著仍然到了GetName成功。GetName返回6,字符串指向了“"是質(zhì)數(shù)。”。

            接著調(diào)用GetSpace,調(diào)用到GetString的時(shí)候就成功了。GetString返回6(注意我們用的是wchar_t),字符串指向了“”。

            后面所有的調(diào)用都失敗了。我們意識(shí)到字符串已經(jīng)用完了,于是對(duì)這一行代碼的分析就到此為止了。

             

            到了這里,我們把所有的行都分割成一堆東西了。于是下面可以在采取一個(gè)步驟。我們首先辨別出哪一些是標(biāo)號(hào),哪一些是命令,然后填入上面的代碼中提到的vector<Command>map<wstring , size_t>中。如果我們遇到了一個(gè)標(biāo)號(hào),那么就將標(biāo)號(hào)名和命令表當(dāng)前存在的命令的數(shù)量加入標(biāo)號(hào)表,其余的都放進(jìn)命令表。于是我們?cè)?/span>goto的時(shí)候,就可以從標(biāo)號(hào)表中查到命令在命令表中的位置,從而成功跳轉(zhuǎn)了。

             

            對(duì)于上面那段檢查是否質(zhì)數(shù)的代碼,最終的分析結(jié)果如下:

            標(biāo)號(hào)表:

            LOOP_BEGIN: 05

            SUCCESS: 10

            FAIL:12

            命令表:

            00  write "請(qǐng)輸入一個(gè)數(shù)字:"

            01  read Number

            02  if Number less_then 2 goto FAIL

            03  if Number is 2 goto SUCCESS

            04  set Divisor 2

            05  if Number is Divisor goto SUCCESS

            06  mod Remainder Number Divisor

            07  if Remainder is 0 goto FAIL

            08  add Divisor Divisor 1

            09  goto LOOP_BEGIN

            10  writeln Number "是質(zhì)數(shù)。"

            11  exit

            12  writeln Number "不是質(zhì)數(shù)。"

             

            命令表里面有13個(gè)項(xiàng),每一個(gè)項(xiàng)都被分成了命令名和參數(shù)表兩個(gè)部分。執(zhí)行的時(shí)候可以通過(guò)命令名來(lái)做相應(yīng)的工作。讓我們來(lái)手工執(zhí)行一下這個(gè)代碼。

             

            執(zhí)行00,執(zhí)行01,我們輸入“5”。

            02條件失敗,03條件失敗,04設(shè)置變量Divisor2

            05條件失敗,06設(shè)置Remainder=5%2=107條件失敗,08 Divisor變成309跳轉(zhuǎn)到05LOOP_BEGIN)。

            05條件失敗,06設(shè)置Remainder=5%3=207條件失敗,08 Divisor變成409跳轉(zhuǎn)。

            05條件失敗,06設(shè)置Remainder=5%4=107條件失敗,08 Divisor變成509跳轉(zhuǎn)。

            05條件成功,跳轉(zhuǎn)到10SUCCESS)。

            10輸出“是質(zhì)數(shù)。”,11退出程序。

             

            于是現(xiàn)在剩下了最后一個(gè)問(wèn)題。writewritelnread原本是不存在于腳本引擎的。但是腳本引擎不具有輸入輸出的方法也是不行的,所以我們需要實(shí)現(xiàn)一個(gè)插件系統(tǒng)。這個(gè)插件系統(tǒng)可以讓我們?cè)谀_本引擎的外部添加命令。也就是說(shuō),我們構(gòu)造了一個(gè)腳本引擎,然后在外部創(chuàng)建一個(gè)插件,包含writewritelnread,然后連接他們。最后做一些手段讓腳本引擎在執(zhí)行到外部命令的時(shí)候?qū)⒖刂茩?quán)轉(zhuǎn)移給插件。

             

            在這里,我們可以使用責(zé)任鏈模式。腳本引擎在遇到一個(gè)不認(rèn)識(shí)的命令的時(shí)候,就訪問(wèn)第一個(gè)鏈接到腳本引擎的插件。這個(gè)時(shí)候插件可以返回三種結(jié)果:成功、失敗或者棄權(quán)。返回成功代表命令被成功執(zhí)行,腳本引擎繼續(xù)往下走。返回失敗代表指令被執(zhí)行了,但是執(zhí)行出錯(cuò),這個(gè)時(shí)候腳本引擎返回錯(cuò)誤信息并停止執(zhí)行。返回棄權(quán)代表這個(gè)插件不受理這個(gè)命令,腳本引擎將這個(gè)命令傳遞給下一個(gè)插件。如果所有的插件都棄權(quán)的話,那么腳本引擎將返回“無(wú)效命令”并停止執(zhí)行。

             

            所以插件只需要有一個(gè)函數(shù)就行了。這個(gè)函數(shù)返回執(zhí)行結(jié)果(成功、失敗或棄權(quán)),參數(shù)為當(dāng)前的命令以及運(yùn)行時(shí)環(huán)境(保存變量的地方)。腳本引擎使用一個(gè)vector去記錄所有鏈接的插件的指針,這樣的話腳本引擎在遇到不能解釋的命令的時(shí)候就可以依次訪問(wèn)插件了。下面是插件的示例代碼:

             

                 class Plugin

                 {

                 public:

                     virtual PluginStatus Execute(const Command& aCommand , Environment& aEnvironment , wstring& ErrorMessage)=0;

            };

            vector<Plugin*> FPlugins;

             

            命令腳本的東西就講到這里了。接下來(lái)的一些文章將講述如何處理高級(jí)語(yǔ)言,并且開(kāi)發(fā)一門(mén)新的語(yǔ)言出來(lái)。這門(mén)語(yǔ)言將只支持boolintdoublestring、數(shù)組和函數(shù)。

             

            點(diǎn)擊這里下載本片文章的示例代碼。

            代碼結(jié)構(gòu)如下:

            Lexer.h/Lexer.cpp:詞法分析器

            ScriptCommand.h/ScriptCommand.cpp:腳本引擎

            Main.cpp:主程序

            這個(gè)程序(SE_02.exe)讀取一個(gè)文本文件(SE_02.txt)并執(zhí)行,可以在debug文件夾下看到編譯結(jié)果。

            posted on 2008-07-09 21:43 陳梓瀚(vczh) 閱讀(8873) 評(píng)論(10)  編輯 收藏 引用 所屬分類(lèi): 腳本技術(shù)

            評(píng)論:
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-09 21:57 | Jetricy
            沙發(fā)  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-10 05:08 | 空明流轉(zhuǎn)
            讓我想起來(lái)一個(gè)曾經(jīng)經(jīng)常用到的工具,好像叫按鍵精靈的。那個(gè)腳本。。。  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-10 06:27 | 夢(mèng)在天涯
            en ,很好啊,看了這個(gè)就知道腳本的運(yùn)行原理了,java,.net也很類(lèi)似哦!



            寫(xiě)的非常的好,希望繼續(xù)啊!  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-10 17:59 | flybest
            不錯(cuò)
            學(xué)習(xí)  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-11 01:40 | 123
            入門(mén)的吧.  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-12 06:59 | 陳梓瀚(vczh)
            當(dāng)然  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-16 18:54 | pdkui
            int LexerChars(const wchar_t*& Input , const wchar_t* Chars)
            {
            const wchar_t* Temp=Input;
            while(*Chars)
            {
            if(*Input++!=*Chars++)return 0;
            }
            Input=Temp;
            return (int)(Chars-Input);
            }
            //最后的return 邏輯:
            //Chars和Input必須在一個(gè)長(zhǎng)字符串內(nèi),才能這樣做減法  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2008-07-17 01:49 | 陳梓瀚(vczh)
            哦,那應(yīng)該是bug。  回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本[未登錄](méi) 2008-10-22 19:11 | Kevin Lynx
            雖然以前知道你發(fā)的這些文章,但是很少看過(guò),理由很簡(jiǎn)單,我覺(jué)得要用一些閑暇時(shí)間去看你的文章,是不夠的。

            今天終于看完了你這個(gè)系列的第二篇,并且看了代碼。大致上算理解了你這篇文章講的東西。感覺(jué)就是,設(shè)計(jì)和代碼都很老練。
              回復(fù)  更多評(píng)論
              
            # re: 手把手教你寫(xiě)腳本引擎(二)——命令腳本 2010-01-11 20:36 | kuafoo
            跟著牛人學(xué)習(xí)寫(xiě)腳本了  回復(fù)  更多評(píng)論
              
            怡红院日本一道日本久久 | 亚洲中文字幕无码一久久区| 日韩欧美亚洲综合久久影院d3| 久久久久免费精品国产| 亚洲欧洲久久久精品| 国産精品久久久久久久| 亚洲国产成人久久精品动漫| 精品久久久久久久久中文字幕| 久久精品蜜芽亚洲国产AV| 久久综合给合久久狠狠狠97色69| 久久99久国产麻精品66| 久久99精品久久久大学生| 久久国产色av免费看| A级毛片无码久久精品免费| 亚洲精品国精品久久99热一| 亚洲综合日韩久久成人AV| 久久久久人妻精品一区| 激情伊人五月天久久综合| 久久99热国产这有精品| 大香网伊人久久综合网2020| 国产亚洲色婷婷久久99精品91| 久久亚洲高清综合| 国产精品成人久久久| 久久精品国产AV一区二区三区| 亚洲va久久久噜噜噜久久狠狠| 久久成人国产精品| 久久综合综合久久97色| 久久久精品国产亚洲成人满18免费网站| 精品欧美一区二区三区久久久| 午夜精品久久久久久久无码| 久久午夜无码鲁丝片秋霞| 久久综合给久久狠狠97色| 亚洲国产精品一区二区久久| 久久久久亚洲爆乳少妇无| 久久久久久精品免费免费自慰| 久久精品黄AA片一区二区三区| 香蕉久久夜色精品国产小说| 无码国内精品久久人妻麻豆按摩| 无码人妻久久一区二区三区| 99久久精品国产综合一区| 伊人久久一区二区三区无码|