juniper による GraphQL の処理を WebAssembly 化する
juniper を使った GraphQL の処理を WebAssembly 化し、Deno 用の JavaScript で実行してみました。
ソースコードは https://github.com/fits/try_samples/tree/master/blog/20220224/
はじめに
今回は wasm-bindgen と wasm-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