Regarding your questions
- 是的,
offset
是从WASM存储器开始的偏移量.它就像WASM存储器中的指针inside.试图将offset
作为主机Rust应用程序内的正常指针访问,很可能会导致段错误.
- 您的主机Rust应用程序的变量与WASM内存分开.它们不会自动成为WASM内存内容的一部分.这就是WASM的全部诀窍.所有内容都必须显式复制到WASM内存中,或者从WASM内存中读取并写入主机应用程序的变量.
- 如果您需要WASM实例内部的内存,则必须在那里手动分配内存,例如使用导出的
malloc()
函数(稍后将详细介绍).对于每次分配,您需要知道需要多少字节.
How to read a string from a TinyGo WASM module
- 当跨越WASM边界[0,1]时,TinyGo将字符串编码为
(ptr, len)
元组.
- 由于返回非平凡类型是一个相当新的特性[2],TinyGo使用了一个变通方法(可能是为了向后兼容):它不返回
(ptr, len)
元组,而是要求you传递一个指向空闲内存段/缓冲区的指针,在那里它可以存储(ptr, len)
元组.因为ptr
和len
属于i32
类型,所以需要传递一个8字节的缓冲区.
- 从哪里获取缓冲区?您需要首先分配它,这必须发生在inside的WASM内存中,所以您需要调用模块的导出的
malloc
函数.
- 现在,您可以调用返回字符串的函数,同时将缓冲区作为参数传递.
- 然后,您必须从WASM内存中读取
(ptr, len)
元组.
- 最后,从WASM内存中读取
[ptr..ptr+len]
,并将字节转换为Rust字符串.
A simple example:
- 创建一个基本的TinyGo WASM模块,导出一个返回字符串的函数:
package main
//export ReturnString
func ReturnString() string {
return "hello from TinyGo/WASM"
}
func main() {}
使用TinyGo编译成WASM:tinygo build -o return_string.wasm -target wasm ./return_string.go
- Rust Code:
use wasmtime::*;
use wasmtime_wasi::sync::WasiCtxBuilder;
use std::mem;
/// Go's string representation for export.
///
/// According to <https://tinygo.org/docs/concepts/compiler-internals/datatypes/#string> and
/// <https://github.com/tinygo-org/tinygo/blob/731532cd2b6353b60b443343b51296ec0fafae09/src/runtime/string.go#L10-L13>
#[derive(Debug)]
#[repr(C)]
struct GoStringParameters {
ptr: i32,
len: i32,
}
fn main() {
// Create wasmtime runtime with WASI support, according to <https://docs.wasmtime.dev/examples-rust-wasi.html#wasirs>
let engine = Engine::default();
let module = Module::from_file(&engine, "../return_string.wasm").expect("Create module");
let mut linker = Linker::new(&engine);
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.inherit_args().expect("WASI: inherit args")
.build();
let mut store = Store::new(&engine, wasi);
wasmtime_wasi::add_to_linker(&mut linker, |s| s).expect("Add WASI to linker");
let instance = linker.instantiate(&mut store, &module).expect("Create instance");
// malloc a GoStringParameters in WASM memory
let go_str_addr = {
let malloc = instance.get_func(&mut store, "malloc").expect("Couldn't get malloc function");
let mut result = [wasmtime::Val::I32(0)];
malloc.call(&mut store, &[wasmtime::Val::I32(mem::size_of::<GoStringParameters>() as i32)], &mut result).expect("malloc GoStringParameters");
result[0].unwrap_i32()
};
// Call ReturnString() and pass a pointer where it should store the GoStringParameters
let wasm_return_string_function = instance.get_func(&mut store, "ReturnString").expect("Couldn't get function");
wasm_return_string_function.call(&mut store, &[wasmtime::Val::I32(go_str_addr)], &mut []).expect("Call ReturnString");
// Read the GoStringParameters from WASM memory
let mut buf = [0u8; mem::size_of::<GoStringParameters>()];
let mem = instance.get_memory(&mut store, "memory").unwrap();
mem.read(&mut store, go_str_addr as usize, &mut buf).expect("Get WASM memory");
// SAFETY: This hack (mem::transmute) only works on little endian machines, because WASM memory is always in little endian
let go_str_parameters: GoStringParameters = unsafe { mem::transmute(buf) };
dbg!(&go_str_parameters);
// Read the actual bytes of the string from WASM memory
let mut str_bytes = vec![0u8; go_str_parameters.len as usize];
mem.read(&mut store, go_str_parameters.ptr as usize, &mut str_bytes).expect("Read string bytes");
let rust_str = String::from_utf8(str_bytes).unwrap();
dbg!(rust_str);
// TODO: Call exported free() function on the GoStringParameters address
}
输出:
$ cargo run -q --release
[src/main.rs:36] &go_str_parameters = GoStringParameters {
ptr: 65736,
len: 22,
}
[src/main.rs:42] rust_str = "hello from TinyGo/WASM"