程序匠

星期二, 八月 29, 2006

“非工业主流”编程语言

所以为什么是“非工业主流”编程语言。

我不知道如何下一个严格的定义。简单地说,就是不被绝大多数程序员所使用的编程语言。

看看tiobe的语言排行榜,你可以了解我在讲什么。Java、C、VB、C++和PHP占据了70%的份额,它们是当之无愧的工业主流语言。而 Ruby尽管连连升级,排名13位,份额也不过是0.804%,Lisp/Scheme连连下挫,目前仅为0.586%,如果你仔细寻找,在The Next 50 Programming Languages的标题下,Erlang,Lua,Scala缩在角落里,这些“可怜”的语言当然是非主流的。自然,你不不太可能认为PL/SQL 、 Visual FoxPro、 VB.NET和Lisp、Ruby、Erlang、Lua是同类,我也这样想。

或许你和我曾经或正在感到非常振奋,那些你我日常的编程语言高居前列,并引以为豪。但是事情并不是完全象我们想像的一样。

编写程序需要乐趣,很难说工业主流语言能够提供你更多的乐趣。我所知道的很多程序员在白天忙乎完手上的Java,C++工作后,晚上会带着一种神秘的快感摸索一些可能自己一辈子也不会用于谋生的语言。

当然,这可能是厌倦造成的,但是当你发现一个苦思冥想、或者需要n多语言规则、框架、n多所谓的高深理论解决的问题,在另外一种语言中是最最简单的一个特性,恐怕这种懊恼的感觉不是可以轻易描述的。譬如,当你天天为C/C++的内存释放绞尽脑汁的时候,当你为垃圾收集在Java的出现而欢呼的时候,你是否知道30年前,那已经是Lisp的一个标准构造了。当你天天面对着无穷无尽的并发要求,纠缠不清的哲学家吃面头皮发麻的时候,你可能很想知道Erlang 20年前就让极大规模的并发和可靠性处理变成小事一桩。

编写程序还需要创造价值,一个非凡的产品在获得巨大利润的同时,更会带来一种心底而生的自豪感。如果要担心工作的问题,那么主流语言是你必不可少的谋生工具。但是如果你从头建立一个公司,希望用有限的资源和人力制造出强有力的产品,一个与众不同的产品,那么你需要秘密武器,这些武器是什么呢?

当然可以有很多,但其中最有杀伤力的武器之一无疑是编程语言--高生产力,适合某一领域的非工业主流语言。这种例子并不罕见,例如:
也许,你喜爱的语言被成千上万的人使用并不是那么令人自豪的事情;自私一点地说,缺乏同伴或许能够带来更多的乐趣和财富

编写可靠软件-面对不可避免的错误

学习一种新语言的目的是什么?

最重要的是能够从新的语言中吸收到新的营养,从不同的角度去理解我们所碰到的问题,以及新的解决问题方式。至少不至于坐井观天。

或许你认为理想上经过严格测试的软件是不应该有Bug的,而当Bug作恶的时候你会产生罪恶感,你觉得总有那么一条道路可以通向完美的零缺陷之路,你自责、困扰。但你是否曾经反过来想想,这种理想是永远达不到的,但是如果我们承认系统、软件是包含错误的,我们是不是无能为力了?

这两年来我做的一些程序和以往的软件相比有一点很大的不同,那就是经常需要和硬件打交道。我们经常碰到的一个问题是在很多情况下,譬如温度过高、或者运行时间过长、或者磁盘读写、删除次数太多的情况下,DSP会暂时停止运行,或者磁盘会损坏。这个时候,你能干什么?假装这些事情不会发生?

有些时候也并不完全是硬件的问题,在如今高度紧张的竞争环境下,我们的客户需求变更完成时间通常以天或者周为单位,如此的速度去响应你的客户,你能保证你的代码没有任何问题?

没有人能够保证,但是这个时候,你还必须做到这样的系统是能够可靠运行的,就拿我们的其中一个软件来说,虽然本身价值不高,但是它可能牵涉到大量的金钱、甚至是人的生命。

为了解决这些问题,在某些关键的位置,工程人员必须在机器上插入硬件狗,在机器停止响应的时候自动重新启动机器。但是某些时候机器本身是没有问题的,而是系统或者软件的问题,例如DSP因长时间运行停止运作,或者程序本身产生错误,所以我们需要软件狗,一个守护进程,监视系统的运行状态,并在出现异常的情况下重启系统。

然而,这样还是不够的,某些时候,整个系统的运行是正常的,只不过某些模块会出现问题,因此,我们进一步把整个系统划分为几个重要的模块,单独的进程运行这些模块,一个比较完备的守护进程管理这些模块之间的依赖关系、它们的启动、状态监视以及出现异常的情况下按照某种顺序重新启动。

这样的需求和实现出现在我们一个小小的产品中,但很快发展到我们所有的软件中。例如,视频编解码通常需要大量的CPU,而在CPU非常紧张的情况下或者某些时候编码器网络传输的流数据不正常,解码器通常会出现异常的情况,轻则停止解码,重则退出系统。而在监控大量视频图像的情况下,工作人员或许根本无法注意到哪一路图像出现异常。所以,客户的要求是不管因为网络、编解码软件或者其他什么原因,必须保证24小时永不停止的图像显示(如果能够自动恢复的话),在我们流媒体、集中存储系统中也存在着同样的需求。

因此,我们必须假设系统、程序是包含错误的,在这样的情况下我们如何编写可靠的软件?

我们用自己的守护系统来应对这个问题,但是并不是那么系统、完备和彻底。

而这也是Erlang要解决的问题,通过编程语言、通过库、通过良好的系统原则来构造可靠的强壮的系统,如果我能够早一年看到,或许就会少走许多弯路,能走得更好。

星期五, 八月 18, 2006

探索 Erlang Abstract Form--动态生成和修改module

上一篇 我们简单描述了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函数没有了。

下一步

当然,我们的目的不是完全覆盖原先的module,而是能够增加、删除和替换原先的函数。因此,我们需要通过前面的beam_lib:chunks方法读取并保存老的Form,然后把根据不同的操作,加入、替换新函数的Form,然后一起编译。
这就是Smerl所做的事情。

星期四, 八月 17, 2006

探索 Erlang Abstract Form--Module声明和Form

我们将具体考察Erlang Abstract Form的组成。方法很简单,参考Eralng文档的Abstract Form一节,用实际的例子加以验证。

Abstract Form


Abstract Form文档中,用函数Rep表示从Erlang源代码C到abstract form形式R的映射。简单地说,如果源代码C解析成为Abstract Form R,那么写成R = Rep(C)。
另外文档中LINE表示源代码的行号。
下面是module声明的描述:

A module declaration consists of a sequence of forms that are either function declarations or attributes.

  • If D is a module declaration consisting of the forms F_1, ..., F_k, then Rep(D) = [Rep(F_1), ..., Rep(F_k)].
  • If F is an attribute -module(Mod), then Rep(F) = {attribute,LINE,module,Mod}.
  • If F is an attribute -export([Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) = {attribute,LINE,export,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}.
  • If F is an attribute -import(Mod,[Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) = {attribute,LINE,import,{Mod,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}}.
  • If F is an attribute -compile(Options), then Rep(F) = {attribute,LINE,compile,Options}.
  • If F is an attribute -file(File,Line), then Rep(F) = {attribute,LINE,file,{File,Line}}.
  • If F is a record declaration -record(Name,{V_1, ..., V_k}), then Rep(F) = {attribute,LINE,record,{Name,[Rep(V_1), ..., Rep(V_k)]}}. For Rep(V), see below.
  • If F is a wild attribute -A(T), then Rep(F) = {attribute,LINE,A,T}.
  • If F is a function declaration Name Fc_1 ; ... ; Name Fc_k, where each Fc_i is a function clause with a pattern sequence of the same length Arity, then Rep(F) = {function,LINE,Name,Arity,[Rep(Fc_1), ...,Rep(Fc_k)]}.
模块声明由一系列Form组成,这些Form要么是函数声明,要么是属性(attribute)。

Simplest

考察我们最简单的模块 simplest。

-module(simplest).

我们对它进行编译,然后获取它的abstract_code:

Eshell V5.5 (abort with ^G)
1> c(simplest,[debug_info]).
{ok,simplest}
2> beam_lib:chunks(simplest, [abstract_code]).
{ok,{simplest,[{abstract_code,{raw_abstract_v1,
[{attribute,1,file,{"./simplest.erl",1}},
{attribute,1,module,simplest},
{eof,1}]}}]}}


beam_lib:chunks返回的abstract_code定义如下:
{ChunkName, DataT} =
{abstract_code, AbstractCode}

AbstractCode = {AbstVersion, Forms} | no_abstract_code
AbstVersion = atom()
如果无法在beam文件中找到abstract form,那么将返回no_abstract_code。如果找到的话,则是一个tuple, tuple的第一项是版本,即我们上面例子中的raw_abstract_v1,tuple的第2项就是真正的form。因此,最简单的simplest beam文件中包含的Form如下:
  [{attribute,1,file,{"./simplest.erl",1}},
{attribute,1,module,simplest},
{eof,1}]
Abstract Form关于module声明Form的第一条说:
If D is a module declaration consisting of the forms F_1, ..., F_k, then Rep(D) = [Rep(F_1), ..., Rep(F_k)].
这可以解释Form为什么是一个列表。
If F is an attribute -module(Mod), then Rep(F) = {attribute,LINE,module,Mod}.

因此,例子中Form的第2行是:
 {attribute,1,module,simplest}
还有:
If F is an attribute -file(File,Line), then Rep(F) = {attribute,LINE,file,{File,Line}}.
这也说明了为什么会出现:
 {attribute,1,file,{"./simplest.erl",1}}
尽管我们没有在源代码中编写-file属性,但是编译器还是在abstract code中加入了这个属性。

最后,由于文件在第一行结束,因此还包含
 {eof,1}


这是在Abstract Form文档的其中一节提到的:

4.1.2 Representation of parse errors and end of file

In addition to the representations of forms, the list that represents a module declaration (as returned by functions in erl_parse and epp) may contain tuples {error,E}, denoting syntactically incorrect forms, and {eof,LINE}, denoting an end of stream encountered before a complete form had been parsed.

加入一个方法


接下去,我们在simplest.erl加入一个新的函数test,并export:

-module(simplest). %1
-export([test/0]). %2
test() -> %3
ok. %4


重新编译simplest,并获取abstract code 如下:

5> c(simplest,[debug_info]).
{ok,simplest}
6> beam_lib:chunks(simplest, [abstract_code]).
{ok,{simplest,[{abstract_code,{raw_abstract_v1,
[{attribute,1,file,{"./simplest.erl",1}},
{attribute,1,module,simplest},
{attribute,2,export,[{test,0}]},
{function,
3,
test,
0,
[{clause,3,[],[],[{atom,4|...}]}]},
{eof,5}]}}]}}


首先,我们看到新增加了export属性,出现在代码的第2行,其中包括0个参数的test这个tuple。最主要的变化是一个新的function Form:

If F is a function declaration Name Fc_1 ; ... ; Name Fc_k, where each Fc_i is a function clause with a pattern sequence of the same length Arity, then Rep(F) = {function,LINE,Name,Arity,[Rep(Fc_1), ...,Rep(Fc_k)]}.
function的Form共有5项,第一项是function这个atom,第二项是行号,第3项是函数的名字,第4项是函数参数的个数。最后一项是一个列表,包含每个子句的Rep。

我们可以深入到function的每一个子句中去,但是探索Erlang Abstract Form的目的是为了能够理解metaprogramming的原理。而在实际编程时,很少有人会用Form来动态生成一个新的函数。通常使用的方法是提供一个函数的源代码,或者直接使用函数参数,关于直接对Form更详细的操纵,我们放到后面再说。

我将在下一篇中描述如何把源代码解析为Form,编译Form并动态加载成功的二进制数据,从而实现运行时刻的metaprogramming能力。

MacBook(Intel)上的高性能Erlang

经过长长的邮件讨论 ,终于可以让我的MacBook成功编译HiPE。感谢Mikael Pettersson ,Joel Reymont还有其它参与讨论的各位。今天Mikael Pettersson在erlang-questions发布了这一消息

Completed. Download the following URLs:
- otp-0804.tar.gz
- tests-0804.tar.gz
- patch-otp-0804-1-x86-aligned-c-stack

otp-0804 is a current snapshot from the branch that will become R11B-1 fairly soon, tests-0804 is the corresponding snapshot of the HiPE test suite, and patch-otp-0804-1-x86-aligned-c-stack is the patch to make the runtime system maintain a 16-byte aligned C stack on x86-32.

After unpacking otp-0804 and applying the patch above, you should update your R11B-0 patches to apply to this code base. Do it in separate steps for separate issues, something like:
- support for OSX/x86-32 floating-point exceptions
- changes to handle OSX/x86-32 assembly syntax
- changes for sigaction() override on OSX/x86-32
- anything else

This should result in a set of patches to be applied in a specific order. Then make them publicly available and we'll start reviewing them.

Why not do this against R11B-0? There are already enough changes to the runtime system that patches against R11B-0 don't automatically apply to or work in the current snapshots. Since R11B-1 isn't very far away, it seems more appropriate to develop for that; you can always backport the changes to R11B-0 if necessary.


看起来R11B-1发布的日子也指日可待了。

虽然没有参与多久,这段日子确实让我感觉到了Erlang社区的热情、互助和力量。

干杯!为我的MacBook和这个社区。

探索 Erlang Abstract Form--生成和获取

Smerl通过修改Erlang的内部解析树,并重新编译这棵解析树实现metaprogramming。为了理解Smerl,我们首先需要理解Erlang内部解析树的生成、表达形式和获取、修改的方法。

Erlang把解析树称为Abstract Form,要获得某一个模块的AbstractForm有两种方法:
  • 从已经编译的beam文件中获取Abstract Form
  • 直接解析源代码生成Abstract Form
在一个实际运行的程序中,我们可能无法存取源代码,即使能够获得源代码,它也不一定完全和正在运行的beam文件同步。因此,让我们首先把精力放在第一种方法上,第二种方法将在后面需要的时候描述。实际上两种方法只是在获得AbstractForm的方法上有所不同,而对Abstract Form的理解和操纵是完全一样的。

beam_lib 提供了操作beam文件所需要的接口。Erlang的beam文件格式是 "EA IFF 1985"标准的一个变种,它把数据分为多个 chunks. Chunk数据可以是二进制或者复合的term。如果通过名字(erlang的atom)去引用chunk,那么将返回复合term,当然这是我们需要的。

获取chunk的函数是beam_lib:chunks,例如

beam_lib:chunks(Beam,[abstract_code])



将返回Beam变量所指定的beam文件中包含的abstract_code,也就是我们需要的Abstract Form。当然,除了abstract_code以外,chunks函数还可以用来获得以下的各种调试信息:

  • abstract_code ("Abst")
  • attributes ("Attr")
  • compile_info ("CInf")
  • exports ("ExpT")
  • labeled_exports ("ExpT")
  • imports ("ImpT")
  • indexed_imports ("ImpT")
  • locals ("LocT")
  • labeled_locals ("LocT")
  • atoms ("Atom")
这里有两点需要注意。
首先,我们前面说用atom去引用将获得复合term,如果用字符串去引用,chunks函数返回二进制数据。上面的列表中,每一项你都可以用两种方法去获取(括号外和括号内)。例如可以用abstract_code这个atom去获得复合term,也可以用括号中的"Abst"去获取二进制数据。

另外,我们说chunks函数用来获得beam文件中的调试信息。这意味着我们必须在编译的时候使用调试选项。

Erlang的compile可以用debug_info选项:
debug_info
Include debug information in the form of abstract code (see The Abstract Format in ERTS User's Guide) in the compiled beam module. Tools such as Debugger, Xref and Cover require the debug information to be included.
Warning: Source code can be reconstructed from the debug information. Use encrypted debug information (see below) to prevent this.
See beam_lib(3) for details.

如果使用debug_info选项,那么编译得到的beam文件内部将以abstract code的形式保存调试信息。但是警告也说明,如果使用该选项,那么其他人就可以从这些信息重建源代码。好在compile还提供了一种加密方法,你在编译的时候提供一个key,那么这些调试信息必须在提供同样key的时候才能解密获得。

现在,我们尝试一个最简单的模块来验证上面的理解。创建一个空的module simplest,内容如下:

module(simplest).

%%
%% Include files
%%

%%
%% Exported Functions
%%
-export([]).


首先,我们没有使用debug_info选项编译:

erl
Erlang (BEAM) emulator version 5.5 [source] [async-threads:0]

Eshell V5.5 (abort with ^G)
1> c(simplest).
{ok,simplest}
2> beam_lib:chunks(simplest,[abstract_code]).
{ok,{simplest,[{abstract_code,no_abstract_code}]}}
3>


shell返回的结果中no_abstract_code表示simplest这个beam文件中并没有包含任何abstract code。接着我们用debug_info选项重新编译:

3> c(simplest,[debug_info]).
{ok,simplest}
4> beam_lib:chunks(simplest,[abstract_code]).
{ok,{simplest,[{abstract_code,{raw_abstract_v1,
[{attribute,1,file,{"./simplest.erl",1}},
{attribute,4,module,simplest},
{attribute,13,export,[]},
{eof,25}]}}]}}


这一次,返回了一个复合的term,也就是我们需要的Abstract Form。

我们先探索到这里,下次,我们将详细分析Erlang Abstract Form的所有组成部分。

星期三, 八月 16, 2006

Erlang meta programming

我昨天在 AgileTao 说过,如果Erlang具备Ruby的meta programming能力,能够有Java第三方库的1/10,那么它将是非常恐怖的。

今天Yariv Sadan发布了Smerl,真是意外之喜。

实际上Erlang和Common Lisp本质上都在处理列表。Lisp中,S-Expression的基本组成除了Atom就是List。而在Erlang中,分为Constant和Compound两种类型。如果不考虑Erlang为了处理并发提供的一些特殊类型,Constant类型基本上对应Lisp的Atom。而Erlang的复合类型可分为List和Tuple两种。其中Tuple可以看作是固定成员数目的List。因此,我们完全应该预期,Erlang可能具备和Common Lisp的宏一样强大的meta programming能力。

可惜,不象Common Lisp的macro成为这种语言的标志之一,Erlang把它的这种能力深深地隐藏起来了。然而,Yariv Sadan用Smerl初步展示了Erlang的meta programming能力是非常强大的,Java自然无可比拟,连Ruby都可能为之倾倒。

当然,Ruby的meta programming能力和它灵活多变的语法结合产生的能力,更可能为普通的程序员所接受。Lisp的macro虽然更加强大,但是确实更加难以编写和维护。和Lisp的纯S-Expression相比,Erlang的语法稍微要复杂一些,而如果我们能够充分挖掘它的meta programming能力,那么或许会引起更多人的兴趣。

无论如何,这是非常有意思的一步。