blog.anqou.net
rss
author
tags

OCaml 5 + Eio + musl でシングルバイナリ HTTP サーバを作る

Go を使うと動的ライブラリに依存しない(シングルバイナリな)実行バイナリを作ることができます。 libc の代わりに musl を使うようにセットアップした OCaml コンパイラを使うと、いくつかハマりポイントはありながらも、OCaml でも同じことができることが分かったので、 HTTP サーバをお題としてやってみます。

なお Eio で直接 HTTP サーバを立てるのは若干面倒なので、拙作のライブラリである Yume を使います。Yume は Dream にインスパイアされたライブラリで、 Eio を使った HTTP サーバや HTTP(S) クライアントを簡単に立ち上げられます。以前書いた記事も参考にしてください。

検証環境は Ubuntu 24.04 LTS で、OCaml コンパイラのバージョンは 5.2.0 です。

#OCaml コンパイラを用意する

まず opam を使って OCaml コンパイラを用意します。 opam switch createocaml-option-muslocaml-option-static を渡すと、シングルバイナリを作ることができる OCaml コンパイラの環境を作ることができます:

opam switch create . --no-install ocaml-option-musl ocaml-option-static

#HTTP サーバのコードを書く

適当に dune init project してプロジェクトを作ります。その上で、以下のようなコードを書きます:

(* bin/main.ml *)
let handler =
  let open Yume.Server in
  Router.(use [ get "/" (fun _ _ -> respond "hello") ]) default_handler

let () =
  Eio_posix.run @@ fun env ->
  Eio.Switch.run @@ fun sw ->
  let listen =
    Eio.Net.getaddrinfo_stream ~service:"38000" env#net "localhost" |> List.hd
  in
  Yume.Server.start_server env ~sw ~listen handler @@ fun _socket -> ()

#opam ファイルを書く

opam ファイルは以下のような感じにしておきます:

# *.opam
# ...

depends: [
  "dune" {>= "3.16"}
  "eio"
  "eio_posix"
  "ocaml" {>= "5.0"}
  "yume"
]

# ...

pin-depends:[
  [ "multipart_form-eio.0.6.0" "git+https://github.com/ushitora-anqou/multipart_form.git#1bca726ecea0cb4cf253e65ae02d348015a1ef06" ]
  [ "yume.0.1.10" "git+https://github.com/ushitora-anqou/yume.git#0.1.10" ]
]

ポイントは eio_main に依存しないことです。 eio_main に依存すると、Eio の Linux バックエンド(eio_linux)が有効化され io_uring を使おうとしますが、musl 経由では Linux のヘッダファイルが見えないのでビルドに失敗します。代わりに Posix バックエンド(eio_posix)を使うようにしておきます。同様の理由で、yume の 0.1.9 までは依存先に(誤って)eio_main を含めていたので使えません。 0.1.10 以降を使ってください。また yume が依存する multipart_form-eio は依存先に eio_main が含まれているので、適当にフォークして依存を外したものを使います[1]

#zarith をインストールする

この状況で opam install . --deps-only すると(主に yume から)必要なライブラリがインストールされますが、途中で zarith のインストールに失敗します。 zarith は gmp を要求するのですが、apt で入る gmp は musl から使えないようです。仕方がないので自前でビルドし、できたディレクトリを指定して zarith のインストールをやり直すとうまく行きます(参考):

$ cd gmp-6.3.0
$ CC=musl-gcc ./configure --prefix /tmp/gmp-prefix
$ make
$ make install
$ cd /path/to/original/code
$ CPPFLAGS=-I/tmp/gmp-prefix/include LDFLAGS=-L/tmp/gmp-prefix/lib opam install zarith

zarith のインストール後に再び opam install . --deps-only すると、全ての依存ライブラリがインストールされます。

#ビルドする

bin/dune ファイルで static なバイナリを作るように (flags (:standard -cclib -static)) を指定します(参考):

(executable
 ...

 (name main)
 (libraries eio eio_posix yume)
 (flags (:standard -cclib -static))

 ...
)

そのうえで dune build を打つと、動的ライブラリに依存しない実行バイナリを作ることができます:

$ dune build

$ ldd _build/default/bin/main.exe
	not a dynamic executable

#注釈

  1. opam ファイルの pin-depends は推移律を満たすように動いてはくれないので、仮に yume で multipart_form-eio を pin していても、結局 yume を使う側でも pin する必要があります(参考)。