2009年11月29日日曜日

erlangの非同期ドライバーのコードを追う

http://groups.google.co.jp/group/simple_index/web/091128--index23 のコピペと続き

1. 初期化
1-1. DRIVER_INIT - erl_ddll:load_driver/2

DRIVER_INIT は erl_ddll:load_driver/2 を読んだときに、初期化のために呼ばれる。ライブラリの関数のエントリポイントを返す。
DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
return &example_driver_entry;
}


ErlDrvEntryには、関数ポインタがセットされている。

* init: load_driverで呼び出される。
* start: open_portで呼び出される。
* stop: close_port で呼び出される。
* output: データをportに送ったときに呼ばれる。

erlang側では Port ! {self(), {command, Data}}, または port_command/2 を使う。
-----
This is called when an erlang process has sent data to the port. The data is pointed to by buf, and is len bytes. Data is sent to the port with Port ! {self(), {command, Data}}, or with port_command/2. Depending on how the port was opened, it should be either a list of integers 0...255 or a binary. See open_port/3 and port_command/2.
----

* ready_async: C側から driver_asyncを実行するとready_asyncに結果が渡される。

参考:
http://www.erlang.org/doc/man/driver_entry.html

1-2. start - open_port

以下の例では、spawn したプロセス内でportをオープンし、ループに入っている。
Portは startで作成したErlDrvDataのhandleを保持している。

init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).

2.関数呼び出し

サンプルだと static void example_drv_output(ErlDrvData handle, char* buff, int bufflen) に

Port ! {self(), {command, Data}}

からメッセージを渡す。

C側では Data をbuffにセットしてoutputにセットされた関数が呼び出される。

static void example_drv_output(ErlDrvData handle, char* buff, int bufflen)
{
example_data* d = (example_data*)handle;
our_async_data* a = (our_async_data*)malloc(sizeof(our_async_data));
a->fn = buff[0];
a->arg = buff[1];
driver_async(d->port, NULL, example_drv_foobar, a, free);
}


最後にhandleをexample_dataにキャストし、portを取り出し、実行する関数と引数をセットしてdriver_asyncを実行している。

drive_asyncについて詳細はこちら
http://www.erlang.org/doc/man/erl_driver.html

long driver_async (ErlDrvPort port, unsigned int* key, void (*async_invoke)(void*), void* async_data, void (*async_free)(void*))

これは、erlangのエミュレータとは別のthreadで実行される。erlangはデフォルトではスレッドプールなしで起動されるので、スレッドプールの数を指定するときは、+Aの引数でスレッド数を指定してエミュレータを起動する。

key がNULLだとスレッドはラウンドロビンで使われるが、keyを指定すると同じkeyの場合は同じスレッドが使用される。

これの、async_invokeで指定された関数が終了すると、ready_async にセットした関数が呼び出される。
async_freeはasyncの処理がキャンセルされたときに呼び出される。

ready_asyncにはhandleとdataが渡されるのでそれぞれキャストしてportと結果をそれぞれ取り出しdriver_outputで結果をerlangに返す。


static void ready_async(ErlDrvData handle, ErlDrvThreadData async_data)
{
example_data* d = (example_data*)handle;
our_async_data* a = (our_async_data*)async_data;

driver_output(d->port, &(a->res), 1);
free(a);
}


---------
以下はerlang側のコード

-module(complex5).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).

start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).

init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).

stop() ->
complex ! stop.

foo(X) ->
call_port({foo, X}).
bar(Y) ->
call_port({bar, Y}).

call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.

loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port);
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, Reason} ->
io:format("~p ~n", [Reason]),
exit(port_terminated)
end.

encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].

decode([Int]) -> Int.