Skip to content

Commit d8b5490

Browse files
youknowoneyouknowdot
authored andcommitted
Lend host key/value storage to the non-JS guest
Add env.kv_get and env.kv_put host imports backed by an in-memory map, and expose them to Python as a native kv module (kv.get / kv.put). kv_get returns -1 for a missing key or the full value length, copying the value only when it fits so the guest can retry with a larger buffer. The host allows exactly these three imports. Assisted-by: Grok:grok-4.7 Assisted-by: Claude:claude-opus-5-5
1 parent a9e260c commit d8b5490

5 files changed

Lines changed: 148 additions & 19 deletions

File tree

‎example_projects/wasm32_without_js/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
This example runs Python source on `wasm32-unknown-unknown` in a native Wasmer
44
host. The guest has no JavaScript or WASI dependency and no operating-system
5-
services. It imports only `env.print`. The host rejects any other import.
5+
services. It imports `env.print`, `env.kv_get`, and `env.kv_put`. The host
6+
rejects any other import. The `kv` module (`kv.get` / `kv.put`) reads and
7+
writes byte storage owned by the host; the map is empty for each run.
68

79
## Build and run
810

@@ -37,5 +39,5 @@ python3 test_runtime.py -v \
3739

3840
The existing WASM CI job runs these checks.
3941

40-
This is a no-OS build: `Interpreter::without_stdlib`, only `env.print`, a fixed
41-
hash seed, and unsupported entropy.
42+
This is a no-OS build: `Interpreter::builder` with a native `kv` module, host-lent
43+
storage, a fixed hash seed, and unsupported entropy.

‎example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use rustpython_vm::{Interpreter, Settings};
1+
use rustpython_vm::{Interpreter, Settings, pymodule};
22

33
#[link(wasm_import_module = "env")]
44
unsafe extern "C" {
55
fn print(p: i32, l: i32) -> i32;
6+
fn kv_put(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32;
7+
fn kv_get(key_ptr: i32, key_len: i32, buf_ptr: i32, buf_len: i32) -> i32;
68
}
79

810
// The host writes source into guest-owned memory before calling eval.
@@ -15,6 +17,45 @@ fn report(message: &str) {
1517
unsafe { print(message.as_ptr() as i32, message.len() as i32) };
1618
}
1719

20+
#[pymodule]
21+
mod kv {
22+
use rustpython_vm::builtins::PyBytesRef;
23+
24+
#[pyfunction]
25+
fn put(key: PyBytesRef, value: PyBytesRef) {
26+
let key = key.as_bytes();
27+
let value = value.as_bytes();
28+
unsafe {
29+
super::kv_put(
30+
key.as_ptr() as i32,
31+
key.len() as i32,
32+
value.as_ptr() as i32,
33+
value.len() as i32,
34+
);
35+
}
36+
}
37+
38+
#[pyfunction]
39+
fn get(key: PyBytesRef) -> Option<Vec<u8>> {
40+
let key = key.as_bytes();
41+
let key_ptr = key.as_ptr() as i32;
42+
let key_len = key.len() as i32;
43+
let mut buf = vec![0u8; 64];
44+
loop {
45+
let len = unsafe {
46+
super::kv_get(key_ptr, key_len, buf.as_mut_ptr() as i32, buf.len() as i32)
47+
};
48+
// A negative length means the key is missing.
49+
let len = usize::try_from(len).ok()?;
50+
if len <= buf.len() {
51+
buf.truncate(len);
52+
return Some(buf);
53+
}
54+
buf.resize(len, 0);
55+
}
56+
}
57+
}
58+
1859
/// Execute UTF-8 Python source. Return 0 on success, 1 for a Python exception,
1960
/// or 2 for invalid UTF-8. The result or exception is sent to env.print.
2061
///
@@ -33,7 +74,9 @@ pub unsafe extern "C" fn eval(source: *mut u8, len: usize) -> i32 {
3374
// rather than pretending that the custom getrandom backend produced bytes.
3475
let mut settings = Settings::default();
3576
settings.hash_seed = Some(0);
36-
let interpreter = Interpreter::without_stdlib(settings);
77+
let builder = Interpreter::builder(settings);
78+
let def = kv::module_def(&builder.ctx);
79+
let interpreter = builder.add_native_module(def).build();
3780
interpreter.enter(|vm| {
3881
let scope = vm.new_scope_with_builtins();
3982
match vm

‎example_projects/wasm32_without_js/test_runtime.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ def __init__(self, value):
4646
"""
4747
self.assertEqual(self.run_guest("-c", source), "'guest:55:한글'\n")
4848

49+
def test_host_storage(self):
50+
source = """
51+
import kv
52+
assert kv.get(b'missing') is None
53+
kv.put(b'k', b'v')
54+
assert kv.get(b'k') == b'v'
55+
kv.put(b'k', b'replaced')
56+
big = bytes(range(256)) * 64
57+
kv.put(b'big', big)
58+
assert kv.get(b'big') == big
59+
(kv.get(b'k'), len(kv.get(b'big')))
60+
"""
61+
self.assertEqual(self.run_guest("-c", source), "(b'replaced', 16384)\n")
62+
4963
def test_file_source(self):
5064
with tempfile.TemporaryDirectory() as directory:
5165
source = Path(directory) / "source with spaces.py"

‎example_projects/wasm32_without_js/wasm-runtime/README.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22

33
See the [example README](../README.md) for build, CLI, and test commands.
44

5-
The guest imports `env.print(ptr: i32, len: i32) -> i32`. The host writes that
6-
UTF-8 message to stdout followed by a newline.
5+
The guest imports:
6+
7+
- `env.print(ptr: i32, len: i32) -> i32`: write a UTF-8 message to stdout
8+
followed by a newline
9+
- `env.kv_put(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32`:
10+
copy the value under the key and return 0
11+
- `env.kv_get(key_ptr: i32, key_len: i32, buf_ptr: i32, buf_len: i32) -> i32`:
12+
return -1 if the key is missing, otherwise the full value length. The value
13+
is copied into the buffer only when it fits; otherwise the caller retries
14+
with a buffer of that length.
715

816
The guest exports:
917

‎example_projects/wasm32_without_js/wasm-runtime/src/main.rs‎

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,85 @@
1-
use std::{path::PathBuf, process::ExitCode};
1+
use std::{collections::HashMap, path::PathBuf, process::ExitCode};
22

33
use wasmer::{
4-
Function, FunctionEnv, FunctionEnvMut, Instance, Memory, Module, RuntimeError, Store, imports,
4+
AsStoreRef, Function, FunctionEnv, FunctionEnvMut, Instance, Memory, Module, RuntimeError,
5+
Store, imports,
56
};
67

78
const USAGE: &str = "usage: wasm-runtime MODULE.wasm (-c CODE | FILE.py)";
89

910
struct Context {
1011
memory: Option<Memory>,
12+
kv: HashMap<Vec<u8>, Vec<u8>>,
1113
}
1214

13-
fn print(mut ctx: FunctionEnvMut<Context>, ptr: u32, len: u32) -> Result<i32, RuntimeError> {
14-
let (context, store) = ctx.data_and_store_mut();
15-
let memory = context
16-
.memory
17-
.as_ref()
18-
.ok_or_else(|| RuntimeError::new("guest memory is not initialized"))?
19-
.view(&store);
15+
impl Context {
16+
fn memory(&self) -> Result<&Memory, RuntimeError> {
17+
self.memory
18+
.as_ref()
19+
.ok_or_else(|| RuntimeError::new("guest memory is not initialized"))
20+
}
21+
}
22+
23+
fn read_memory(
24+
context: &Context,
25+
store: &impl AsStoreRef,
26+
ptr: u32,
27+
len: u32,
28+
) -> Result<Vec<u8>, RuntimeError> {
2029
let mut bytes = vec![0; len as usize];
21-
memory
30+
context
31+
.memory()?
32+
.view(store)
2233
.read(u64::from(ptr), &mut bytes)
2334
.map_err(|err| RuntimeError::new(err.to_string()))?;
35+
Ok(bytes)
36+
}
37+
38+
fn print(mut ctx: FunctionEnvMut<Context>, ptr: u32, len: u32) -> Result<i32, RuntimeError> {
39+
let (context, store) = ctx.data_and_store_mut();
40+
let bytes = read_memory(context, &store, ptr, len)?;
2441
let message = std::str::from_utf8(&bytes).map_err(|err| RuntimeError::new(err.to_string()))?;
2542
println!("{message}");
2643
Ok(0)
2744
}
2845

46+
fn kv_put(
47+
mut ctx: FunctionEnvMut<Context>,
48+
key_ptr: u32,
49+
key_len: u32,
50+
value_ptr: u32,
51+
value_len: u32,
52+
) -> Result<i32, RuntimeError> {
53+
let (context, store) = ctx.data_and_store_mut();
54+
let key = read_memory(context, &store, key_ptr, key_len)?;
55+
let value = read_memory(context, &store, value_ptr, value_len)?;
56+
context.kv.insert(key, value);
57+
Ok(0)
58+
}
59+
60+
fn kv_get(
61+
mut ctx: FunctionEnvMut<Context>,
62+
key_ptr: u32,
63+
key_len: u32,
64+
buf_ptr: u32,
65+
buf_len: u32,
66+
) -> Result<i32, RuntimeError> {
67+
let (context, store) = ctx.data_and_store_mut();
68+
let key = read_memory(context, &store, key_ptr, key_len)?;
69+
let Some(value) = context.kv.get(&key) else {
70+
return Ok(-1);
71+
};
72+
let len = i32::try_from(value.len()).map_err(|err| RuntimeError::new(err.to_string()))?;
73+
if value.len() <= buf_len as usize {
74+
context
75+
.memory()?
76+
.view(&store)
77+
.write(u64::from(buf_ptr), value)
78+
.map_err(|err| RuntimeError::new(err.to_string()))?;
79+
}
80+
Ok(len)
81+
}
82+
2983
fn run() -> Result<i32, Box<dyn std::error::Error>> {
3084
let mut args = std::env::args_os().skip(1);
3185
let module_path = PathBuf::from(args.next().ok_or(USAGE)?);
@@ -48,14 +102,22 @@ fn run() -> Result<i32, Box<dyn std::error::Error>> {
48102
let module = Module::new(&store, std::fs::read(&module_path)?)?;
49103
// Fail closed if a future build accidentally acquires OS or JS imports.
50104
for import in module.imports() {
51-
if import.module() != "env" || import.name() != "print" {
105+
if import.module() != "env" || !matches!(import.name(), "print" | "kv_get" | "kv_put") {
52106
return Err(format!("unexpected import: {}.{}", import.module(), import.name()).into());
53107
}
54108
}
55-
let env = FunctionEnv::new(&mut store, Context { memory: None });
109+
let env = FunctionEnv::new(
110+
&mut store,
111+
Context {
112+
memory: None,
113+
kv: HashMap::new(),
114+
},
115+
);
56116
let imports = imports! {
57117
"env" => {
58118
"print" => Function::new_typed_with_env(&mut store, &env, print),
119+
"kv_get" => Function::new_typed_with_env(&mut store, &env, kv_get),
120+
"kv_put" => Function::new_typed_with_env(&mut store, &env, kv_put),
59121
}
60122
};
61123
let instance = Instance::new(&mut store, &module, &imports)?;

0 commit comments

Comments
 (0)