前面,我們簡單描述了Abstract Form的基本組成。現在,我們來看看如何利用Abstract Form動態產生和修改module。
在介紹Erlang Abstract Form--產生和擷取,已經提到過,要獲得Abstract Form有兩種方法,一種讀取beam檔案中的debug_info,另一種方法就是直接解析原始碼。
提供原始碼文本
修改一個module最有用的功能是增加新的函數。我們從beam檔案可以擷取現有模組的Abstract Form,但是如果需要動態增加方法,最容易想到的就是提供函數的原始碼文本。
解析原始碼通常需要兩個工具,即掃描器和解析器。Erlang提供的基本掃描器是erl_scan,解析器為erl_parse。我們看看它們的文檔。
MODULE
erl_scan
MODULE SUMMARY
The Erlang Token Scanner
DESCRIPTION
This module contains functions for tokenizing characters into Erlang tokens.
EXPORTS
string(CharList,StartLine]) -> {ok, Tokens, EndLine} | Error
string(CharList) -> {ok, Tokens, EndLine} | Error
Types:
CharList = string()
StartLine = EndLine = Line = integer()
Tokens = [{atom(),Line}|{atom(),Line,term()}]
Error = {error, ErrorInfo, EndLine}
Takes the list of characters CharList and tries to scan (tokenize) them. Returns {ok, Tokens, EndLine}, where Tokens are the Erlang tokens from CharList. EndLine is the last line where a token was found.
StartLine indicates the initial line when scanning starts. string/1 is equivalent to string(CharList,1).
{error, ErrorInfo, EndLine} is returned if an error occurs. EndLine indicates where the error occurred.
erl_scan:string方法掃描字串文本,如果沒有發生錯誤,則在結果tuple中返回所有的token,不然返回錯誤的行號。
Eshell V5.5 (abort with ^G)
1> c(simplest, [debug_info]).
{ok,simplest}
2> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
3>
我們傳入一個最簡單函數test,erl_scan:string掃描的結果返回Tokens,其中包含5個token。分別是atom類型的函數名test、左括弧、右括弧、函數頭的結束->、atom類型的atom ok,以及最後結束的dot。
有了這個 Tokens,我們可以用erl_parse解析產生Abstract Form。erl_parse的文檔中說:
MODULE
erl_parse
MODULE SUMMARY
The Erlang Parser
DESCRIPTION
This module is the basic Erlang parser which converts tokens into the abstract form of either forms (i.e., top-level constructs), expressions, or terms. The Abstract Format is described in the ERTS User's Guide. Note that a token list must end with the dot token in order to be acceptable to the parse functions (see erl_scan).
EXPORTS
parse_form(Tokens) -> {ok, AbsForm} | {error, ErrorInfo}
Types:
Tokens = [Token]
Token = {Tag,Line} | {Tag,Line,term()}
Tag = atom()
AbsForm = term()
ErrorInfo = see section Error Information below.
This function parses Tokens as if it were a form. It returns:
{ok, AbsForm}
The parsing was successful. AbsForm is the abstract form of the parsed form.
{error, ErrorInfo}
An error occurred.
erl_parse可以解析很多種token,包括運算式、term、Form等等,我們需要的是完全解析Form的函數parse_form。同樣,如果解析成功,那麼返回的tuple中將包含tokens代表的Abstract Form,不然返回語法錯誤資訊。
3> erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}
我們可以看到,返回結果中包含了函數的Abtract Form。
編譯Abstract Form
有了Abstract Form以後,我們就可以編譯,並載入它。
compile模組的文檔中說:
forms(Forms)
Is the same as forms(File, [verbose,report_errors,report_warnings]).
forms(Forms, Options) -> CompRet
Types:
Forms = [Form]
CompRet = BinRet | ErrRet
BinRet = {ok,ModuleName,BinaryOrCode} | {ok,ModuleName,BinaryOrCode,Warnings}
BinaryOrCode = binary() | term() ErrRet = error | {error,Errors,Warnings}
Analogous to file/1, but takes a list of forms (in the Erlang abstract format representation) as first argument. The option binary is implicit; i.e., no object code file is produced. Options that would ordinarily produce a listing file, such as 'E', will instead cause the internal format for that compiler pass (an Erlang term; usually not a binary) to be returned instead of a binary.
compile:forms這個函數取要編譯的Form作為參數,把它編譯成可以被虛擬機器執行的二進位對象碼資料。它和compile:file基本一樣,只不過提供的是Form參數,而compile:file是需要編譯的檔案名稱。
實際上,compile:file這個方法我們一直都在用。erl的shell中,輸入c(File, options)就是在編譯檔案:
c(File) -> {ok, Module} | error
c(File, Options) -> {ok, Module} | error
Types:
File = Filename | Module
Filename = string() | atom()
Options = [Opt] -- see compile:file/2
Module = atom()
c/1,2 compiles and then purges and loads the code for a file. Options defaults to []. Compilation is equivalent to:
compile:file(File, Options ++ [report_errors, report_warnings])
Note that purging the code means that any processes lingering in old code for the module are killed without warning. See code/3 for more information.
c調用compile的file方法編譯.erl檔案,然後從記憶體中移去原先存在的代碼,然後載入新的代碼。
要編譯Abstract Form,我們必須提供整個module完整的Form。因此,我們需要提供module屬性、export屬性等等。
1> c(simplest,[debug_info]).
{ok,simplest}
2> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
3> {ok, Forms} = erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}
4>
4> NewForms = [{attribute, 1, module, simplest},{attribute, 2, export, [{test,0}]}, Forms].
[{attribute,1,module,simplest},
{attribute,2,export,[{test,0}]},
{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}]
5> compile:forms(NewForms).
{ok,simplest,
<<70,79,82,49,0,0,1,188,66,69,65,77,65,116,111,109,0,0,0,55,0,0,0,6,7,...>>}
載入新編譯的對象碼
閱讀上面文檔的另外一個好處就是我們知道編譯以後如何載入。code模組定義了這些函數:
purge(Module) -> true | false
Types:
Module = atom()
Purges the code for Module, that is, removes code marked as old. If some processes still linger in the old code, these processes are killed before the code is removed.
Returns true if successful and any process needed to be killed, otherwise false.
purge函數把現有的代碼移去並標記為老版本,如果有任何process在使用舊代碼,那麼這些process將被殺死。注意儘管purge總是成功的,但是它的傳回值只有在任何process需要被殺死的情況下才會返回true.
load_binary(Module, Filename, Binary) -> {module, Module} | {error, What}
Types:
Module = atom()
Filename = string()
What = sticky_directory | badarg | term()
This function can be used to load object code on remote Erlang nodes. It can also be used to load object code where the file name and module name differ. This, however, is a very unusual situation and not recommended. The parameter Binary must contain object code for Module. Filename is only used by the code server to keep a record of from which file the object code for Module comes. Accordingly, Filename is not opened and read by the code server.
Returns {module, Module} if successful, or {error, sticky_directory} if the object code resides in a sticky directory, or {error, badarg} if any argument is invalid. Also if the loading fails, an error tuple is returned. See erlang:load_module/2 for possible values of What.
load_binary載入編譯好的對象碼,從而使得Module可以被程式使用。如果對象代碼存在於sticky目錄下的話,可能無法成功替換。sticky目錄是erlang自己的運行時系統,包括kernel、stdlib和compiler,為了保證erlang的運行正常,預設情況下這些目錄是受保護的,被認為是sticky的。
組合起來
利用我們前面討論過的內容,我們可以進行完整的實驗.
假設現在有以下程式simplest.erl:
-module(simplest).
-export([foo/0]).
foo() ->
io:format("foo~n").
我們用erl一步一步進行實驗。
1> c(simplest,[debug_info]).
{ok,simplest}
2> simplest:foo().
foo
ok
3> simplest:test().
=ERROR REPORT==== 18-Aug-2006::15:06:17 ===
Error in process <0.32.0> with exit value: {undef,[{simplest,test,[]},{erl_eval,do_apply,5},{shell,exprs,6},{shell,eval_loop,3}]}
** exited: {undef,[{simplest,test,[]},
{erl_eval,do_apply,5},
{shell,exprs,6},
{shell,eval_loop,3}]} **
4> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
5> {ok, Forms} = erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}
6> NewForms = [{attribute, 1, module, simplest},{attribute, 2, export, [{test,0}]}, Forms].
[{attribute,1,module,simplest},
{attribute,2,export,[{test,0}]},
{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}]
7> {ok,simplest,Binary} = compile:forms(NewForms).
{ok,simplest,
<<70,79,82,49,0,0,1,188,66,69,65,77,65,116,111,109,0,0,0,56,0,0,0,6,8,...>>}
8> code:purge(simplest).
false
9> code:load_binary(simplest,"simplest.erl",Binary).
{module,simplest}
10> simplest:foo().
=ERROR REPORT==== 18-Aug-2006::15:08:13 ===
Error in process <0.40.0> with exit value: {undef,[{simplest,foo,[]},{erl_eval,do_apply,5},{shell,exprs,6},{shell,eval_loop,3}]}
** exited: {undef,[{simplest,foo,[]},
{erl_eval,do_apply,5},
{shell,exprs,6},
{shell,eval_loop,3}]} **
11> simplest:test().
ok
12>
結果是新加入了一個export的test/0函數,原先的foo/0函數沒有了。
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/minyskirt/archive/2009/12/13/4996838.aspx