Erlang丨获取Record字段名

前言

数据结构 Record 通过 -record 指令来定义,第一个参数是 Record 的名字(Atom 类型),第二个参数是 Record 的字段名以及对应的默认值(Tuple 类型)。代码中的 Record 的定义只存在于编译期,编译完成后编译器将 Record 数据“翻译”为 Tuple 数据(Record 本质上是 Tuple 在语法层面的语法糖),所以 Record 在 Erlang 运行时并不存在,无法获取它的字段名。
只有字段值

情景

根据 官方文档 可知,伪函数 record_info/2 是在编译过程中增加的,不属于某个模块的函数,所以 erlang:record_info/2 这种写法会出现异常。
然后,record_info/2 的第一个参数是 fields 或者 size,第二个参数是 Record 的原子名,不能是变量名。
所以想写一个通用的函数实现——传入一个 Record 数据,返回 [{FieldName, FieldVal} ...] 的 K-V 列表结果,以下写法是不行的:
不能传递变量名
别忘了宏 Macro 也是编译期的。可以定义一个 record.hrl 头文件,其中包含了需要使用其字段名的众多 Record 定义,如下所示:

-record(people, {
    age = 18,
    height = 180,
    weight = 70,
    money = 7000
}).

-define(RECORD_FIELDS(RecordName), record_info(fields, RecordName)).

接着在测试代码 test.erl 中引入该头文件并使用宏:

-module(test).

-include("record.hrl").

-export([test_record/0]).

test_record() -> 
    PeopleFields = ?RECORD_FIELDS(people),
    io:format("PeopleFields:~w~n", [PeopleFields]),
    ok.

但是这种做法并不优雅,还是要在代码中写死 Record 名,属于硬编码。其实 Record 的定义存在于编译后的 test.beam 文件中。
首先通过 code:get_object_code/1 来获取获取目标模块的 beam 文件,然后通过 beam_lib:chunks/2 从 beam 文件中提取目标模块的属性。新建一个 test_beam.erl 文件来验证一下 Record 是否存在:

-module(test_beam).

%% External API
-export([test/0]).

test() -> 
    %% 获取 test.beam 文件
    BeamFile = get_beam_file(test),
    AbsCode = get_abstract_code(BeamFile),
    io:format("Get TestErl Code:~p~n", [AbsCode]),
    ok.

%% @doc 获取对应模块的 Beam 文件
get_beam_file(Module) -> 
    case code:get_object_code(Module) of
        {Module, Binary, _Filename} -> 
            Binary;
        error -> 
            throw({object_code_not_found, Module})
    end.

%% @doc 从 Beam 文件中获取属性
get_abstract_code(BeamFile) ->
    case beam_lib:chunks(BeamFile, [abstract_code]) of
        {ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}]}} ->
            Forms;
        {ok, {_, [{abstract_code, no_abstract_code}]}} ->
            throw(no_abstract_code)
    end.

注意:如果直接编译 c(test). 后,运行 test_beam:test() 会抛出以下异常:

** exception throw: no_abstract_code
     in function  test_beam:get_abstract_code/1 (test_beam.erl, line 52)
     in call from test_beam:test/0 (test_beam.erl, line 29)

因为此时的 Beam 文件未包含 debug_info 信息,所以 beam_lib:chunks/2 失效了,重新编译 test.erl 时携带附加选项 c(test, [debug_info]). 就正常了,如图所示:
保存record_field字段

警告
如非必要,请不要在编译重要的代码时使用 debug_info 选项,或者已经对 debug_info 进行了加密,否则别人也可以通过 beam 文件来反推源代码

既然 Beam 文件是编译后产生的,而且其中保存了 Record 结构的 record_field 字段名以及默认值,有没有办法可以在编译时新增一个提供 Record 信息的接口呢?有!
根据 StackOverFlow 网友的评论,Github 上已经有这种 写好的轮子 了,作者也很详细的描述其用法与用途,在你的代码中加入一句 -compile({parse_transform, dynarec}).

原理

在 Emakefile 的编译选项加上 {parse_transform, xxx} (xxx 是模块名),xxx.erl 代码中需要有 -export([parse_transform/2]). 。那么在使用 make 编译 erl 文件生成 abstract_code 之后会调用 xxx:parse_transform/2 对传入的 abstract_code 进行处理(嵌入我们需要的额外函数 / 替换某个方法),处理完返回新的 abstract_code 再写入。

parse_transform(Forms, _Options) ->
    Records = [Record || {attribute, _, record, _} = Record <- Forms],
    Tuples = lists:flatten(lists:map(fun get_tuples/1, Records)),
    NewForms = lists:reverse([gen_new_record(Tuples) |
                              [gen_records(Tuples) |
                               [gen_fields(Tuples) |
                                [gen_getter(Tuples) |
                                 [gen_setter(Tuples) |
                                  lists:reverse(Forms)]]]]]),
    add_exports(NewForms).

函数第一行将传入的数据使用列表解析过滤出需要的 Record,如下图所示:
过滤Record
经过处理后额外增加了几个函数,通过 Module:module_info/1 查看 exports 选项即可知道。

参考

  1. stackoverflow
  2. Erlang 官网文档
  3. 博客园 -- 简单的反汇编beam文件
打赏
评论区
头像
文章目录