Erlang的二进制操作很简单很强大,《Erlang二进制创建的内部机制和优化》一文介绍了binary的创建,现在就来探索它的逆过程,匹配操作。
erlc +\'S\' test.erl
打开文件 $ERL_TOP/erts/emulator/beam/ops.tab 文件,找到映射的相关指令:
bs_get_integer2对应的有很多,在我们实例中是按字节匹配的,所以这里会选择 i_bs_get_integer_8
现在我们来看 beam_emu.c 中的具体实现。
在文件beam_emu.c中搜索i_bs_start_match2,经过一些参数处理后就goto do_start_match,
由于beam_emu.c中的代码比较多,这里就不贴了,它最后会调用erl_bits.c中的erts_bs_start_match_2函数。
接着来看 i_bs_get_integer_8 经过一系列参数处理后,最终执行的代码是:
要理解这两行代码,先要了解match context,
关于match context二进制的介绍和两个结构体,可参见《Erlang Binary的内部结构和分类介绍》一文。
先重温一下ErlBinMatchBuffer的结构:
在#MARK_A处,BYTE_OFFSET(_mb->offset)是把位偏量移转换成字节偏移量,
由于base指针是直接指向二进制数据的,现在就可以很方便的从中取出匹配到的值,即_mb->base[N],N为字节偏移量。
匹配完成后,位偏移量要自增一个字节(_mb->offset += 8),让它定位到下一个匹配点。
_result = make_small(_mb->base[BYTE_OFFSET(_mb->offset)]);
以上贴出的代码中,一些地方我们加上了一些测试输出,现在就来运行一下这个实例,看看输出结果:
END
下面是一个简单实例,它的功能是binary_to_list,把二进制流按字节匹配出来生成list。
%% test.erl
-module(test).
-export([t/1,t2/4]).
t(<<H,T/binary>>) ->
[H|t(T)];
t(<<>>) -> [].
将test.erl生成test.S,看看它执行了哪些指令。
erlc +\'S\' test.erl
%% test.S
{function, t, 1, 2}.
{label,1}.
{line,[{location,"test.erl",4}]}.
{func_info,{atom,test},{atom,t},1}.
{label,2}.
%% bs_start_match2,匹配初始化
{test,bs_start_match2,{f,1},1,[{x,0},0],{x,0}}.
%% bs_get_integer2,从二制制流中取出整数
{test,bs_get_integer2,
{f,3},
1,
[{x,0},
{integer,8},
1,
{field_flags,[{anno,[4,{file,"test.erl"}]},unsigned,big]}],
{x,1}}.
{'%',{bin_opt,[4,{file,"test.erl"}]}}.
{test,bs_test_unit,{f,4},[{x,0},8]}.
{allocate,1,2}.
{move,{x,1},{y,0}}.
{line,[{location,"test.erl",5}]}.
{call,1,{f,2}}.
{test_heap,2,1}.
{put_list,{y,0},{x,0},{x,0}}.
{deallocate,1}.
return.
{label,3}.
{test,bs_test_tail2,{f,4},[{x,0},0]}.
{move,nil,{x,0}}.
return.
{label,4}.
{bs_context_to_binary,{x,0}}.
{jump,{f,1}}.
上面的匹配操作是通过 bs_start_match2 和 bs_get_integer2 这两条指令实现的。
打开文件 $ERL_TOP/erts/emulator/beam/ops.tab 文件,找到映射的相关指令:
bs_start_match2 Fail=f ica X Y D => jump Fail
bs_start_match2 Fail Bin X Y D => i_bs_start_match2 Bin Fail X Y D
i_bs_start_match2 r f I I d
i_bs_start_match2 x f I I d
i_bs_start_match2 y f I I d
# Fetching integers from binaries.
bs_get_integer2 Fail=f Ms=rx Live=u Sz=sq Unit=u Flags=u Dst=d => \
gen_get_integer2(Fail, Ms, Live, Sz, Unit, Flags, Dst)
i_bs_get_integer_small_imm r I f I d
i_bs_get_integer_small_imm x I f I d
i_bs_get_integer_imm r I I f I d
i_bs_get_integer_imm x I I f I d
i_bs_get_integer f I I d
i_bs_get_integer_8 r f d
i_bs_get_integer_8 x f d
i_bs_get_integer_16 r f d
i_bs_get_integer_16 x f d
i_bs_get_integer_32 r f I d
i_bs_get_integer_32 x f I d
bs_start_match2对应i_bs_start_match2
bs_get_integer2对应的有很多,在我们实例中是按字节匹配的,所以这里会选择 i_bs_get_integer_8
现在我们来看 beam_emu.c 中的具体实现。
在文件beam_emu.c中搜索i_bs_start_match2,经过一些参数处理后就goto do_start_match,
由于beam_emu.c中的代码比较多,这里就不贴了,它最后会调用erl_bits.c中的erts_bs_start_match_2函数。
Eterm
erts_bs_start_match_2(Process *p, Eterm Binary, Uint Max)
{
Eterm Orig;
Uint offs;
Uint* hp;
Uint NeededSize;
ErlBinMatchState *ms;
Uint bitoffs;
Uint bitsize;
Uint total_bin_size;
ProcBin* pb;
ASSERT(is_binary(Binary));
total_bin_size = binary_size(Binary);
if ((total_bin_size >> (8*sizeof(Uint)-3)) != 0) {
return THE_NON_VALUE;
}
// 测试输出
erts_fprintf(stderr, "start match, total_bin_size:%ld\n", total_bin_size);
NeededSize = ERL_BIN_MATCHSTATE_SIZE(Max);
// 为match context二进制分配内存空间
hp = HeapOnlyAlloc(p, NeededSize);
ms = (ErlBinMatchState *) hp;
ERTS_GET_REAL_BIN(Binary, Orig, offs, bitoffs, bitsize);
pb = (ProcBin *) boxed_val(Orig);
if (pb->thing_word == HEADER_PROC_BIN && pb->flags != 0) {
erts_emasculate_writable_binary(pb);
}
// 初始化match context二进制
ms->thing_word = HEADER_BIN_MATCHSTATE(Max);
(ms->mb).orig = Orig;
(ms->mb).base = binary_bytes(Orig);
(ms->mb).offset = ms->save_offset[0] = 8 * offs + bitoffs;
(ms->mb).size = total_bin_size * 8 + (ms->mb).offset + bitsize;
return make_matchstate(ms);
}
到此为止 i_bs_start_match2 指令执行完毕。
接着来看 i_bs_get_integer_8 经过一系列参数处理后,最终执行的代码是:
do_bs_get_integer_8: {
ErlBinMatchBuffer *_mb;
Eterm _result;
_mb = ms_matchbuffer(bs_get_integer8_context);
如果剩余不足8位,则匹配失败。
if (_mb->size - _mb->offset < 8) {
ClauseFail();
}
// 如果offset不是8的倍数,则不能按正常的字节读取,跳转到erl_bits.c文件中的 erts_bs_get_integer_2函数中处理
// 我们的这个例子中不会有这种情况,暂时不分析这部分。
if (BIT_OFFSET(_mb->offset) != 0) {
_result = erts_bs_get_integer_2(c_p, 8, 0, _mb);
} else {
// 测试输出
erts_fprintf(stderr, "matched value:%d\n", _mb->base[BYTE_OFFSET(_mb->offset)]);
// #MARK_A
_result = make_small(_mb->base[BYTE_OFFSET(_mb->offset)]);
_mb->offset += 8;
}
StoreBifResult(1, _result);
}
上面#MARK_A处就是匹配操作中取值的内部实现,也是本例匹配操作的关键和精华,虽然它只有两行代码。
要理解这两行代码,先要了解match context,
关于match context二进制的介绍和两个结构体,可参见《Erlang Binary的内部结构和分类介绍》一文。
先重温一下ErlBinMatchBuffer的结构:
// This structure represents a binary to be matched.
typedef struct erl_bin_match_buffer {
Eterm orig; /* 指向原始二进制数据包(ProcBin或ErlHeapBin) */
byte* base; /* 直接指向二进制数据(ProcBin->bytes或ErlHeapBin->data) */
Uint offset; /* Offset in bits. */
size_t size; /* Size of binary in bits. */
} ErlBinMatchBuffer;
在#MARK_A处,BYTE_OFFSET(_mb->offset)是把位偏量移转换成字节偏移量,
由于base指针是直接指向二进制数据的,现在就可以很方便的从中取出匹配到的值,即_mb->base[N],N为字节偏移量。
匹配完成后,位偏移量要自增一个字节(_mb->offset += 8),让它定位到下一个匹配点。
_result = make_small(_mb->base[BYTE_OFFSET(_mb->offset)]);
以上贴出的代码中,一些地方我们加上了一些测试输出,现在就来运行一下这个实例,看看输出结果:
Eshell V5.10.2 (abort with ^G)
1> test:t(<<1,2,3>>).
start match, total_bin_size:3
matched value:1
matched value:2
matched value:3
...(此处省略N行与本文内容无关的输出)
[1,2,3]
END