juniper による GraphQL の処理を WebAssembly 化する

juniper を使った GraphQL の処理を WebAssembly 化し、Deno 用の JavaScript で実行してみました。

ソースコードhttps://github.com/fits/try_samples/tree/master/blog/20220224/

はじめに

今回は wasm-bindgenwasm-pack を使いません。(wasm-bindgen 版のサンプルは こちら

そのため、これらの便利ツールが上手くやってくれている箇所を自前で実装する必要があり、以下が課題となります。

  • 所有権による値の破棄にどう対処するか

ここでは、Box::into_raw を使う事でメモリー解放(値の破棄)の実行責任を呼び出し側(今回は JavaScript)に移す事で対処します。

(1) GraphQL 処理の WebAssembly 化

Cargo.toml は以下のように設定しました。

Cargo.toml
[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
juniper = "0.15"

GraphQL 処理

GraphQL の処理は juniper を使って普通に実装します。 HashMap を使って id 毎に Item を管理するだけの単純な作りにしました。

src/lib.rs (GraphQL 処理部分)
use juniper::{execute_sync, EmptySubscription, FieldError, FieldResult, Variables};

use std::collections::HashMap;
use std::sync::RwLock;

#[derive(Default, Debug)]
struct Store {
    store: RwLock<HashMap<String, Item>>,
}

impl juniper::Context for Store {}

#[derive(Debug, Clone, juniper::GraphQLObject)]
struct Item {
    id: String,
    value: i32,
}

#[derive(Debug, Clone, juniper::GraphQLInputObject)]
struct CreateItem {
    id: String,
    value: i32,
}

・・・

#[derive(Debug)]
struct Query;

#[juniper::graphql_object(context = Store)]
impl Query {
    fn find(ctx: &Store, id: String) -> FieldResult<Item> {
        ・・・
    }
}

#[derive(Debug)]
struct Mutation;

#[juniper::graphql_object(context = Store)]
impl Mutation {
    fn create(ctx: &Store, input: CreateItem) -> FieldResult<Item> {
        ・・・
    }
}

type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Store>>;

・・・

WebAssembly 用の処理

ここからが本題です。

基本的に、WebAssembly とランタイム(今回は JavaScript のコード)間で文字列等を直接受け渡したりはできません。

共有メモリー ※ にデータを書き込み、その位置(ポインタ)やバイトサイズをやり取りする事で文字列等の受け渡しを実施する事になります。

 ※ JavaScript 側だと WebAssembly インスタンスの exports.memory.buffer

ここでは、JavaScript からメモリー領域の確保や破棄を実施するために以下のような処理を用意しました。

  • (a) データを保持する HashMap と GraphQL のスキーマを含む Context の生成と破棄
  • (b) GraphQL の入力文字列を書き込む領域の確保と破棄
  • (c) GraphQL 処理結果(文字列)の破棄とポインタやサイズの取得 ※

なお、(a) の Context や (b) と (c) の文字列が Rust(WebAssembly)側で勝手に破棄されては困るので、そうならないように Box::into_raw を使います。

Box::into_raw で取得した raw pointer は Box::from_raw で Box へ戻して処理する事になりますが、そのままだとスコープを外れた際にデストラクタ(drop メソッド)が呼び出されて破棄されてしまうので、そうされたくない場合は再度 Box::into_raw します。

query の戻り値である *mut String 型の raw pointer は、文字列を格納している位置では無いため、(c) では文字列の格納位置(ポインタ)を _result_ptr で、そのバイトサイズを _result_size でそれぞれ取得するようにしてみました。

また、(b) では slice を使う事で、文字列の位置をやり取りできるようにしています。

ちなみに、関数名の先頭に _ を付けたりしていますが、特に意味はありません。(単なる見た目上の区別のためだけ)

これでとりあえずは動作しましたが、処理的に何か問題があるかもしれませんのでご注意ください。

src/lib.rs (WebAssembly 用の処理部分)
・・・
struct Context {
    context: Store,
    schema: Schema,
}

// (a) Context の生成
#[no_mangle]
extern fn open() -> *mut Context {
    let context = Store::default();
    let schema = Schema::new(Query, Mutation, EmptySubscription::new());

    let d = Box::new(Context{ context, schema });
    // Context の raw pointer を返す
    Box::into_raw(d)
}
// (a) Context の破棄
#[no_mangle]
extern fn close(ptr: *mut Context) {
    unsafe {
        Box::from_raw(ptr);
    }
}

// (b) GraphQL 入力文字列の領域を確保
#[no_mangle]
extern fn _new_string(size: usize) -> *mut u8 {
    let v = vec![0; size];

    Box::into_raw(v.into_boxed_slice()) as *mut u8
}
// (b) GraphQL 入力文字列の破棄
#[no_mangle]
extern fn _drop_string(ptr: *mut u8) {
    unsafe {
        Box::from_raw(ptr);
    }
}

// (c) GraphQL 結果文字列の破棄
#[no_mangle]
extern fn _drop_result(ptr: *mut String) {
    unsafe {
        Box::from_raw(ptr);
    }
}
// (c) GraphQL 結果文字列のポインタ取得
#[no_mangle]
extern fn _result_ptr(ptr: *mut String) -> *const u8 {
    unsafe {
        let s = Box::from_raw(ptr);
        let r = s.as_ptr();
        // 結果文字列を破棄させないための措置
        Box::into_raw(s);

        r
    }
}
// (c) GraphQL 結果文字列のバイトサイズ取得
#[no_mangle]
extern fn _result_size(ptr: *mut String) -> usize {
    unsafe {
        let s = Box::from_raw(ptr);
        let r = s.len();
        // 結果文字列を破棄させないための措置
        Box::into_raw(s);

        r
    }
}

// GraphQL のクエリー処理
#[no_mangle]
extern fn query(ptr: *mut Context, sptr: *const u8, len: usize) -> *mut String {
    unsafe {
        // Context の取得
        let ctx = Box::from_raw(ptr);

        // GraphQL の入力文字列を取得
        let slice = std::slice::from_raw_parts(sptr, len);
        let q = std::str::from_utf8_unchecked(slice);

        // GraphQL の処理実行
        let r = execute_sync(q, None, &ctx.schema, &Variables::new(), &ctx.context);

        // 処理結果の文字列化
        let msg = match r {
            Ok((v, _)) => format!("{}", v),
            Err(e) => format!("{}", e),
        };
        // Context を破棄させないための措置
        Box::into_raw(ctx);
        // 結果文字列の raw pointer を返す
        Box::into_raw(Box::new(msg))
    }
}

ビルド

WASI を使っていないので --target を wasm32-unknown-unknown にしてビルドします。

ビルド例
> cargo build --release --target wasm32-unknown-unknown

(2) ランタイムの実装

(1) で作成した WebAssembly を呼び出す処理を Deno 用の JavaScript で実装してみました。

run_wasm_deno.js
const wasmFile = 'target/wasm32-unknown-unknown/release/sample.wasm'
// 処理結果の取得
const toResult = (wasm, retptr) => {
    const sptr = wasm.exports._result_ptr(retptr) 
    const len = wasm.exports._result_size(retptr)

    const memory = wasm.exports.memory.buffer

    const buf = new Uint8Array(memory, sptr, len)
    // JavaScript 上で文字列化
    const res = new TextDecoder('utf-8').decode(buf)

    return JSON.parse(res)
}

const query = (wasm, ptr, q) => {
    const buf = new TextEncoder('utf-8').encode(q)
    // 入力文字列用の領域確保
    const sptr = wasm.exports._new_string(buf.length)

    try {
        // 入力文字列の書き込み
        new Uint8Array(wasm.exports.memory.buffer).set(buf, sptr)
        // GraphQL の実行
        const retptr = wasm.exports.query(ptr, sptr, buf.length)

        try {
            return toResult(wasm, retptr)
        } finally {
            // 処理結果の破棄
            wasm.exports._drop_result(retptr)
        }
    } finally {
        // 入力文字列の破棄
        wasm.exports._drop_string(sptr)
    }
}

const buf = await Deno.readFile(wasmFile)
const module = await WebAssembly.compile(buf)
// WebAssembly のインスタンス化
const wasm = await WebAssembly.instantiate(module, {})

// Context の作成
const ctxptr = wasm.exports.open()
// GraphQL を処理して結果を出力
const queryAndShow = (q) => {
    console.log( query(wasm, ctxptr, q) )
}

try {
    queryAndShow(`
        mutation {
            create(input: { id: "item-1", value: 12 }) {
                id
            }
        }
    `)

    queryAndShow(`
        mutation {
            create(input: { id: "item-2", value: 34 }) {
                id
            }
        }
    `)

    queryAndShow(`
        query {
            find(id: "item-1") {
                id
                value
            }
        }
    `)

    queryAndShow(`
        {
            find(id: "item-2") {
                id
                value
            }
        }
    `)

    queryAndShow(`
        {
            find(id: "item-3") {
                id
            }
        }
    `)
} finally {
    // Context の破棄
    wasm.exports.close(ctxptr)
}

実行

実行すると以下のような結果になりました。

実行例
> deno run --allow-read run_wasm_deno.js
{ create: { id: "item-1" } }
{ create: { id: "item-2" } }
{ find: { id: "item-1", value: 12 } }
{ find: { id: "item-2", value: 34 } }
null