エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixirでmutableなバイト列を扱う(Rustの力を借りて)

先日のブログに書いた通り、いつ以来かのセルラオートマトンに手を出しています。 そのときの記事の実装では、更新処理に Rust を利用してはいるものの、世代ごとにimmutableなバイト列を生成していました。

Erlang のアロケータを利用するので、確保したメモリの解放忘れのような心配はありませんが、効率を考えるとあまりうれしくない。

というわけで。 今回は、バイト列は Rust で確保し、関数の呼び出しを介して Elixir からそのバイト列を操作してみよう、という回です。

ハンドルあるいはリファレンス

まず一般的な話として。 一方で確保したリソースを、もう一方で利用するばあい、ハンドルを渡すことが常套手段として考えられます。

今回のケースであれば、Rust でメモリ領域を確保し、その情報をハンドルとして Elixir に渡し、Elixir は受け取ったハンドルとその他のパラメータを引数にして Rust の関数を呼び出す、という手順です。

Rustler にはちょうど、Rust でファイルのオープンと読み込みを実装し、それを Elixir から利用するというサンプルが公開されています。 ファイルハンドルを Rust と Elixir の間でやり取りすることで、Elixir から Rust のファイル操作を利用することができるようになっています。

github.com

ちなみに。 ここまでハンドルという言葉を使いましたが、このような用途の値はElixirではリファンレンス型の値として扱われますので、以下リファレンスという言葉で話を進めることにします。

MutableBinary.NIF モジュール

今回も Rust との接続には Rustler を使います。

hex.pm

コード量はそれほど多いわけではありませんが、Elixir と Rust の両方にまたがって実装するので、設定などのためにファイルの数が増えてしまいます。 コード全体は GitHub に上げましたのでそちらを参照ください。

github.com

GitHubリポジトリは、依存パッケージに直接指定することができます。 もし興味がありましたら、新しくプロジェクトを作成して試してみてください。

# mix.exs

  defp deps do
    [
      {:mutable_binary, github: "mattsan/mutable_binary"}
    ]
  end

以降では、注目するコードを抜粋して見ていきたいと思います。

Rust の実装

Rust の実装から見てゆきましょう。

ファイルは、リポジトリnative/mutable_binary_nif/src/lib.rs です。

構造体定義

まず可変なバイト列を格納する構造体を定義します。

後述する初期化のコードで、マクロを利用して Elixir との間で値をやり取りするために必要な種々のコードを生成する…ようです。 ドキュメントの記述が少ないのと、わたしが Rust を学んでいる途中ということで、深いツッコミは今回のところはご容赦を。

struct MutableBinaryResource {
    pub stream: Mutex<Vec<u8>>,
}

バイト列を生成する

先に定義した構造体を利用してバイト列を生成します。

このとき、構造体の値を rustler::resource::ResourceArc という構造体で包みます。 これは、ドキュメントに「std::sync::Arc のようなもの」とあるように、リファレンスカウンタでリソースを管理する構造体のようです。 Drop トレイトが実装されていてガベージコレクションによって解放されるとのこと。

Elixir で扱える形式に変換できる値には rustler::types::Encoder トレイトが実装されていて、encode で変換できるようになっています。

ここでは Rust のタプルを Elixir のタプルに変換するために利用しています。

#[rustler::nif]
fn new(env: Env, size: usize) -> Term {
    if size > 0 {
        let resource = ResourceArc::new(MutableBinaryResource {
            stream: Mutex::new(vec![0; size]),
        });

        (atoms::ok(), resource).encode(env)
    } else {
        (atoms::error(), atoms::out_of_range()).encode(env)
    }
}

リソースを確保できたばあい、アトム :ok とリソースのタプルを返しますが、これを Elixir では アトム :ok とリファレンスの値のタプルとして受け取ります。 確保したバイト列の操作をするときに、このリファレンスを関数の引数として渡します。

指定した位置のバイト値を読み出す

リファレンスとインデクスを引数に受け取り、インデクスの位置のバイトの値を返します。

Elixir が渡したリファレンスは、Rust では元のリソースの形式である ResourceArc<MutableBinaryResource> として受け取ることになります。

この値をロックして元々のバイト列の構造体を取り出します。

インデクスがバイト列の範囲内のばあいは、アトム :ok とバイト列のインデクスで指定されたバイトの値のタプルを encode して返します。

#[rustler::nif]
fn get(env: Env, resource: ResourceArc<MutableBinaryResource>, index: usize) -> Term {
    let resource_struct = resource.stream.try_lock().unwrap();

    if index < resource_struct.len() {
        (atoms::ok(), resource_struct[index]).encode(env)
    } else {
        (atoms::error(), atoms::out_of_range()).encode(env)
    }
}

書き込む、長さを取得する、バイナリとして取得する

これ以外の操作も、基本的に手順になります。

Elixir 側でリファレンスとして渡される ResourceArc<MutableBinaryResource> を受け取り、ロックして Rust の値として取り出し、操作する。

リポジトリに置いたコードには、get の他に set, length, to_string という関数を定義しています。 これらの詳細はリポジトリを参照してみてください。

初期化する

構造体の定義のところで書いたように、マクロを使って必要なコードを生成します。 そしてそのコードを実装に持つ関数を定義します。

fn load(env: Env, _: Term) -> bool {
    rustler::resource!(MutableBinaryResource, env);
    true
}

この関数は、公開する関数と一緒に NIF を初期化する関数に渡されます。

rustler::init!(
    "Elixir.MutableBinary.NIF",
    [new, length, set, get, to_string],
    load = load
);

これで Rust 側の実装は終わりました。

Elixir の実装

次に Elixir の実装です。

ファイルはリポジトリlib/mutable_binary/nif.ex です。

Rust の実装との接続は Rustler が面倒を見てくれるので、Elixir の実装は多くはありません。 Rustleruse しエントリとなる関数を定義するだけで終わってしまいます。 エントリの関数の定義も、単に NIF をロードできなかったばあいのためにエラーを返すだけのシンプルなものです。

defmodule MutableBinary.NIF do
  use Rustler, otp_app: :mutable_binary, crate: :mutable_binary_nif

  def new(_), do: err()
  def length(_), do: err()
  def get(_, _), do: err()
  def set(_, _, _), do: err()
  def to_string(_), do: err()

  defp err, do: :erlang.nif_error(:nif_not_loaded)
end

実演

簡単なスクリプトを書いて実行してみます。

# sample.exs

{:ok, ref} = MutableBinary.NIF.new(16)

MutableBinary.NIF.length(ref) |> IO.inspect(label: "length")
MutableBinary.NIF.to_string(ref) |> IO.inspect(label: "to_string")

MutableBinary.NIF.get(ref, 0) |> IO.inspect(label: "get [0]")
MutableBinary.NIF.set(ref, 0, 123) |> IO.inspect(label: "set [0] <- 123")
MutableBinary.NIF.get(ref, 0) |> IO.inspect(label: "get [0]")
MutableBinary.NIF.get(ref, 16) |> IO.inspect(label: "get [16]")

MutableBinary.NIF.set(ref, 0, 69)
MutableBinary.NIF.set(ref, 1, 108)
MutableBinary.NIF.set(ref, 2, 105)
MutableBinary.NIF.set(ref, 3, 120)
MutableBinary.NIF.set(ref, 4, 105)
MutableBinary.NIF.set(ref, 5, 114)
MutableBinary.NIF.set(ref, 6, 33)
MutableBinary.NIF.set(ref, 7, 32)
MutableBinary.NIF.set(ref, 8, 38)
MutableBinary.NIF.set(ref, 9, 32)
MutableBinary.NIF.set(ref, 10, 82)
MutableBinary.NIF.set(ref, 11, 117)
MutableBinary.NIF.set(ref, 12, 115)
MutableBinary.NIF.set(ref, 13, 116)
MutableBinary.NIF.set(ref, 14, 33)
MutableBinary.NIF.set(ref, 15, 33)

MutableBinary.NIF.to_string(ref) |> IO.inspect(label: "to_string")

実行に先立って、パッケージの取得とコンパイル

$ mix do deps.get, compile

mix runスクリプトを実行します。

$ mix run sample.exs
length: 16
to_string: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
get [0]: {:ok, 0}
set [0] <- 123: :ok
get [0]: {:ok, 123}
get [16]: {:error, :out_of_range}
to_string: "Elixir! & Rust!!"

何も不思議なところはないはずなのですが、Elixir で mutable な値を利用できるというだけで、ちょっと新鮮な感じがしてくるので不思議です。

ちなみに。 GitHub に上げたコードには、MutableBinary.NIF モジュールを背後に隠して、外部とインタフェースする MutableBinary モジュールを用意しています。

いつか読むはずっと読まない:素数が香り、形がきこえる

ライフゲイム Conway's Game of Life で知られる J.H.コンウェイの著作。

新装改題され昨年末に出版されたもの。 書店で偶然見つけ、何かの縁を感じて購入。