说起元编程,lisp的抽象能力无疑是最强的,独特的S-expression和macro,简直是居家旅行,杀人必备之神器= =

其实erlang的元编程能力也不弱。让我们一切先从smerl开始,慢慢了解erlang的meta programming

smerl是erlyweb项目中内部使用的一个模块,它可以让我们很容易的动态创建编译模块,动态添加function等等。

首先我们来热下身,先做个小功能,动态创建一个叫hello_world的模块,往中间添加一个echo函数,打印出"hello, world!"

-module(test).
-compile(export_all).

test_hello_world() ->
    M = smerl:new('hello_world'),
    {ok, M2} = smerl:add_func(M, "echo() -> io:format(\"hello, world!\")."),
    smerl:compile(M2).

 

我们将test模块编译,然后在命令行中运行hello_world:echo().可以顺利打印出"hello, world!"

 

接下来,我们来分析一下smerl的源码,上面那段代码是怎样完成我们的需求的

首先我们来看一看smerl:new/1:

%% @type meta_mod(). A data structure holding the abstract representation
%%  for a module.
%% @type func_form(). The abstract form for the function, as described
%%    in the ERTS Users' manual.

%% The record type holding the abstract representation for a module.
-record(meta_mod, {module, file, exports = [], forms = [],
           export_all = false}).

%% @doc Create a new meta_mod for a module with the given name.
%%
%% @spec new(Module::atom()) -> meta_mod()
new(ModuleName) when is_atom(ModuleName) ->   %填充meta_mod的module name
    #meta_mod{module = ModuleName}.

 

这段其实很简单啦, 就是填充一个元模块的record,成为smerl操作的上下文。

接下来,我们看一下,往模块中添加函数的过程。

%% @doc Add a new function to the meta_mod and return the resulting meta_mod.
%% This function calls add_func(MetaMod, Form, true).
%%
%% @spec add_func(MetaMod::meta_mod(), Form::func_form() | string()) ->
%%   {ok, NewMod::meta_mod()} | {error, parse_error}
add_func(MetaMod, Form) ->
    add_func(MetaMod, Form, true).

%% @doc Add a new function to the meta_mod and return the new MetaMod
%% record. Export is a boolean variable indicating if the function should
%% be added to the module's exports.
%%
%% @spec add_func(MetaMod::meta_mod(), Func::func_form() | string(),
%%   Export::boolean()) ->
%%   {ok, NewMod::meta_mod()} | {error, parse_error}
add_func(MetaMod, Func, Export) when is_list(Func) ->
    case parse_func_string(Func) of
    {ok, Form} ->
        add_func(MetaMod, Form, Export);
    Err ->
        Err
    end;
add_func(MetaMod, {function, _Line, FuncName, Arity, _Clauses} = Form,
     true) ->
    Foo = {ok, MetaMod#meta_mod{
      exports = [{FuncName, Arity} | MetaMod#meta_mod.exports],
      forms = [Form | MetaMod#meta_mod.forms]
     }},
    Foo;
add_func(MetaMod, {function, _Line, _FuncName, _Arity, _Clauses} = Form,
     false) ->
   {ok, MetaMod#meta_mod{forms = [Form | MetaMod#meta_mod.forms]}};

%%add_func(MetaMod, Name, Fun) when is_function(Fun) ->
%%    add_func(MetaMod, Name, Fun, true);

add_func(_, _, _) ->
    {error, parse_error}.





parse_func_string(Func) ->
    case erl_scan:string(Func) of
    {ok, Toks, _} ->
        case erl_parse:parse_form(Toks) of
        {ok, _Form} = Res ->
            Res;
        _Err ->
            {error, parse_error}
        end;
    _Err ->
        {error, parse_error}
    end.

上面的代码其实很简单,最核心的部分,在于parse_func_string/1函数,他利用erl_scan:string/1将一段字符串转换成了tokens,

接下来用erl_parse:parse_form/1将这段tokens解析成ASF

(就是erlang的erlang 的Abstract Format,关于ASF,大家可以移步erlang的文档http://www.erlang.org/doc/apps/erts/absform.html,这里就不冗余介绍)

最后将这段ASF填充入meta_mod这个record的forms段中。其实请注意到现在为止,我们也只是对meta_mod这个上下文数据进行了修改。

接下来,我们来看看编译过程smerl:compile/1,2:

%% @doc Compile the module represented by the meta_mod and load the
%% resulting BEAM into the emulator. This function calls
%% compile(MetaMod, [report_errors, report_warnings]).
%%
%% @spec compile(MetaMod::meta_mod()) -> ok | {error, Error}
compile(MetaMod) ->
    compile(MetaMod, undefined).

%% @doc Compile the module represented by the meta_mod and load the
%% resulting BEAM into the emulator. 'Options' is a list of options as
%% described in the 'compile' module in the Erlang documentation.
%%
%% If the 'outdir' option is provided,
%% the .beam file is written to the destination directory.
%%
%% @spec compile(MetaMod::meta_mod(), Options::[term()]) -> ok | {error, Error}
compile(MetaMod, undefined) ->
    compile(MetaMod, [report_errors, report_warnings,
              return_errors]);

compile(MetaMod, Options) ->  %根据传入的meta_mod,和编译选项进行编译
    Forms = [{attribute, 2, module, MetaMod#meta_mod.module},   %添加-module属性的asf。
         {attribute, 3, export, get_exports(MetaMod)}],     %以及-export属性的asf
    FileName =
    case MetaMod#meta_mod.file of                           
        undefined -> atom_to_list(get_module(MetaMod));
        Val -> Val
    end,

    Forms1 = [{attribute, 1, file, {FileName, 1}} | Forms],     %添加-file属性的asf
    Forms2 = Forms1 ++ lists:reverse(MetaMod#meta_mod.forms),   %添加元模块结构中的相关其他asf。
    
    case compile:forms(Forms2, Options) of                      %进行编译
    {ok, Module, Bin} ->
        Res = 
        case lists:keysearch(outdir, 1, Options) of
            {value, {outdir, OutDir}} ->                 %根据编译选项确定是否输出beam文件
            file:write_file(
              OutDir ++
              ['/' | atom_to_list(MetaMod#meta_mod.module)] ++
              ".beam", Bin);
            false -> ok
        end,
        case Res of
        ok ->
            code:purge(Module),                                    %清理旧代码
            case code:load_binary(                                 %载入新编译的模块
               Module,
               atom_to_list(Module) ++ ".erl", Bin) of
            {module, _Module} ->
                ok;
            Err ->
                Err
            end;
        Err ->
            Err
        end;
    Err ->
        Err
    end.

 

我在上面的代码里加入了必要的注释,到现在为止,大家可以看到,我们动态修改模块,其实本质上,利用的是ASF。

erl_scan和erl_parse相关模块可以帮助我们将一段字符串转变为所需要的ASF

而在编译时,compile模块可以通过这个ASF中间层,顺利得到编译后可以运行的字节码。

 

OK,这篇就到这里,抛砖引玉而已,关于erlang元编程能力的体现,我会在后面的blog里慢慢展开。

 

 

 

 

 

 

 

 posted on 2013-01-25 15:47  文武双全大星星  阅读(711)  评论(0编辑  收藏  举报