>> 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());
}