MirageOS Unikernel アプリケーションの中身を理解する(その1)

## 概要
本記事は、MirageOS Unikernel アプリケーションを自分で書くための第一歩です。まずは例題アプリケーションを題材にして、仕組みの理解が出来ることを目的としています。

MirageOS Unikernel アプリケーションの記述言語は [OCaml](https://ocaml.org/) です。OCaml は関数型言語のためこれまで手続き型言語しか利用してこなかった方には苦痛ではあります。しかし一度覚えてみると世界が広がるので、これを機に頑張ってみるのもいいかもしれません。

「OCaml なんぞサッパリ分からん」という方には、[Real World OCaml](https://realworldocaml.org/) というオライリー本の Web 版を参考にすると良いかと思います。Real World OCaml は OCaml のバイブル的な書籍で、若干癖があるものの Web 版を無料で利用できます。また、[日本語のコミュニティサイト](http://ocaml.jp/)も参考になります。

以降は、[こちら](https://qiita.com/t-imada/items/6ee299653ac063532b4f)を参考に MirageOS Unikernel 環境をインストールし、[mirage-skeleton](https://github.com/mirage/mirage-skeleton) をすでに `git clone` してあることを想定します。

## 事前準備
今回はネットワークアプリケーションを利用するため、bridge と tap を作成しておきます。これにより、ホストOS(Linux) から MirageOS Unikernel ネットワークアプリケーションへ通信できるようにします。MirageOS Unikernel ではネットワーク通信に tap デバイスを利用するため、利用可能な tap デバイスが存在する環境であれば好きなネットワークを組んでもらって構いません。
![network_bridge.png](https://qiita-image-store.s3.amazonaws.com/0/291185/34b5ebb2-6cfb-b348-f551-029dc646835f.png)

```bash:bridgeとtapの作成
$ sudo brctl addbr mirage_br
$ sudo ip link set mirage_br up
$ sudo ip tuntap add tap0 mode tap
$ sudo ip link set dev tap0 up
$ sudo brctl addif mirage_br tap0
$ sudo ip addr add 192.168.100.1/24 dev mirage_br
$ ip addr show
...
4: mirage_br: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 3e:6b:2d:7d:d5:77 brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.1/24 scope global mirage_br
       valid_lft forever preferred_lft forever
5: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel master br0 state DOWN group default qlen 1000
    link/ether 3e:6b:2d:7d:d5:77 brd ff:ff:ff:ff:ff:ff
...
```
`mirage_br` `tap0` ともにステータスがUPとなり、`mirage_br`にはIPアドレス192.168.100.1が付与されました。(tap0にはIPアドレスが必要ないため、ここでは付与しません)

## お題のアプリケーション
mirage-skeleton で提供されているネットワークの[アプリケーションコード](https://github.com/mirage/mirage-skeleton/tree/master/device-usage/network)を利用します。

```:networkディレクトリ
$ cd ./mirage-skeleton/device-usage/network/
$ ls
config.ml  unikernel.ml
```

```ocaml:unikernel.ml
open Lwt.Infix

module Main (S: Mirage_stack_lwt.V4) = struct

  let start s =
    let port = Key_gen.port () in
    S.listen_tcpv4 s ~port (fun flow ->
        let dst, dst_port = S.TCPV4.dst flow in
        Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
                  (Ipaddr.V4.to_string dst) dst_port);
        S.TCPV4.read flow >>= function
        | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
        | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
        | Ok (`Data b) ->
          Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
          S.TCPV4.close flow
      );

    S.listen s

end
```

```ocaml:config.ml
open Mirage

let port =
  let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
  Key.(create "port" Arg.(opt int 8080 doc))

let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)

let stack = generic_stackv4 default_network

let () =
  register "network" [
    main $ stack
  ]
```

このプログラムの機能は、サービスプログラムとしてネットワークから流れてきた文字列データをコンソール上に表示するだけのものです。

しかしながら...そのままだと文字列データが表示されないため、下記のようにunikernel.mlファイルを修正します。具体的には、15行目にて `Logs.debug` 関数をコールしている箇所を `Logs.info` 関数に変更します。`Logs` モジュールについては[ここ](https://github.com/mirage/mirage-logs)にコードがあります。

```ocaml:unikernel.ml
(* 変更前 *)
15: Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));

(* 変更後 *)
15: Logs.info (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
```

#### コンパイル
早速コンパイルします。

```bash:コンパイル作業
# 固定IPアドレスの場合
$ mirage configure -t hvt --ipv4=192.168.100.10/24
$ make depend
$ make

# DHCPの場合
$ mirage configure -t hvt --dhcp=true
$ make depend
$ make
```
最初の`mirage configure`コマンドにて、MirageOS Unikernel アプリケーションへ付与するIPアドレスを設定します。固定IP(IPV4)とするには `--ipv4` オプションを使います。もしtapデバイスがDHCPサービスのあるネットワークにつながっている場合は、動的アドレスとすることも可能です。この場合は、`--dhcp`オプションを使います。

`mirage configure`コマンドの詳細は `mirage configure --help`で参照可能です。例えば、デフォルトゲートウェイアドレスの設定方法などもここで確認できます。

```
$ mirage configure --help
mirage-configure(1)              Mirage Manual             mirage-configure(1)
...
...
       --ipv4=IPV4 (absent=10.0.0.2/24)
           The network of the unikernel specified as an IP address and
           netmask, e.g. 192.168.0.1/16 .

       --ipv4-gateway=IPV4-GATEWAY (absent=10.0.0.1)
           The gateway of the unikernel.
...
```
プログラムバイナリができたので、動かしてみます。必要なのは、`solo5-hvt` と `network.hvt` です。仮想CPU上にてMirageOS Unikernel アプリケーションを動作させるための仮想マシンレイヤを提供するプログラムが`solo5-hvt`で、MirageOS Unikernel アプリケーションバイナリが`network.hvt`です。

#### アプリケーション実行
アプリケーションの実行はそんなに難しくありません。事前準備のところで作成していた `tap0` をオプションに指定して実行します。

```bash:アプリケーションの実行
# 実行
$ sudo ./solo5-hvt --net=tap0 ./network.hvt
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Memory map: 512 MB addressable:
Solo5:     unused @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0x239fff)
Solo5:     rodata @ (0x23a000 - 0x269fff)
Solo5:       data @ (0x26a000 - 0x36afff)
Solo5:       heap >= 0x36b000 < stack < 0x20000000
2019-01-12 14:03:02 -00:00: INF [netif] Plugging into 0 with mac b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [ethif] Connected Ethernet interface b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [arpv4] Connected arpv4 device on b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [udp] UDP interface connected on 192.168.100.10
2019-01-12 14:03:02 -00:00: INF [tcpip-stack-direct] stack assembled: mac=b6:a0:fe:12:4c:00,ip=192.168.100.10
```
違うコンソールウィンドウを開き、`ping`コマンドによる通信確認と`nc`コマンドによるデータ送信を行います。

```bash:pingとncコマンド実行
# 通信確認
$ ping 192.168.100.10
PING 192.168.100.10 (192.168.100.10) 56(84) bytes of data.
64 bytes from 192.168.100.10: icmp_seq=1 ttl=38 time=1.42 ms
64 bytes from 192.168.100.10: icmp_seq=2 ttl=38 time=1.02 ms
64 bytes from 192.168.100.10: icmp_seq=3 ttl=38 time=1.04 ms
^C
--- 192.168.100.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.029/1.166/1.424/0.186 ms

# 文字列"hogehoge"の送信
$ echo -en "hogehoge" | nc 192.168.100.10 8080
```
すると、MirageOS Unikernel アプリケーション側のコンソールに文字列"hogehoge"が表示されます。

```:アプリケーション側コンソール
2019-01-12 14:21:38 -00:00: INF [application] new tcp connection from IP 192.168.100.1 on port 59798
2019-01-12 14:21:38 -00:00: INF [application] read: 8 bytes:
hogehoge
```
今回のような自動的に終了しないサービス形式の MirageOS Unikernel アプリケーションを停止させるには、ctrl+c キーを使います。

```:アプリケーション停止時の出力
^Csolo5-hvt: Exiting on signal 2
```
以上により、MirageOS Unikernelを使ったネットワークアプリケーションを試すことができました！

#### コンパイルの仕組み
最初は `config.ml` と `unikernel.ml` の2ファイルしかディレクトリには存在していませんでしたが、`make` コマンド実行後はかなりファイルが増えています。

```:make後のディレクトリ内容
$ ls
_build            config.ml   main.ml   Makefile.solo5-hvt                 myocamlbuild.ml  solo5-hvt
_build-solo5-hvt  key_gen.ml  Makefile  mirage-unikernel-network-hvt.opam  network.hvt      unikernel.ml
```

まず、OCamlソースコードである拡張子 `.ml` を持つファイルが5つになりました。
&nbsp;&nbsp;&nbsp;&nbsp;- config.ml
&nbsp;&nbsp;&nbsp;&nbsp;- unikernel.ml
&nbsp;&nbsp;&nbsp;&nbsp;- main.ml
&nbsp;&nbsp;&nbsp;&nbsp;- key_gen.ml
&nbsp;&nbsp;&nbsp;&nbsp;- myocamlbuild.ml (0byteなのでコンパイルには無影響)
`config.ml` と `unikernel.ml` 以外のファイルは `mirage configure -t hvt` を実行すると生成されるものです。実は `config.ml` の内容をベースにそれらのファイルが自動生成されます。特に、`main.ml` はOSで言うところのkernel部分のソースコードに相当します。

`Makefile` と `Makefile.solo5-hvt` も `mirage configure -t hvt` 実行により生成されます。前者は MirageOS Unikernel アプリケーションバイナリ、後者は仮想マシンレイヤの Solo5-hvt バイナリを生成するための Makefile です。

最後に `make` を実行しますが、そのときの出力ログを眺めるとどんな感じでバイナリが生成されるか分かります。
(下記は `mirage build -v`の出力結果の一部です。`make` は単に `mirage build`を実行するだけなのですが、それだと出力結果が不十分なので `-v` を足して verbose モードにしています)

```:mirage&nbsp;build&nbsp;-vの出力内容
...
# 各.mlのオブジェクトファイル生成
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o key_gen.cmx key_gen.ml
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o unikernel.cmx unikernel.ml
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o main.cmx main.ml
# Unikernelのオブジェクトファイル(プラットフォームに依存しない部分だけ)生成
ocamlfind ocamlopt -g -dontlink unix -dontlink str -dontlink num -dontlink threads -linkpkg -output-obj -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 key_gen.cmx unikernel.cmx main.cmx -o main.native.o
# 他のライブラリ(プラットフォーム依存オブジェクトファイルを含む)とのリンク
mirage: [INFO] using ld as ld (pkg-config solo5-bindings-hvt --variable=ld)
mirage: [INFO] linking with ld -nostdlib -z max-page-size=0x1000 -static -T
                              /home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/solo5-bindings-hvt/solo5_hvt.lds
                              /home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/solo5-bindings-hvt/solo5_hvt.o
                              _build/main.native.o
                              -L/home/imada/.opam/4.07.0/lib/mirage-entropy
                              -lmirage-entropy_stubs+mirage-freestanding
                              -L/home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/ocaml-freestanding
                              /home/imada/.opam/4.07.0/share/pkgconfig/../../lib/mirage-solo5/libmirage-solo5_bindings.a
                              -lasmrun -lnolibc -lopenlibm
                              /usr/lib/gcc/aarch64-linux-gnu/7/libgcc.a -o
                              network.hvt
# solo5-hvtバイナリの生成
mkdir -p _build-solo5-hvt
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_core.c -o _build-solo5-hvt/hvt_core.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_elf.c -o _build-solo5-hvt/hvt_elf.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_main.c -o _build-solo5-hvt/hvt_main.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_module_net.c -o _build-solo5-hvt/hvt_module_net.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_kvm.c -o _build-solo5-hvt/hvt_kvm.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_kvm_aarch64.c -o _build-solo5-hvt/hvt_kvm_aarch64.o
cc -Wall -Werror -std=c99 -O2 -g  -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_cpu_aarch64.c -o _build-solo5-hvt/hvt_cpu_aarch64.o
cc  -o solo5-hvt _build-solo5-hvt/hvt_core.o _build-solo5-hvt/hvt_elf.o _build-solo5-hvt/hvt_main.o _build-solo5-hvt/hvt_module_net.o _build-solo5-hvt/hvt_kvm.o _build-solo5-hvt/hvt_kvm_aarch64.o _build-solo5-hvt/hvt_cpu_aarch64.o
```

ざっくりと何をしているかというと、

1. 各.mlファイルのオブジェクトファイル生成
2. 1.で生成したオブジェクトファイルからUnikernelの基本となる部分(Xen、KVMなどプラットフォームに依存しない実行部分)のオブジェクトファイル生成
3. 2.で生成したオブジェクトファイルと、その他必要なライブラリを `ld` コマンドでリンクさせて最終バイナリを生成

となります。

`_build` と `_build-solo5-hvt` は、`make` 実行によって生成される中間オブジェクトファイルが保持するためのディレクトリです。

最後に、`mirage-unikernel-network-hvt.opam`は本プログラムで生成されたバイナリをopamパッケージ化するときに使われるファイルです。つまり自作プログラムをパッケージ化するためのパッケージ構成ファイルが自動的に生成されるので、自分で書かなくてよいのです。(今回は詳細を割愛します)

## ソースコードを深追いする
次は、自分で MirageOS Unikernel アプリケーションを実装するためのはじめの一歩として、コードの中身を見ていきます。

上記の作業でファイルがたくさん生成されているため、一旦ディレクトリを綺麗にします。これにより、`git clone`した直後の状態に戻ります。

```bash:ディレクトリのクリーンアップ
$ make clean
$ ls
config.ml  unikernel.ml
```
元々置いてあるファイルは、`config.ml` と `unikernel.ml` の2つです。一般的に、MirageOS Unikernel アプリケーションをコンパイルするのに必要なのは、

1. アプリケーション実行内容がOCaml言語で書かれた、幾つかの ml ファイル
2. config.ml ファイル
 
です。

#### unikernel.ml の中身
まずは、アプリケーション実行内容が書かれたunikernel.mlファイルを見ていきます。

```ocaml:unikernel.ml
 1: open Lwt.Infix
 2:
 3: module Main (S: Mirage_stack_lwt.V4) = struct
 4:
 5:   let start s =
 6:     let port = Key_gen.port () in
 7:     S.listen_tcpv4 s ~port (fun flow ->
 8:       let dst, dst_port = S.TCPV4.dst flow in
 9:       Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
10:                 (Ipaddr.V4.to_string dst) dst_port);
11:       S.TCPV4.read flow >>= function
12:       | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
13:       | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
14:       | Ok (`Data b) ->
15:         Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
16:         S.TCPV4.close flow
17:     );
18:
19:     S.listen s
20:
21: end
```
上から順に説明していくと、

```ocaml:unikernel.mlの1行目
 1: open Lwt.Infix
```
1行目は、関数を順次実行させるために使われる演算子(>>=)を利用するために`Lwt.Infix`というモジュールを読み込んでいます。この演算子は11行目の`S.TCPV4.read flow >>= function`にて使われています。

```ocaml:unikernel.mlの3,21行目
 3: module Main (S: Mirage_stack_lwt.V4) = struct
    ...
21: end
```
3行目と21行目のコードブロックによって、Main という名前のモジュールを定義しています。モジュール名の1文字目は、英数大文字で始めなければいけません。また、このモジュール Main は引数として別のモジュール(モジュール名:S)をとります。このような別のモジュール引数によってパラメタ化されるようなモジュールを OCaml では[ファンクタ(functor)](https://v1.realworldocaml.org/v1/en/html/functors.html)と呼びます。 
モジュール S の型は [Mirage_stack_lwt.V4](https://docs.mirage.io/mirage-stack-lwt/Mirage_stack_lwt/module-type-V4/index.html) であり、これは IPV4 と Ethernet レイヤに関わるネットワーク機能が定義されています。

```ocaml:unikernel.mlの5行目
 5:  let start s =
     ...
     ...
```
5行目から始まるのは `start`関数 の定義です。この関数のコードブロックは19行目の `listen`関数コール までとなります。MirageOS Unikernel アプリケーションにおいては、**「アプリケーションコードのエントリポイントとなる関数は、あるモジュールに定義されている`start`関数でないといけない」** と定められており、本プログラムではここに定義されています。

また、`start`関数の引数として、`Main`ファンクタの引数として指定されていた`Mirage_stack_lwt.V4`のインスタンスが格納されている変数`s`が引数として設定されています。この変数`s`は、Ethernetデバイスと各種ネットワークスタックの集合体に相当します。加えてEthernetデバイスとしては、アプリケーション実行時に指定するtapデバイス(実行例ではtap0)にマッピングされています。主な使い方としては、「パケット受信時のコールバック関数を登録するための関数や、listenするときの関数の引数として変数`s`をとる」となります。

では、このstart関数の中で何が行われているのかを見ていきます。

```ocaml:unikernel.mlの6行目
 6:    let port = Key_gen.port () in
```
6行目では、config.mlの5行目`Key.(create "port" Arg.(opt int 8080 doc))`によって作成された変数値を格納する領域から、本ネットワークサービスプログラムがListenするためのport番号を取得し、変数`port`へ格納しています。なお、変数`port`は `let ... in` によって定義されているため、start関数のコードブロック内でしか参照することができません。

```ocaml:unikernel.mlの7〜17行目
 7:     S.listen_tcpv4 s ~port (fun flow ->
        ...
17:     );
```
7行目から17行目は、listen_tcpv4関数によってTCP/IP接続があったときのコールバック関数を登録しています。具体的には、7行目のlisten_tcpv4関数への引数 `s` と `port` によってlistenすべき`Mirage_stack_lwt.V4`のインスタンス(ここではtap0へのマッピングを有する)とポート番号を指定しています。さらに、その先の17行目まで続く `(fun flow -> ...)` がコールバック関数の定義です。このコールバック関数定義の大まかな解釈は、「TCPのコネクションが確立されたあとにクライアントから送られてくるTCPのペイロードとその他のメタデータが `flow` としてコールバック関数の引数に与えられ、 `fun flow ->` の右側に定義された処理がデータ到着毎に実行される」というものです。このとき、コールバック関数のスコープは、

```ocaml:コールバック関数のスコープ
 8:       let dst, dst_port = S.TCPV4.dst flow in
          ...
16:         S.TCPV4.close flow
```
ということになります。

```ocaml:宛先情報取得と出力
 8:       let dst, dst_port = S.TCPV4.dst flow in
 9:       Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
10:                 (Ipaddr.V4.to_string dst) dst_port);
```
具体的に何をやっているかというと、引数である`flow`からTCPパケットの宛先IPアドレスとポート番号を取得し、コンソールに出力しています。(8-10行目)

```ocaml:ペイロード内容取得と出力
11:       S.TCPV4.read flow >>= function
12:       | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
13:       | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
14:       | Ok (`Data b) ->
15:         Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
16:         S.TCPV4.close flow
```

さらに、`S.TCPV4.read` 関数によってクライアントから送信されたTCPペイロードのreadを試み、その結果に応じてその後の処理を変えています。結果による場合分けは、 1)クライアントからのConnection close、2)エラー発生、3)ペイロード取得完了 の3つです。正常パスである3)のケースでは、`S.TCPV4.read`の戻り値が ```Ok (`Data b)```となってペイロードは変数 `b` (型は[Cstruct](https://github.com/mirage/ocaml-cstruct))に格納されます。ペイロードが取得できたので、その長さとペイロード内容をコンソールに出力し、TCPコネクションをcloseします。(11-16行目)

14行目に記載の戻り値に利用する変数名は`b`である必要はありません。`x`とかでも構いません。その場合は15行目にて `(Cstruct.len x) (Cstruct.to_string x)` と記述し、`x` を参照する必要があります。

#### config.mlの中身
続けて、config.mlファイルを見ていきます。config.mlファイルはアプリケーションをコンパイルするために必要な情報を記述したりするためのもので、必須のファイルです。このファイルは拡張子が ml であり、こちらもOCaml言語で記述します。

```ocaml:config.ml
 1: open Mirage
 2:
 3: let port =
 4:   let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
 5:   Key.(create "port" Arg.(opt int 8080 doc))
 6:
 7: let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)
 8:
 9: let stack = generic_stackv4 default_network
10: 
11: let () =
12:  register "network" [
13:    main $ stack
14:  ]
```
こちらも1行目から順に見ていきます。

```ocaml:config.mlの1行目
 1: open Mirage
```
1行目は、MirageOS Unikernel アプリケーションを記述するために必要な各種モジュール、関数、変数が定義された[Mirageモジュール](https://docs.mirage.io/mirage/Mirage/index.html)を読み込むためのものです。MirageOS Unikernel アプリケーションコードには必須の文です。

```ocaml:config.mlの3〜5行目
 3: let port =
 4:   let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
 5:   Key.(create "port" Arg.(opt int 8080 doc))
```
3-5行目では、[Key](https://github.com/mirage/functoria#defining-new-keys) を利用してサーバプログラムで listen するためのポート番号を構成しています。具体的には、4行目にてコマンドオプション引数名 `port` とその説明文("The TCP port ... connections.")をからなる変数 `doc` を定義し、その `doc` とポート番号のデフォルト値`(int) 8080`の情報を持つ `Key` を5行目で生成し、その生成結果を3行目の変数`port`に格納しています。5行目の `Key.create` の引数として渡されている `"port"` は、プログラム中(本例ではunikernel.ml)にてその情報を参照するときに call するメンバ関数の名前となります。

ちょっとだけ元に戻って unikernel.ml の6行目を思い出してみましょう。

```ocaml:unikernel.mlの6行目
 6:    let port = Key_gen.port () in
```

`Key_gen.port()` という関数を呼ぶことで生成されるポート番号値を変数 `port` に格納していますが、config.mlの5行目にて設定している文字列 `"port"` がそのままメンバ関数名となっていることが分かります。基本的に、config.mlにて文字列`"XXXX"`を引数としてcreateされたKey情報は、`Key_gen.XXXX()`を呼ぶことで取り出すことが出来ます。

これによりこのプログラムはオプション名 `--port`vの引数をとることが出来るようになり、unikernel.ml の6行目にある変数 `port` に代入されることになります。もし省略された場合には自動的に8080が代入されます。

```ocaml:config.mlの7行目
 7: let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)
```
7行目では変数 `main` を定義しています。名前は何でもいいので、`main` である必要はないので自由に変更可能です。ここでは作成する Unikernel アプリケーションがどのような構成となるかを、[foreign]()関数を用いて記述します。この例では 
&nbsp;&nbsp;&nbsp;&nbsp;(1) 6行目で定義されたポート番号情報(Keyモジュールのリスト)
&nbsp;&nbsp;&nbsp;&nbsp;(2) start関数を含むモジュール名(=`Unikernel.Main` モジュール)
&nbsp;&nbsp;&nbsp;&nbsp;(3) `Unikernel.Main` モジュールの型定義
を引数として、MirageOSアプリケーション構成を定義するために利用される `foreign` 関数を呼んでいます。

(2) は `start` 関数を含むモジュールの明示的指定です。`Unikernel.Main` モジュールがどこから現れたかというと、unikernl.ml ソースコードファイルです。このファイルには `Main` モジュールが定義されていました。そしてこのファイルはコンパイルされると、ソースコードファイル内で定義された変数やクラス、モジュール等を `Unikernel` モジュールとして利用することが出来ます。ちなみにコンパイル後のモジュールの命名規則は簡単です。abc.ml をコンパイルすると、拡張子を除いた文字列 "abc" の先頭文字を大文字にした "Abc" がモジュール名となります。

(3) の型定義については、最後は必ず `job` になります。今回は `Unikernel.Main` モジュールがネットワークデバイスモジュール(`Mirage_stack_lwt.V4`)を引数にとるので、その型である `stackv4` を左側に書いて `@->` でつないでいきます。引数を必要としない場合には単に `job` とだけ書けばOKです。なお、どのような型があるかは [Mirageモジュールの定義](https://docs.mirage.io/mirage/Mirage/index.html)に記載があります。例えば Key-Value ストアデバイスであれば下記の2つです。
&nbsp;&nbsp;&nbsp;&nbsp;*kv_ro* (Read only, 型定義は[ここ](https://docs.mirage.io/mirage/Mirage/index.html#type-kv_ro))
&nbsp;&nbsp;&nbsp;&nbsp;*kv_rw* (Read/Write, 型定義は[ここ](https://docs.mirage.io/mirage/Mirage/index.html#type-kv_rw))

MirageOSアプリケーション構成の定義は、Functoria を使います。Functoria はファンクタを構成するための DSL(Domain Specific Language) で、[FunctoriaのREADME.md](https://github.com/mirage/functoria#how-to-write-a-configuration-file) に少し説明があります。また、使われている `foreign` 関数の定義は[Functoriaのソースコード](https://github.com/mirage/functoria/blob/master/lib/functoria.ml)にあります。

```ocaml:config.mlの9行目
 9: let stack = generic_stackv4 default_network
```

続いて9行目では `generic_stackv4` 関数を呼んで、IPV4 ネットワークスタックのインスタンスを生成して変数 `stack` に格納しています。これにより、Ethernet プロトコルと IPV4 プロトコル(ARP、DHCP、TCP、UDPなど)の基本的なパケット処理を実施するためのオブジェクトが生成されます。この行をさらに深追いしようとすると情報量がとても多くなってしまうので割愛しますが、

| 項目                    | 説明               |
|:-----------------------|:------------------|
| generic_stackv4        | 引数のインスタンスに対してEthernetプロトコルとIPV4プロトコル(ARP、DHCP、TCP、UDPなど)の基本的なパケット処理を設定するための関数               |
| default_network        | id番号0を有するMirageOS Unikernel内のEthernetデバイスインスタンス(hvt利用時には、`solo5-hvt`コマンドの引数`--net`で指定されるtapデバイスにマッピングされる)|

と考えて頂ければいいかと思います。

ちなみに、2019年4月現在では `solo5-hvt` コマンドの引数 `--net` を1つしかとれません。したがって利用できる tap デバイスは1つだけということになります。

さあ、最後に11-14行目です！

```ocaml:config.mlの11-14行目
11: let () =
12:  register "network" [
13:    main $ stack
14:  ]
```
ここの処理では生成するアプリケーションの名前を "network" として、7行目で宣言した 変数 `main` および9行目で宣言した変数 `stack` を構成要素に OCaml のエントリポイントを定義しています。

書き方としては、まずエントリポイント情報を含む `foreign` 関数で生成された変数(ここでは `main`)を一番左側に記述します。`unikernel.ml` にて引数がある場合には、対応する `config.ml` の変数(ここでは `stack`)を引数の記載順に `$` で区切りながら追加します。このあたりの書き方は、mirage-skeleton の[Webサーバアプリ](https://github.com/mirage/mirage-skeleton/tree/master/applications/static_website_tls)を参考にするといいかと思います。

ここで設定したアプリケーションの名前は、生成される Unikernel アプリケーションバイナリのファイル名に利用されます。(そういえば、`network.hvt` というファイルが生成されていました)

## 最後に
お題のネットワークアプリケーションの例を通じて、MirageOS Unikernel アプリケーションの中身を少しだけ理解できるようになったかと思います。

とはいえ「サンプルアプリを改変してみる」や「一から自分でアプリを実装してみる」はまだ難しいと思いますので、それらのお助けになるような記事を増やしていきたいと思います。