先日のブログに書いた通り、いつ以来かのセルラオートマトンに手を出しています。
そのときの記事の実装では、更新処理に 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 のリポジトリは、依存パッケージに直接指定することができます。
もし興味がありましたら、新しくプロジェクトを作成して試してみてください。
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
実演
簡単なスクリプトを書いて実行してみます。
{: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.コンウェイの著作。
新装改題され昨年末に出版されたもの。
書店で偶然見つけ、何かの縁を感じて購入。