>> whoami
name: なぎ
twitter: ___x8
github: x8xx
Rust
WASM
wasmerでランタイム上からホスト関数を呼び出すメモ
注意) 絶対他に最適な方法があると思うんだけど, まぁ動くのでヨシ
コードはここ
やりたいこと
WASMランタイムからホストのメモリ領域のデータを取りたい!!!!!!
メモリ領域をいい感じに共有できる方法があると思うけど, どうすればいいのか分からない
そこでホストでデータを取ってくる関数を実装して, そいつを呼び出したらいいじゃんってなったので, とりあえず実装してみた話
ランタイムにはwasmerを使った
wasmプログラムの用意
プログラム書く前にwasm32-wasiのターゲットを追加しておきます
rustup target add wasm32-wasi
計算するだけのプログラムを書きます.
readがホスト環境で実装される関数です.
i64にキャストされたポインタが, 実行時にcalc関数に渡されるので, それをreadに渡してあげればホストからデータが取れます.
#![allow(unused)] #![no_main] fn main() { extern { pub fn read(pointer: i64, offset: u8) -> i32; } #[no_mangle] pub fn calc(pointer: i64, input: u8) -> i32 { let val = unsafe { read(pointer, 2) }; input as i32 + val } }
コンパイルします. そのままじゃ"read"の実装がないやんけ!って怒られるので, --emit=objをつけて黙らせます
rustc -O --emit=obj --target wasm32-wasi calc.rs
できたcalc.o(wasm)をwatに変換して見てみる
# wasm2wat calc.o
(module
(type (;0;) (func (param i64 i32) (result i32)))
(import "env" "__linear_memory" (memory (;0;) 0))
(import "env" "read" (func (;0;) (type 0)))
(func $calc (type 0) (param i64 i32) (result i32)
local.get 0
i32.const 2
call 0
local.get 1
i32.const 255
i32.and
i32.add))
これでできてる気がしますが, exportがないのでランタイムからcalcを呼べません.
コンパイラくんにexportを付与してもらう方法が, 分からなかったので無理やりつけます.
wasm2wat calc.o | sed '$ s/)$/\(export "calc" \(func $calc\)\)\)/' > tmp.wat
wat2wasm tmp.wat -o calc.wasm
wasm2watとwat2wasmは, wabtにあります.
ランタイムを用意して実行
use std::fs::File; use std::io::prelude::*; use wasmer::{ Store, Module, Function, Memory, MemoryType, Instance, imports, Value }; fn main() { /** * wasmファイル読み込み */ let mut f = File::open("../wasm/calc.wasm").unwrap(); let metadata = std::fs::metadata("../wasm/calc.wasm").unwrap(); let mut wasm = vec![0;metadata.len() as usize]; f.read(&mut wasm).unwrap(); /** * wasmから呼び出すホスト関数と仮データの用意 */ let mut data: Vec<u8> = Vec::new(); data.push(0); data.push(1); data.push(2); data.push(3); let data_ptr = data.as_ptr(); fn read(pointer: i64, offset: u8) -> i32 { unsafe { // 生ポインタに戻してアクセスする (*(pointer as *const u8).offset(offset as isize)) as i32 } } /** * wasmer用意 */ // storeの役割がいまいちわからない // とりあえず分けてみた let store = Store::default(); let read_fn_store = Store::default(); let memory_store = Store::default(); let module = Module::from_binary(&store, &wasm).unwrap(); let read_fn = Function::new_native(&read_fn_store, read); let linear_memory = Memory::new(&memory_store, MemoryType::new(1, None, false)).unwrap(); // ホストのread関数をimportしてあげる let import_object = imports! { "env" => { "read" => read_fn, "__linear_memory" => linear_memory, }, }; let instance = Instance::new(&module, &import_object).unwrap(); /** * 実行フェイズ */ let calc_fn = instance.exports.get_function("calc").unwrap(); // 引数を設定 let mut calc_args: Vec<Value> = Vec::new(); calc_args.push(Value::I64(data_ptr as i64)); calc_args.push(Value::I32(20)); // 実行 let result = calc_fn.call(&calc_args).unwrap(); // result: 22 println!("result: {}", result[0].unwrap_i32()); }
おわりだよー
Rustのスライスとポインタの速度比較
どんぐらい違うのかなーって気になったので, 適当に実験した軽いメモ
1000万回回したらこんな感じ
Start!
slice_time: 0.666
pointer_time: 0.485
43175
39986
コードはこれ
変に最適化されないようにしたつもり
use std::time::Instant;
use rand::Rng;
fn main() {
// 試行回数
let count = 10000000;
// バッファサイズ
let size = 100000;
let mut rng = rand::thread_rng();
// 2つのバッファ用意して適当に値放り込んどく
let mut buf1: Vec<u8> = Vec::new();
let mut buf2: Vec<u8> = Vec::new();
for _ in 0..size {
buf1.push(rng.gen_range(0..255));
buf2.push(rng.gen_range(0..255));
}
// スライスとポインタ用意するよ
let slice_buf = &buf1;
let pointer_buf = buf2.as_ptr();
// アクセス先のindexを事前に用意
let mut index_list: Vec<usize> = Vec::new();
for _ in 0..count {
index_list.push(rng.gen_range(0..size));
}
// アクセスした値を放り込む箱用意しとくよ
let mut slice_result: Vec<u8> = vec![0;count];
let mut pointer_result: Vec<u8> = vec![0;count];
println!("Start!");
// スライスアクセス計測
let slice_start = Instant::now();
for (i, index) in index_list.iter().enumerate() {
slice_result[i] = slice_buf[*index];
}
let slice_end = slice_start.elapsed();
println!("slice_time: {}.{:03}", slice_end.as_secs(), slice_end.subsec_nanos() / 1_000_000);
// ポインタアクセス計測
let pointer_start = Instant::now();
for (i, index) in index_list.iter().enumerate() {
unsafe {
pointer_result[i] = *pointer_buf.offset(*index as isize);
}
}
let pointer_end = pointer_start.elapsed();
println!("pointer_time: {}.{:03}", pointer_end.as_secs(), pointer_end.subsec_nanos() / 1_000_000);
// なんか処理しとかないと, 最適化で消える気がするので適当に
println!("{}", slice_result.iter().filter(|&v| *v == 100).count());
println!("{}", pointer_result.iter().filter(|&v| *v == 100).count());
}