先日のブログに書いた通り、いつ以来かのセルラオートマトンに手を出しています。 そのときの記事の実装では、更新処理に Rust を利用してはいるものの、世代ごとにimmutableなバイト列を生成していました。
Erlang のアロケータを利用するので、確保したメモリの解放忘れのような心配はありませんが、効率を考えるとあまりうれしくない。
というわけで。 今回は、バイト列は Rust で確保し、関数の呼び出しを介して Elixir からそのバイト列を操作してみよう、という回です。
ハンドルあるいはリファレンス
まず一般的な話として。 一方で確保したリソースを、もう一方で利用するばあい、ハンドルを渡すことが常套手段として考えられます。
今回のケースであれば、Rust でメモリ領域を確保し、その情報をハンドルとして Elixir に渡し、Elixir は受け取ったハンドルとその他のパラメータを引数にして Rust の関数を呼び出す、という手順です。
Rustler にはちょうど、Rust でファイルのオープンと読み込みを実装し、それを Elixir から利用するというサンプルが公開されています。 ファイルハンドルを Rust と Elixir の間でやり取りすることで、Elixir から Rust のファイル操作を利用することができるようになっています。
ちなみに。 ここまでハンドルという言葉を使いましたが、このような用途の値はElixirではリファンレンス型の値として扱われますので、以下リファレンスという言葉で話を進めることにします。
MutableBinary.NIF モジュール
今回も Rust との接続には Rustler を使います。
コード量はそれほど多いわけではありませんが、Elixir と Rust の両方にまたがって実装するので、設定などのためにファイルの数が増えてしまいます。 コード全体は GitHub に上げましたのでそちらを参照ください。
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 の実装は多くはありません。
Rustler
を use
しエントリとなる関数を定義するだけで終わってしまいます。
エントリの関数の定義も、単に 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.コンウェイの著作。
新装改題され昨年末に出版されたもの。 書店で偶然見つけ、何かの縁を感じて購入。