ErlangでUNIX domain socketを使う

ErlangでUNIX domain socketに接続しようと思ったら、Erlangの標準では接続できなかった。なので、接続するための方法を調べた時のことを書く。
また、時間の都合もあり、ClientとしてUNIX domain socketに接続する方法を中心に調べた。

# UNIX domain socketが使えるLibrary

まず、ネットを検索してみると、UNIX domain socketを使うためのライブラリはGitHub上にいくらかあった。

## GitHub repository

* [procket](https://github.com/msantos/procket)
    * procket is an Erlang library for socket creation and manipulation.
    * 様々なSocketを使うためのライブラリ
* [unixdom_drv](https://github.com/mikma/unixdom_drv)
    * This is a very incomplete reimplementation of the UNIX domain socket driver that I wrote and added to the www.erlang.org "User Contributions" collection.
    * UNIX domain socketを使うためのライブラリ
* [basho/enm](https://github.com/basho/enm)
    * enm is an Erlang port driver that wraps the nanomsg C library, allowing Erlang systems to communicate with other nanomsg endpoints. enm supports idioms and approaches common to standard Erlang networking facilities such as gen_tcp and gen_udp.
    * C言語で書かれた[nanomsg](http://nanomsg.org/)をwrapしたライブラリ。
* [afunix](https://github.com/tonyrog/afunix)
    * afunix is an api to unix domain sockets. The afunix is a "plugin" to the inet/gen_tcp. The api is binary compatible with the gen_tcp interface. The afunix is not available on the windows platform, hence the unix part of afunix.
    * UNIX domain socketを使うためのライブラリ
        * _1番シンプル？_

## その他

ncコマンドでUNIX domain socketに接続することもできる。それを利用して、以下のように外部コマンド呼び出しでUNIX domain socketと接続する方法もある。

* [ErlangでUNIXドメインソケットのクライアント接続を行なう簡単な方法 - sileのブログ]
(http://sile.hatenablog.jp/entry/2014/11/27/042638)

# 標準Packagesにexamplesとして入っている "uds_dist"

Erlang OTPにuds_distというライブラリがある。これは、 `${OTPROOT}/lib/kernel/examples` の中にある（[GitHub](https://github.com/erlang/otp/tree/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples)）。
ただし、 **examples** とあるように、標準ではloadされていないし、コード自体も怪しい。Cのコードが約1,000行、Erlangのコードが約600行なので、見てみることにした。

## 使用

<font color="red">まず、src/uds_server.erl内の `erlang:info(machine)` だけ使えないので、 `erlang:system_info(machine)` に修正する必要がある。</font>

Makefileのサンプルもあるが、rebar.configを作って、rebarの実行ファイルを持って来てコンパイルできるようにした。
主に下記の2設定を書いておけば良い。（他は通常通り）

```erlang:rebar.config
{port_specs,
  [
   {"priv/lib/uds_drv.so", ["c_src/*.c"]}
  ]}.

{port_env,
  [
   {".*", "CFLAGS", "$CFLAGS -O3 -g -fPIC -pedantic -Wall -Werror -Ic_src/"}
  ]}.

```

UNIX domain socketを（クライアントとして）使いたい場合は、以下のようになる。

```erl
$ ./rebar shell
1> uds_server:start_link().
{ok,<0.43.0>}
% 開かれているUnix domain socketのpathは"/tmp/erlang/unix_domain_socket_path"とする
2> {ok, Port} = uds:connect("unix_domain_socket_path").
{ok,#Port<0.920>}
3> uds:send(Port, "send message").
{ok,#Port<0.920>}
4> uds:close(Port).
ok
5> 
```

※今回、uds_dist.erl周りは時間の関係上終えていない

# 制限

## "/tmp/erlang" 以下しか、待ち受け/接続できない

[define部分 - GitHub](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L98)

```c:uds_drv.c
#define SOCKET_PATH "/tmp/erlang"
```

この部分の設定より、 `/tmp/erlang/` 以下にしかUNIX domain socketを作ることができない。この制限をなくすには `SOCKET_PATH` を空文字にすればよい。（ただし、Erlangの呼び出し側でUNIX domain socketのパスの最初の `/` を除く必要がある（ `/tmp/socket` に接続したい場合は、 `"tmp/socket"` ）。嫌だったらコードを修正。

## 4byteのSize Headerが付く

[define部分 - GitHub](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L109)

```c:uds_drv.c
#define HEADER_LENGTH 4
```

[send関数](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L707)

```c:uds_drv.c
static void do_send(UdsData *ud, char *buff, int bufflen) 
{
    char header[4];
    int written;
    SysIOVec iov[2];
    ErlIOVec eio;
    ErlDrvBinary *binv[] = {NULL,NULL};

    put_packet_length(header, bufflen);
    DEBUGF(("Write packet header %u,%u,%u,%u.", (Word) header[0],
	   (Word) header[1], (Word) header[2],(Word) header[3]));
    iov[0].iov_base = (char *) header;
    iov[0].iov_len = 4;
    iov[1].iov_base = buff;
    iov[1].iov_len = bufflen;
    eio.iov = iov;
    eio.binv = binv;
    eio.vsize = 2;
    eio.size = bufflen + 4;
    written = 0;
    if (driver_sizeq(ud->port) == 0) {
	if ((written = writev(ud->fd, iov, 2)) == eio.size) {
```

[recv関数](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L753)

```c:uds_drv.c
static int buffered_read_package(UdsData *ud, char **result)
{
    int res;
    int data_size;

    if (ud->buffer_pos < ud->header_pos + HEADER_LENGTH) {
	/* The header is not read yet */
	DEBUGF(("Header not read yet"));
	if ((res = read_at_least(ud, ud->header_pos + HEADER_LENGTH - 
				 ud->buffer_pos)) < 0) {
	    DEBUGF(("Header read failed"));
	    return res;
	}
    } 
    DEBUGF(("Header is read"));
    /* We have at least the header read */
    data_size = get_packet_length((char *) ud->buffer + ud->header_pos);
    DEBUGF(("Input packet size = %d", data_size));
    if (ud->buffer_pos < ud->header_pos + HEADER_LENGTH + data_size) {
	/* We need to read more */
	DEBUGF(("Need to read more (bufferpos %d, want %d)", ud->buffer_pos,
		ud->header_pos + HEADER_LENGTH + data_size));
	if ((res = read_at_least(ud, 
				 ud->header_pos + HEADER_LENGTH + 
				 data_size - ud->buffer_pos)) < 0) {
	    DEBUGF(("Data read failed"));
	    return res;
	}
    }
    DEBUGF(("Data is completely read"));
    *result = (char *) ud->buffer + ud->header_pos + HEADER_LENGTH;
    ud->header_pos += HEADER_LENGTH + data_size;
    return data_size;
}
```

上に適当にコードを抜粋してきたが、このように、通信内容に4byteのSize Headerを付ける/付いている前提で実装されている。しかも、Sizeを変える/Size Headerを付けない場合、定数の値を修正するだけでは動かない（主にsend周り）。Erlangのコードで実装されたアプリケーション同士がこのライブラリを使ってUNIX domain socketで通信する場合は問題がないが（そのような状況があるのかは別として）、仕様を変えたい場合はある程度コードを修正する必要がある。

# Port drivers

最後に、コードを見るレベルでPort Driverがどういったことをしているか簡単に書いておく。

[ErlDrvEntry - GitHub](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L191)

```c:ErlDrvEntry
ErlDrvEntry uds_driver_entry = {
    NULL,		   /* init, N/A */
    uds_start,             /* start, called when port is opened */
    uds_stop,              /* stop, called when port is closed */
    uds_command,           /* output, called when erlang has sent */
    uds_input,             /* ready_input, called when input descriptor ready */
    uds_output,            /* ready_output, called when output descriptor ready */
    "uds_drv",             /* char *driver_name, the argument to open_port */
    uds_finish,            /* finish, called when unloaded */
    NULL,                  /* void * that is not used (BC) */
    uds_control,           /* control, port_control callback */
    NULL,                  /* timeout, called on timeouts */
    NULL,                  /* outputv, vector output interface */
    NULL,                  /* ready_async */
    NULL,                  /* flush */
    NULL,                  /* call */
    NULL,                  /* event */
    ERL_DRV_EXTENDED_MARKER,
    ERL_DRV_EXTENDED_MAJOR_VERSION,
    ERL_DRV_EXTENDED_MINOR_VERSION,
    0,	/* ERL_DRV_FLAGs */
    NULL,
    NULL,                  /* process_exit */
    uds_stop_select
};
```

まず、上記のように[`ErlDrvEntry`](http://www.erlang.org/doc/tutorial/c_portdriver.html#id63867)が定義されていて、ここにどんなイベントが起きたら、どの関数が呼ばれるかが書かれている。

* `uds_start` / `uds_stop` 
    * portがopen/closeした時に呼ばれる関数
* `uds_command`
    * Erlangで `port_command` が呼ばれた時に呼び出される関数
* `uds_input` / `uds_output`
    * `driver_select` しているdiscriptorに入出力があった時に呼ばれる関数
* etc...

それぞれインターフェースが決まっているが、それは[公式ドキュメント](http://www.erlang.org/doc/man/driver_entry.html)参照とする。

また、上記関数の引数として取られている `ErlDrvData` は、[ここ](https://github.com/erlang/otp/blob/743ed31108ee555db18d9833186865e85e34333e/erts/emulator/beam/erl_driver.h#L224)で定義されているポインタである。

```c:erl_driver.h
typedef struct _erl_drv_data* ErlDrvData; /* Data to be used by the driver itself. */
```

ここには適当に持ち回すデータを入れるだけみたいだ。uds_distでは[次のように](https://github.com/erlang/otp/blob/172e812c491680fbb175f56f7604d4098cdc9de4/lib/kernel/examples/uds_dist/c_src/uds_drv.c#L126)なっている。

```c:uds_drv.h
typedef struct uds_data {
    int fd;                   /* File descriptor */
    ErlDrvPort port;          /* The port identifier */
    int lockfd;               /* The file descriptor for a lock file in case of listen sockets */
    Byte creation;            /* The creation serial derived from the lockfile */
    PortType type;            /* Type of port */
    char *name;               /* Short name of socket for unlink */
    Word sent;                /* Messages sent */
    Word received;            /* Messages received */
    struct uds_data *partner; /* The partner in an accept/listen pair */
    struct uds_data *next;    /* Next structure in list */

    /* The input buffer and it's data */
    int buffer_size;          /* The allocated size of the input buffer */
    int buffer_pos;           /* Current position in input buffer */
    int header_pos;           /* Where the current header is in the input buffer */
    Byte *buffer;            /* The actual input buffer */
} UdsData;
```

この構造体が各関数の先頭でキャストされて使われている。

後は、uds_server.erl で `erl_dll:load_driver/2` して、 `erlang:port_command/2` 等の実行により処理されている。

# 最後に

このような感じに、自分で一から（examplesを見ているということはおいておいて）ErlangでUNIX domain socket周りを実装するとなると、結構苦労することが多い。また、[「プログラミングErlang p.178」](http://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Erlang-Joe-Armstrong/dp/4274067149)にも書かれているのだが、「リンクインドライバで何か重大なエラーが起こると、Erlangシステムがクラッシュし、システムのすべてのプロセスに影響が出る」（[プログラミングErlangの文章を引用](https://books.google.co.jp/books?id=fErWbQVSM3YC&pg=PA178&lpg=PA178&dq=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Erlang+%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%A4%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%90&source=bl&ots=Id0SpcFPT0&sig=Mf66XhMeIppbmtPjmPkjUD3ADts&hl=ja&sa=X&ei=kIqSVNnVDsvx8gXLrIDoDw&ved=0CB8Q6AEwAA#v=onepage&q&f=false)）ので、「ご利用は計画的に」。（Segmentation Faultが出るとErlang VMが落ちる…gkbr）