Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第33章. ネイティブ関数の登録

この章の目的

この章を読み終える頃には、スクリプトが呼ぶホスト関数を、人間工学的なルートと低レベルなルートの両方で登録できるようになります。

ネイティブ関数とは

ネイティブ関数とは、ホストが名前を付けてVMに登録するRustの関数のことです。スクリプトはuse宣言のあと、その名前で呼びます。ネイティブ関数は橋渡しです — スクリプトは何が起こるべきかを決め、ネイティブがそれを行います。ピアノロールのhost::playはネイティブです。スクリプトがそれを呼び、Rust側がオーディオ音声状態を更新します。

ネイティブ関数はVm::newのあと、スクリプトが走る前に登録します。

人間工学的なルート: register_fn

推奨されるのがregister_fnです。これは引数0個から4個までの任意のRust関数やクロージャを受け付けます。条件として、その引数と戻り値の型がKeleusmaTypeマーシャリングトレイトを実装している必要があります。プリミティブ型はすでに実装しています。

#![allow(unused)]
fn main() {
vm.register_fn("math::add", |a: i64, b: i64| -> i64 { a + b });
vm.register_fn("math::sin", |x: f64| -> f64 { libm::sin(x) });
}

引数の取り出し、引数数のチェック、戻り値の包装は自動的に行われます。失敗するかもしれない関数には、register_fn_fallibleがResult<R, VmError>の戻り値を受け付けます。

#![allow(unused)]
fn main() {
vm.register_fn_fallible("io::read_setting", |key: &str| -> Result<String, VmError> {
    fetch(key).map_err(|e| VmError::NativeError(format!("{}", e)))
});
}

Strings: take a borrowed view

A native that takes a script’s Text should declare its argument as &str, not String:

#![allow(unused)]
fn main() {
vm.register_fn("host::name_length", |s: &str| -> i64 { s.len() as i64 });
}

The borrow is valid for the call and no longer. That is not a restriction the marshalling layer invented; a script’s dynamic string lives in the arena, and the arena is reclaimed at the next resume. The lifetime simply makes the rule one the compiler checks. Copy what you need to keep before returning.

Two properties of the view matter and are easy to get wrong:

  • The length is authoritative. A Keleusma string is a byte string, so it may contain a NUL byte in the middle, and that byte is content. Do not treat the view as NUL-terminated.
  • The length counts bytes, not characters. A three-character string may be six bytes long.

An owned String argument still works and behaves as it always has:

#![allow(unused)]
fn main() {
vm.register_fn("host::name_length", |s: String| -> i64 { s.len() as i64 });
}

It is a convenience for the VM embedding only. The ahead-of-time native backend cannot hand a native an owned String without copying, so a native written against String does not carry across to that embedding, while one written against &str does. If you may ever compile your scripts ahead of time, take the borrowed view. The full contract is in docs/spec/NATIVE_STRING_ABI.md.

A borrowed string may appear in any argument position, mixed freely with owned arguments:

#![allow(unused)]
fn main() {
vm.register_fn("host::pad", |s: &str, width: i64| -> i64 {
    core::cmp::max(s.len() as i64, width)
});
}

ホストの構造体や列挙型はKeleusmaTypeを派生(derive)させることで、境界を超えられます。

#![allow(unused)]
fn main() {
#[derive(KeleusmaType, Debug, Clone)]
struct Point { x: f64, y: f64 }
}

低レベルなルート: register_native_closure

register_fnはホストの状態を取り込めません。その引数は素のシグネチャだからです。ネイティブがホストの所有する状態を読んだり書いたりする必要があるときは、register_native_closureを使います。これは生の&[Value]引数を受け取ってResult<Value, VmError>を返す、ボックスされたクロージャを取ります。

#![allow(unused)]
fn main() {
let voices = shared_voices.clone();
vm.register_native_closure("host::silence", move |args: &[Value]| {
    let channel = match args[0] {
        Value::Int(n) => n as usize,
        ref other => return Err(VmError::TypeError(
            format!("expected Int, got {:?}", other))),
    };
    voices.lock().unwrap()[channel].gate = false;
    Ok(Value::Unit)
});
}

これがピアノロールがすべてのネイティブに対して使うルートです。各ネイティブが共有のArc<Mutex<[Voice; 8]>>音声テーブルを取り込んでいるからです。クロージャは取り込んだクローンを所有し、生のValueを見ることで引数を明示的に検証できます。状態を取り込まない普通の関数ポインタは、代わりにregister_nativeを使えます。

バンドルされているライブラリ

keleusma::stddslモジュールは3つのネイティブ関数のバンドルを提供しています。それぞれ1回の呼び出しで登録します。

#![allow(unused)]
fn main() {
vm.register_library(stddsl::Math);   // math::sqrt, math::pow, ...
vm.register_library(stddsl::Audio);  // audio::midi_to_freq, ...
vm.register_library(stddsl::Shell);  // shell::getenv, shell::run, shell::sleep_ms,
                                      // shell::read_file, shell::write_file, ...
}

V0.2.1 の Shell バンドルは、環境変数アクセス(getenv、has_env、setenv)、サブプロセス実行(run、run_full、run_checked、run_timeout)、プロセス終了(exit)、タイミング(sleep_ms、now_unix_ms)、ファイル入出力(read_file、write_file、append_file、file_exists)、標準エラー出力(write_err、writeln_err)、およびホストメタデータ(pid、hostname、arg_count、arg、pwd、cd)を扱います。完全な一覧については STANDARD_LIBRARY.md を参照してください。

各バンドルは、ソース形式の use 宣言を含む公開の SIGNATURES 定数を公開します。コンパイル時の型およびアリティの検証を望むホストは、解析の前にこの定数をスクリプトソースの先頭に付加します。同梱の keleusma-cli は三つのバンドルすべてに対してこれを行います。カスタムバンドルは Library トレイトを実装し、並行する SIGNATURES 定数を公開することで、同じ検証フローに参加できます。

この章のまとめ

  • ネイティブ関数は、スクリプトが名前で呼び出すホストの Rust 関数です。
  • register_fn および register_fn_fallible は、型が KeleusmaType を実装する関数に対する人間工学的な経路です。
  • register_native_closure は、すべてのピアノロールのネイティブがそうであるように、ホストの状態をキャプチャするネイティブ向けの経路です。
  • register_library は、バンドルされた、またはカスタムのネイティブ一式をインストールします。
  • 失敗し得るネイティブの失敗は、ネイティブエラー構文 native(args) { ok(v) => ..., error(code) => ... } を用いてスクリプト側で処理されます。ホストは、フィールドを持たない列挙型のバリアントをそれらの判別子へマッピングする KeleusmaError derive で構築したエラーを返すことにより、Word エラーコードを報告します。第23章を参照してください。

次章では、yield を行うスクリプトを駆動します。