📦

cargo-component:生成されるコードでの所有権

2024/01/27に公開

cargo-comopnentをつかってRustのコードを生成する場合、生成されたデーター構造の表現を都合に合わせて変えたい場合があります。

TL;DR

  • 標準では、所有権を移動するようにコードが生成されます
  • Cargo.tomlに設定を記述することで、所有権の移動をしないようにコード生成できます
  • 生成されるコードはWITに定義されるインポートする関数と、エキスポートされる関数の型によって変わります

所有権の設定について

次のようなデータ構造をWITで定義したとします。

record scan-line {
    filter-type: filter-type,
    data: list<u8>,
}

enum filter-type {
    none,
    sub,
    up,
    average,
    paeth,
}

この実現方法は次の2つがあるかと思います。所有するパターンはlist<u8>Vec<u8>として解釈するか、それともu8のスライスとして解釈するかが違いです。cargo-componentは前者として解釈するのが標準です。これはWasmコンポーネントは使用しているメモリーを隠蔽するというデザインと、WITで定義されているデーター構造はコンポーネントの外部に露出することを考えると妥当な振る舞いのように感じます。

// 所有するパターン
struct ScanLine {
    filter_type: FilterType,
    data: Vec<u8>
}

// 借用するパターン
struct ScanLine<'a> {
    filter_type: FilterType,
    data: &'a [u8]
}

借用するパターンで表現したい場合もある

上記のScanLineという構造体は画像の各行を表しています。画像自体はバイト列となっており、ScanLineはバイト列の部分に対するビューを定義しています。コンポーネントの外部にビューを露出しない場合は、後者の借用するパターンで実装することも多いと思います。

トリッキーなのは、ビューに対する一連の処理の中にコンポーネント外の関数がある場合です。コンポーネント内部の処理のためには元のデーターを借用する形でビューを定義したいが、コンポーネントの外とやりとりする時には元のデーターを露出できないという制約を満たさなくてはなりません。

cargo-componentはこのような制約を満たすようなコードを生成することもできます。そのためには、Cargo.tomlに次のような設定を追加します。

[package.metadata.component.bindings]
ownership = "borrowing-duplicate-if-necessary"

生成されるコードは、その使われ方によって変化します。WITで定義されるデーター構造の使われ方を整理すると次の3パターンになるかと思います。インポートされる関数とエキスポートされる関数が、どのパターンに当てはまるかによって生成されるコードが変化します。

  • パターン1:関数の返り値となる場合
  • パターン2:関数の引数となる場合
  • パターン3:パターン1とパターン2の両方を満たす場合

パターン1:関数の返り値となる場合

このパターンは、次のような使われ方をする場合です。

world default {
    use types.{ scan-line };
    export run: func(data: list<u8>) -> scan-line;
    import parse: func(data: list<u8>) -> scan-line;
}

最終成果物としてのみ利用されるため、所有するパターンで生成されます。

#[derive(Clone)]
pub struct ScanLine {
    pub filter_type: FilterType,
    pub data: ::cargo_component_bindings::rt::vec::Vec::<u8>,
}

パターン2:関数の引数となる場合

例えば次のようなワールドの定義があったとします。これから生成されるScanLineに対する処理の最後に、外部からインポートされるnotifyという関数を呼び出すことを想定しています。

world default {
    use types.{ scan-line }
    import notify: func(line: scan-line);
}

この場合ScanLineは、次のように生成されます。data属性が借用する形で定義されているのがわかります。

#[derive(Clone)]
pub struct ScanLine<'a,> {
    pub filter_type: FilterType,
    pub data: &'a [u8],
}

外部には使用しているメモリーを露出しないという制約は、modify関数の実装によって満たされます。次のように、与えられたScanLineオブジェクトのコピー作り、このコピーを与えてインポートした関数を呼び出すことで、元のScanLineオブジェクトがコンポーネント外部に露出することを防いでいます。

pub fn notify(line: ScanLine<'_,>,){

  #[allow(unused_imports)]
  use ::cargo_component_bindings::rt::{alloc, vec::Vec, string::String};
  unsafe {
    let component::guest::types::ScanLine{ filter_type:filter_type0, data:data
0, } = line;
    let vec1 = data0;
    let ptr1 = vec1.as_ptr() as i32;
    let len1 = vec1.len() as i32;

    #[cfg(target_arch = "wasm32")]
    #[link(wasm_import_module = "$root")]
    extern "C" {
      #[link_name = "modify"]
      fn wit_import(_: i32, _: i32, _: i32, );
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn wit_import(_: i32, _: i32, _: i32, ){ unreachable!() }
    wit_import(filter_type0.clone() as i32, ptr1, len1);
  }
}

パターン3:関数の返り値にも使われ、引数にもなる場合

最後のパターンは、次のような場合です。

world default {
    use types.{ scan-line };
    import notify: func(line: scan-line);
    export run: func(data: list<u8>) -> scan-line;
}

次のような場合も、このパターンに当てはまります。

world default {
    use types.{ scan-line };
    import modify: func(line: scan-line) -> scan-line;
}

このパターンの場合、scan-lineの定義から次の2つの構造体が定義されます。名前から推測されるように、前者が返り値として利用されるデーター型で、後者が引数として与えるためのデーター型となります。

#[derive(Clone)]
pub struct ScanLineResult {
  pub filter_type: FilterType,
  pub data: ::cargo_component_bindings::rt::vec::Vec::<u8>,
}

#[derive(Clone)]
pub struct ScanLineParam<'a> {
  pub filter_type: FilterType,
  pub data: &'a [u8],
}

上記のmodify関数は次のように、ScanLineParamScanLineResultに移す関数として定義されます

pub fn modify(line: ScanLineParam<'_,>,) -> ScanLineResult{
    // 中略
}

まとめ

cargo-componentはpackage.metadata.component.bindings.ownershipの値と、ワールドの定義から生成するコードを変えることがあります。

多くのユースケースは標準の設定で十分カバーできるようにも思いますが、データーの一部分を別のコンポーネントで繰り返し処理させるような場合などは、borrowing-duplicate-if-necessaryを設定するとコードが書きやすくなるように思います。

borrowing-duplicate-if-necessaryの振る舞いは、wit-bindgenの振る舞いと同様かとは思います。

https://github.com/bytecodealliance/wit-bindgen/blob/main/crates/rust/src/lib.rs#L147-L157

ただwit-bindgenにはあるborrowingの設定がcargo-componentからは消えています。この消えた背景については不明ですが、別の名前が付いていた方が用途が明確になって事故が起きにくくなることから妥当な変更なのかな、とも感じています。

なおcargo-componentは生成したコードをsrc/bindings.rsに出力します。生成されたコードを確認されることで、ご自身で記事の内容を検証いただけます。

レファレンス

Discussion