上文介绍了代码生成过程,成功的从erlang抽象码生成了erlang源文件,抽象码的替换步骤很少,这主要得益于模板文件pokemon_pb.erl的设计。这里将继续分析pokemon_pb.erl的执行过程,从中学习它的编程技巧。
编码过程:
pokemon_pb.erl
encode_pikachu(Record) when is_record(Record, pikachu) ->
encode(pikachu, Record).
encode(pikachu, Record) ->
iolist_to_binary(iolist(pikachu, Record)).
iolist(pikachu, Record) ->
[pack(1, required, with_default(Record#pikachu.abc, none), string, [])].
主要的编码工作交给了pack函数,因此iolist函数仅仅需要保存message各个域的域编号,操作类型(required等),默认值,域类型(string等)。
pack(_, optional, undefined, _, _) -> [];
pack(_, repeated, undefined, _, _) -> [];
pack(_, repeated_packed, undefined, _, _) -> [];
pack(_, repeated_packed, [], _, _) -> [];
pack(FNum, required, undefined, Type, _) ->
exit({error, {required_field_is_undefined, FNum, Type}});
pack(_, repeated, [], _, Acc) ->
lists:reverse(Acc);
pack(FNum, repeated, [Head|Tail], Type, Acc) ->
pack(FNum, repeated, Tail, Type, [pack(FNum, optional, Head, Type, [])|Acc]);
pack(FNum, repeated_packed, Data, Type, _) ->
protobuffs:encode_packed(FNum, Data, Type);
pack(FNum, _, Data, _, _) when is_tuple(Data) ->
[RecName|_] = tuple_to_list(Data),
protobuffs:encode(FNum, encode(RecName, Data), bytes);
pack(FNum, _, Data, Type, _) when Type=:=bool;Type=:=int32;Type=:=uint32;
Type=:=int64;Type=:=uint64;Type=:=sint32;
Type=:=sint64;Type=:=fixed32;Type=:=sfixed32;
Type=:=fixed64;Type=:=sfixed64;Type=:=string;
Type=:=bytes;Type=:=float;Type=:=double ->
protobuffs:encode(FNum, Data, Type);
pack(FNum, _, Data, Type, _) when is_atom(Data) ->
protobuffs:encode(FNum, enum_to_int(Type,Data), enum).
enum_to_int(pikachu,value) ->
1.
pack函数有五个地方值得注意:
1.对于optional的域,其值存在与否都不会有影响;
2.对于required的域,若值不存在,则应该引发异常;
3.对于repeated的域,由于其值是一个列表,因此在编码时,应该对列表的每一个元素都进行pack;
4.对于内建类型,直接调用protobuffs:encode进行编码,protobuffs:encode编码方式是protocol buffers编码方式的erlang实现,分析起来也比较枯燥,读者可以自行查阅;
5.对于枚举类型,需要先通过enum_to_int将枚举类型替换为具体的值,这个函数也是通过抽象码替换过程生成的,message的每个枚举定义都有一个独立的值。
解码过程:
decode_pikachu(Bytes) when is_binary(Bytes) ->
decode(pikachu, Bytes).
decode(pikachu, Bytes) when is_binary(Bytes) ->
Types = [{1, abc, int32, []}, {2, def, double, []}],
Decoded = decode(Bytes, Types, []),
to_record(pikachu, Decoded).
decode/2保存了解码时遇到的每一个域的处理方法,这相当于一个简单的语法定义,具体的解析过程由decode/3完成。
decode(<<>>, _, Acc) -> Acc;
decode(Bytes, Types, Acc) ->
{ok, FNum} = protobuffs:next_field_num(Bytes),
case lists:keysearch(FNum, 1, Types) of
{value, {FNum, Name, Type, Opts}} ->
{Value1, Rest1} =
case lists:member(is_record, Opts) of
true ->
{{FNum, V}, R} = protobuffs:decode(Bytes, bytes),
RecVal = decode(list_to_atom(string:to_lower(atom_to_list(Type))), V),
{RecVal, R};
false ->
case lists:member(repeated_packed, Opts) of
true ->
{{FNum, V}, R} = protobuffs:decode_packed(Bytes, Type),
{V, R};
false ->
{{FNum, V}, R} = protobuffs:decode(Bytes, Type),
{unpack_value(V, Type), R}
end
end,
case lists:member(repeated, Opts) of
true ->
case lists:keytake(FNum, 1, Acc) of
{value, {FNum, Name, List}, Acc1} ->
decode(Rest1, Types, [{FNum, Name, lists:reverse([int_to_enum(Type,Value1) | lists:reverse(List)])} | Acc1]);
false ->
decode(Rest1, Types, [{FNum, Name, [int_to_enum(Type,Value1)]}|Acc])
end;
false ->
decode(Rest1, Types, [{FNum, Name, int_to_enum(Type,Value1)}|Acc])
end;
false ->
exit({error, {unexpected_field_index, FNum}})
end.
int_to_enum(_,Val) ->
Val.
这个函数是最大的一个函数,Types即是语法定义,而该函数相当于一个词法分析器,通过protobuffs:decode/2得到的每一个“符号”,都将根据Types的语法定义进行相应的动作:
1.对于嵌套定义的message,将进行递归类型解码;
2.对于repeated的域,将合并到原先找到的域的值列表中;
3.对于枚举类型,需要先通过int_to_enum将枚举值替换为具体的atom,这个函数也是通过抽象码替换过程生成的,message的每个枚举定义都有一个独立的atom。
4.对于标量类型,将直接记录到message的域值中。
最终,decode/3将生成一个列表,记录了每一个message的各个域,一个message的所有域放置在一个列表内。
decode(pikachu, Bytes) when is_binary(Bytes) ->
Types = [{1, abc, int32, []}, {2, def, double, []}],
Decoded = decode(Bytes, Types, []),
to_record(pikachu, Decoded).
to_record(pikachu, DecodedTuples) ->
lists:foldl(
fun({_FNum, Name, Val}, Record) ->
set_record_field(record_info(fields, pikachu), Record, Name, Val)
end, #pikachu{}, DecodedTuples).
set_record_field(Fields, Record, Field, Value) ->
Index = list_index(Field, Fields),
erlang:setelement(Index+1, Record, Value).
list_index(Target, List) -> list_index(Target, List, 1).
list_index(Target, [Target|_], Index) -> Index;
list_index(Target, [_|Tail], Index) -> list_index(Target, Tail, Index+1);
list_index(_, [], _) -> 0.
to_record是一个语义分析过程,根据Types定义,遍历decode/3生成的语法结构,提取每个message的每个域及其值的绑定关系,然后填充到具体的record中去,这个填充的过程也较为巧妙,由于record实质也是一个元组,to_record将遍历一个message的域值绑定列表,由于每一个域在proto文件中都有顺序编号,取出域值绑定关系时也就得到了域在message里面的未知,继而也就得到了域在record元组中的位置,set_record_field即用于填充元组指定字段的值。
至此,erlang的protocol buffers实现protobuffs就已经分析完毕了,其设计思路较为新颖,从中可以学到很多编程技巧,感兴趣的读者可以深挖并应用。
抽象码是连接erlang代码与erlang虚拟机代码的纽带,抽象码的执行过程即代表了虚拟机的执行过程,分析抽象码可以有效帮助我们理解erlang代码的执行方式、性能等,利用抽象码替换(可参考smerl的实现),可以实现erlang代码trace、profile插桩,帮助我们找到代码的bug,调试代码的性能。