diff --git a/.cspell.dict/rust-more.txt b/.cspell.dict/rust-more.txt index f27e53bd6ed..ff2013e81a7 100644 --- a/.cspell.dict/rust-more.txt +++ b/.cspell.dict/rust-more.txt @@ -82,6 +82,7 @@ unsync wasip1 wasip2 wasmbind +wasmer wasmtime widestring winapi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe45a1d71bb..95a1ac98d62 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -404,11 +404,13 @@ jobs: with: { wabt-version: "1.0.36" } - name: check wasm32-unknown without js run: | - cd wasm/wasm-unknown-test - cargo build --release --verbose - if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then - echo "ERROR: wasm32-unknown module expects imports from the host environment" >2 + cd example_projects/wasm32_without_js/rustpython-without-js + cargo build + cd .. + if wasm-objdump -xj Import rustpython-without-js/target/wasm32-unknown-unknown/debug/rustpython_without_js.wasm; then + echo "ERROR: wasm32-unknown module expects imports from the host environment" >&2 fi + cargo run --release --manifest-path wasm-runtime/Cargo.toml rustpython-without-js/target/wasm32-unknown-unknown/debug/rustpython_without_js.wasm - name: build notebook demo if: github.ref == 'refs/heads/release' run: | diff --git a/example_projects/wasm32_without_js/.gitignore b/example_projects/wasm32_without_js/.gitignore new file mode 100644 index 00000000000..50a623b5daa --- /dev/null +++ b/example_projects/wasm32_without_js/.gitignore @@ -0,0 +1,2 @@ +*/target/ +*/Cargo.lock diff --git a/example_projects/wasm32_without_js/README.md b/example_projects/wasm32_without_js/README.md new file mode 100644 index 00000000000..67fef3fba47 --- /dev/null +++ b/example_projects/wasm32_without_js/README.md @@ -0,0 +1,18 @@ +# RustPython wasm32 build without JS + +To test, build rustpython to wasm32-unknown-unknown target first. + +```shell +cd rustpython-without-js # due to `.cargo/config.toml` +cargo build +cd .. +``` + +Then there will be `rustpython-without-js/target/wasm32-unknown-unknown/debug/rustpython_without_js.wasm` file. + +Now we can run the wasm file with wasm runtime: + +```shell +cargo run --release --manifest-path wasm-runtime/Cargo.toml rustpython-without-js/target/wasm32-unknown-unknown/debug/rustpython_without_js.wasm +``` + diff --git a/wasm/wasm-unknown-test/.cargo/config.toml b/example_projects/wasm32_without_js/rustpython-without-js/.cargo/config.toml similarity index 100% rename from wasm/wasm-unknown-test/.cargo/config.toml rename to example_projects/wasm32_without_js/rustpython-without-js/.cargo/config.toml diff --git a/example_projects/wasm32_without_js/rustpython-without-js/Cargo.toml b/example_projects/wasm32_without_js/rustpython-without-js/Cargo.toml new file mode 100644 index 00000000000..f987fe94a24 --- /dev/null +++ b/example_projects/wasm32_without_js/rustpython-without-js/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rustpython-without-js" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +getrandom = "0.3" +rustpython-vm = { path = "../../../crates/vm", default-features = false, features = ["compiler"] } + +[workspace] + +[patch.crates-io] diff --git a/wasm/wasm-unknown-test/README.md b/example_projects/wasm32_without_js/rustpython-without-js/README.md similarity index 100% rename from wasm/wasm-unknown-test/README.md rename to example_projects/wasm32_without_js/rustpython-without-js/README.md diff --git a/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs new file mode 100644 index 00000000000..0a8695fd7fb --- /dev/null +++ b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs @@ -0,0 +1,59 @@ +use rustpython_vm::{Interpreter}; + +unsafe extern "C" { + fn kv_get(kp: i32, kl: i32, vp: i32, vl: i32) -> i32; + + /// kp and kl are the key pointer and length in wasm memory, vp and vl are for the value + fn kv_put(kp: i32, kl: i32, vp: i32, vl: i32) -> i32; + + fn print(p: i32, l: i32) -> i32; +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn eval(s: *const u8, l: usize) -> i32 { + // let src = unsafe { std::slice::from_raw_parts(s, l) }; + // let src = std::str::from_utf8(src).unwrap(); + // TODO: use src + let src = "1 + 3"; + + // 2. Execute Python code + let interpreter = Interpreter::without_stdlib(Default::default()); + let result = interpreter.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + let res = match vm.run_block_expr(scope, src) { + Ok(val) => val, + Err(_) => return Err(-1), // Python execution error + }; + let repr_str = match res.repr(vm) { + Ok(repr) => repr.as_str().to_string(), + Err(_) => return Err(-1), // Failed to get string representation + }; + Ok(repr_str) + }); + let result = match result { + Ok(r) => r, + Err(code) => return code, + }; + + let msg = format!("eval result: {result}"); + + unsafe { + print( + msg.as_str().as_ptr() as usize as i32, + msg.len() as i32, + ) + }; + + 0 +} + +#[unsafe(no_mangle)] +unsafe extern "Rust" fn __getrandom_v03_custom( + _dest: *mut u8, + _len: usize, +) -> Result<(), getrandom::Error> { + // Err(getrandom::Error::UNSUPPORTED) + + // WARNING: This function **MUST** perform proper getrandom + Ok(()) +} diff --git a/example_projects/wasm32_without_js/wasm-runtime/.gitignore b/example_projects/wasm32_without_js/wasm-runtime/.gitignore new file mode 100644 index 00000000000..2e2101b5066 --- /dev/null +++ b/example_projects/wasm32_without_js/wasm-runtime/.gitignore @@ -0,0 +1,4 @@ +*.wasm +target +Cargo.lock +!wasm/rustpython.wasm diff --git a/example_projects/wasm32_without_js/wasm-runtime/Cargo.toml b/example_projects/wasm32_without_js/wasm-runtime/Cargo.toml new file mode 100644 index 00000000000..a1d0de51719 --- /dev/null +++ b/example_projects/wasm32_without_js/wasm-runtime/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "wasm-runtime" +version = "0.1.0" +edition = "2024" + +[dependencies] +wasmer = "6.1.0" + +[workspace] \ No newline at end of file diff --git a/example_projects/wasm32_without_js/wasm-runtime/README.md b/example_projects/wasm32_without_js/wasm-runtime/README.md new file mode 100644 index 00000000000..2fa2f9e119a --- /dev/null +++ b/example_projects/wasm32_without_js/wasm-runtime/README.md @@ -0,0 +1,19 @@ +# Simple WASM Runtime + +WebAssembly runtime POC with wasmer with HashMap-based KV store. +First make sure to install wat2wasm and rust. + +```bash +# following command installs rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +cargo run --release +``` + +## WASM binary requirements + +Entry point is `eval(code_ptr: i32, code_len: i32) -> i32`, following are exported functions, on error return -1: + +- `kv_put(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32) -> i32` +- `kv_get(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32) -> i32` +- `print(msg_ptr: i32, msg_len: i32) -> i32` diff --git a/example_projects/wasm32_without_js/wasm-runtime/src/main.rs b/example_projects/wasm32_without_js/wasm-runtime/src/main.rs new file mode 100644 index 00000000000..8ca7d581ce4 --- /dev/null +++ b/example_projects/wasm32_without_js/wasm-runtime/src/main.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; +use wasmer::{ + Function, FunctionEnv, FunctionEnvMut, Instance, Memory, Module, Store, Value, imports, +}; + +struct Ctx { + kv: HashMap, Vec>, + mem: Option, +} + +/// kp and kl are the key pointer and length in wasm memory, vp and vl are for the return value +/// if read value is bigger than vl then it will be truncated to vl, returns read bytes +fn kv_get(mut ctx: FunctionEnvMut, kp: i32, kl: i32, vp: i32, vl: i32) -> i32 { + let (c, s) = ctx.data_and_store_mut(); + let mut key = vec![0u8; kl as usize]; + if c.mem + .as_ref() + .unwrap() + .view(&s) + .read(kp as u64, &mut key) + .is_err() + { + return -1; + } + match c.kv.get(&key) { + Some(val) => { + let len = val.len().min(vl as usize); + if c.mem + .as_ref() + .unwrap() + .view(&s) + .write(vp as u64, &val[..len]) + .is_err() + { + return -1; + } + len as i32 + } + None => 0, + } +} + +/// kp and kl are the key pointer and length in wasm memory, vp and vl are for the value +fn kv_put(mut ctx: FunctionEnvMut, kp: i32, kl: i32, vp: i32, vl: i32) -> i32 { + let (c, s) = ctx.data_and_store_mut(); + let mut key = vec![0u8; kl as usize]; + let mut val = vec![0u8; vl as usize]; + let m = c.mem.as_ref().unwrap().view(&s); + if m.read(kp as u64, &mut key).is_err() || m.read(vp as u64, &mut val).is_err() { + return -1; + } + c.kv.insert(key, val); + 0 +} + +// // p and l are the buffer pointer and length in wasm memory. +// fn get_code(mut ctx:FunctionEnvMut, p: i32, l: i32) -> i32 { +// let file_name = std::env::args().nth(2).expect("file_name is not given"); +// let code : String = std::fs::read_to_string(file_name).expect("file read failed"); +// if code.len() > l as usize { +// eprintln!("code is too long"); +// return -1; +// } + +// let (c, s) = ctx.data_and_store_mut(); +// let m = c.mem.as_ref().unwrap().view(&s); +// if m.write(p as u64, code.as_bytes()).is_err() { +// return -2; +// } + +// 0 +// } + +// p and l are the message pointer and length in wasm memory. +fn print(mut ctx: FunctionEnvMut, p: i32, l: i32) -> i32 { + let (c, s) = ctx.data_and_store_mut(); + let mut msg = vec![0u8; l as usize]; + let m = c.mem.as_ref().unwrap().view(&s); + if m.read(p as u64, &mut msg).is_err() { + return -1; + } + let s = std::str::from_utf8(&msg).expect("print got non-utf8 str"); + println!("{s}"); + 0 +} + +fn main() { + let mut store = Store::default(); + let module = Module::new( + &store, + &std::fs::read(&std::env::args().nth(1).unwrap()).unwrap(), + ) + .unwrap(); + + // Prepare initial KV store with Python code + let mut initial_kv = HashMap::new(); + initial_kv.insert( + b"code".to_vec(), + b"a=10;b='str';f'{a}{b}'".to_vec(), // Python code to execute + ); + + let env = FunctionEnv::new( + &mut store, + Ctx { + kv: initial_kv, + mem: None, + }, + ); + let imports = imports! { + "env" => { + "kv_get" => Function::new_typed_with_env(&mut store, &env, kv_get), + "kv_put" => Function::new_typed_with_env(&mut store, &env, kv_put), + // "get_code" => Function::new_typed_with_env(&mut store, &env, get_code), + "print" => Function::new_typed_with_env(&mut store, &env, print), + } + }; + let inst = Instance::new(&mut store, &module, &imports).unwrap(); + env.as_mut(&mut store).mem = inst.exports.get_memory("memory").ok().cloned(); + let res = inst + .exports + .get_function("eval") + .unwrap() + // TODO: actually pass source code + .call(&mut store, &[wasmer::Value::I32(0), wasmer::Value::I32(0)]) + .unwrap(); + println!( + "Result: {}", + match res[0] { + Value::I32(v) => v, + _ => -1, + } + ); +} diff --git a/wasm/wasm-unknown-test/Cargo.toml b/wasm/wasm-unknown-test/Cargo.toml deleted file mode 100644 index 277a9810b99..00000000000 --- a/wasm/wasm-unknown-test/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "wasm-unknown-test" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -getrandom = "0.3" -rustpython-vm = { path = "../../crates/vm", default-features = false, features = ["compiler"] } - -[workspace] - -[patch.crates-io] diff --git a/wasm/wasm-unknown-test/src/lib.rs b/wasm/wasm-unknown-test/src/lib.rs deleted file mode 100644 index aae922864dd..00000000000 --- a/wasm/wasm-unknown-test/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -use rustpython_vm::{Interpreter, eval}; - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn eval(s: *const u8, l: usize) -> u32 { - let src = std::slice::from_raw_parts(s, l); - let src = std::str::from_utf8(src).unwrap(); - Interpreter::without_stdlib(Default::default()).enter(|vm| { - let res = eval::eval(vm, src, vm.new_scope_with_builtins(), "").unwrap(); - res.try_into_value(vm).unwrap() - }) -} - -#[unsafe(no_mangle)] -unsafe extern "Rust" fn __getrandom_v03_custom( - _dest: *mut u8, - _len: usize, -) -> Result<(), getrandom::Error> { - Err(getrandom::Error::UNSUPPORTED) -}