Implement RFC 11: Redesigning Wasmtime's APIs (#2897)
Implement Wasmtime's new API as designed by RFC 11. This is quite a large commit which has had lots of discussion externally, so for more information it's best to read the RFC thread and the PR thread.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst};
|
||||
use std::sync::Arc;
|
||||
use wasmtime::*;
|
||||
|
||||
#[test]
|
||||
@@ -11,47 +11,50 @@ fn test_limits() -> Result<()> {
|
||||
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
|
||||
)?;
|
||||
|
||||
let store = Store::new_with_limits(
|
||||
&engine,
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(
|
||||
StoreLimitsBuilder::new()
|
||||
.memory_pages(10)
|
||||
.table_elements(5)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let instance = Instance::new(&store, &module, &[])?;
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
// Test instance exports and host objects hitting the limit
|
||||
for memory in std::array::IntoIter::new([
|
||||
instance.get_memory("m").unwrap(),
|
||||
Memory::new(&store, MemoryType::new(Limits::new(0, None)))?,
|
||||
instance.get_memory(&mut store, "m").unwrap(),
|
||||
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
|
||||
]) {
|
||||
memory.grow(3)?;
|
||||
memory.grow(5)?;
|
||||
memory.grow(2)?;
|
||||
memory.grow(&mut store, 3)?;
|
||||
memory.grow(&mut store, 5)?;
|
||||
memory.grow(&mut store, 2)?;
|
||||
|
||||
assert_eq!(
|
||||
memory.grow(1).map_err(|e| e.to_string()).unwrap_err(),
|
||||
memory
|
||||
.grow(&mut store, 1)
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_err(),
|
||||
"failed to grow memory by `1`"
|
||||
);
|
||||
}
|
||||
|
||||
// Test instance exports and host objects hitting the limit
|
||||
for table in std::array::IntoIter::new([
|
||||
instance.get_table("t").unwrap(),
|
||||
instance.get_table(&mut store, "t").unwrap(),
|
||||
Table::new(
|
||||
&store,
|
||||
&mut store,
|
||||
TableType::new(ValType::FuncRef, Limits::new(0, None)),
|
||||
Val::FuncRef(None),
|
||||
)?,
|
||||
]) {
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(1, Val::FuncRef(None))?;
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 1, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
|
||||
assert_eq!(
|
||||
table
|
||||
.grow(1, Val::FuncRef(None))
|
||||
.grow(&mut store, 1, Val::FuncRef(None))
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_err(),
|
||||
"failed to grow table by `1`"
|
||||
@@ -69,38 +72,42 @@ fn test_limits_memory_only() -> Result<()> {
|
||||
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
|
||||
)?;
|
||||
|
||||
let store = Store::new_with_limits(&engine, StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
|
||||
let instance = Instance::new(&store, &module, &[])?;
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
// Test instance exports and host objects hitting the limit
|
||||
for memory in std::array::IntoIter::new([
|
||||
instance.get_memory("m").unwrap(),
|
||||
Memory::new(&store, MemoryType::new(Limits::new(0, None)))?,
|
||||
instance.get_memory(&mut store, "m").unwrap(),
|
||||
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
|
||||
]) {
|
||||
memory.grow(3)?;
|
||||
memory.grow(5)?;
|
||||
memory.grow(2)?;
|
||||
memory.grow(&mut store, 3)?;
|
||||
memory.grow(&mut store, 5)?;
|
||||
memory.grow(&mut store, 2)?;
|
||||
|
||||
assert_eq!(
|
||||
memory.grow(1).map_err(|e| e.to_string()).unwrap_err(),
|
||||
memory
|
||||
.grow(&mut store, 1)
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_err(),
|
||||
"failed to grow memory by `1`"
|
||||
);
|
||||
}
|
||||
|
||||
// Test instance exports and host objects *not* hitting the limit
|
||||
for table in std::array::IntoIter::new([
|
||||
instance.get_table("t").unwrap(),
|
||||
instance.get_table(&mut store, "t").unwrap(),
|
||||
Table::new(
|
||||
&store,
|
||||
&mut store,
|
||||
TableType::new(ValType::FuncRef, Limits::new(0, None)),
|
||||
Val::FuncRef(None),
|
||||
)?,
|
||||
]) {
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(1, Val::FuncRef(None))?;
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(1, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 1, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 1, Val::FuncRef(None))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -111,9 +118,10 @@ fn test_initial_memory_limits_exceeded() -> Result<()> {
|
||||
let engine = Engine::default();
|
||||
let module = Module::new(&engine, r#"(module (memory (export "m") 11))"#)?;
|
||||
|
||||
let store = Store::new_with_limits(&engine, StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
|
||||
match Instance::new(&store, &module, &[]) {
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
@@ -121,7 +129,7 @@ fn test_initial_memory_limits_exceeded() -> Result<()> {
|
||||
),
|
||||
}
|
||||
|
||||
match Memory::new(&store, MemoryType::new(Limits::new(25, None))) {
|
||||
match Memory::new(&mut store, MemoryType::new(Limits::new(25, None))) {
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
@@ -140,38 +148,38 @@ fn test_limits_table_only() -> Result<()> {
|
||||
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
|
||||
)?;
|
||||
|
||||
let store =
|
||||
Store::new_with_limits(&engine, StoreLimitsBuilder::new().table_elements(5).build());
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().table_elements(5).build());
|
||||
|
||||
let instance = Instance::new(&store, &module, &[])?;
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
// Test instance exports and host objects *not* hitting the limit
|
||||
for memory in std::array::IntoIter::new([
|
||||
instance.get_memory("m").unwrap(),
|
||||
Memory::new(&store, MemoryType::new(Limits::new(0, None)))?,
|
||||
instance.get_memory(&mut store, "m").unwrap(),
|
||||
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
|
||||
]) {
|
||||
memory.grow(3)?;
|
||||
memory.grow(5)?;
|
||||
memory.grow(2)?;
|
||||
memory.grow(1)?;
|
||||
memory.grow(&mut store, 3)?;
|
||||
memory.grow(&mut store, 5)?;
|
||||
memory.grow(&mut store, 2)?;
|
||||
memory.grow(&mut store, 1)?;
|
||||
}
|
||||
|
||||
// Test instance exports and host objects hitting the limit
|
||||
for table in std::array::IntoIter::new([
|
||||
instance.get_table("t").unwrap(),
|
||||
instance.get_table(&mut store, "t").unwrap(),
|
||||
Table::new(
|
||||
&store,
|
||||
&mut store,
|
||||
TableType::new(ValType::FuncRef, Limits::new(0, None)),
|
||||
Val::FuncRef(None),
|
||||
)?,
|
||||
]) {
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(1, Val::FuncRef(None))?;
|
||||
table.grow(2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 1, Val::FuncRef(None))?;
|
||||
table.grow(&mut store, 2, Val::FuncRef(None))?;
|
||||
|
||||
assert_eq!(
|
||||
table
|
||||
.grow(1, Val::FuncRef(None))
|
||||
.grow(&mut store, 1, Val::FuncRef(None))
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_err(),
|
||||
"failed to grow table by `1`"
|
||||
@@ -186,10 +194,10 @@ fn test_initial_table_limits_exceeded() -> Result<()> {
|
||||
let engine = Engine::default();
|
||||
let module = Module::new(&engine, r#"(module (table (export "t") 23 anyfunc))"#)?;
|
||||
|
||||
let store =
|
||||
Store::new_with_limits(&engine, StoreLimitsBuilder::new().table_elements(4).build());
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().table_elements(4).build());
|
||||
|
||||
match Instance::new(&store, &module, &[]) {
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
@@ -198,7 +206,7 @@ fn test_initial_table_limits_exceeded() -> Result<()> {
|
||||
}
|
||||
|
||||
match Table::new(
|
||||
&store,
|
||||
&mut store,
|
||||
TableType::new(ValType::FuncRef, Limits::new(99, None)),
|
||||
Val::FuncRef(None),
|
||||
) {
|
||||
@@ -234,9 +242,10 @@ fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> {
|
||||
r#"(module (memory (export "m1") 2) (memory (export "m2") 5))"#,
|
||||
)?;
|
||||
|
||||
let store = Store::new_with_limits(&engine, StoreLimitsBuilder::new().memory_pages(3).build());
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().memory_pages(3).build());
|
||||
|
||||
match Instance::new(&store, &module, &[]) {
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
@@ -247,135 +256,136 @@ fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> {
|
||||
// An instance should still be able to be created after the failure above
|
||||
let module = Module::new(&engine, r#"(module (memory (export "m") 2))"#)?;
|
||||
|
||||
Instance::new(&store, &module, &[])?;
|
||||
Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MemoryContext {
|
||||
host_memory_used: usize,
|
||||
wasm_memory_used: usize,
|
||||
host_memory_used: AtomicUsize,
|
||||
wasm_memory_used: AtomicUsize,
|
||||
memory_limit: usize,
|
||||
limit_exceeded: bool,
|
||||
limiter_dropped: bool,
|
||||
limit_exceeded: AtomicBool,
|
||||
limiter_dropped: AtomicBool,
|
||||
}
|
||||
|
||||
struct HostMemoryLimiter(Rc<RefCell<MemoryContext>>);
|
||||
struct HostMemoryLimiter(Arc<MemoryContext>);
|
||||
|
||||
impl ResourceLimiter for HostMemoryLimiter {
|
||||
fn memory_growing(&self, current: u32, desired: u32, maximum: Option<u32>) -> bool {
|
||||
let mut ctx = self.0.borrow_mut();
|
||||
|
||||
fn memory_growing(&mut self, current: u32, desired: u32, maximum: Option<u32>) -> bool {
|
||||
// Check if the desired exceeds a maximum (either from Wasm or from the host)
|
||||
if desired > maximum.unwrap_or(u32::MAX) {
|
||||
ctx.limit_exceeded = true;
|
||||
self.0.limit_exceeded.store(true, SeqCst);
|
||||
return false;
|
||||
}
|
||||
|
||||
assert_eq!(current as usize * 0x10000, ctx.wasm_memory_used);
|
||||
assert_eq!(
|
||||
current as usize * 0x10000,
|
||||
self.0.wasm_memory_used.load(SeqCst)
|
||||
);
|
||||
let desired = desired as usize * 0x10000;
|
||||
|
||||
if desired + ctx.host_memory_used > ctx.memory_limit {
|
||||
ctx.limit_exceeded = true;
|
||||
if desired + self.0.host_memory_used.load(SeqCst) > self.0.memory_limit {
|
||||
self.0.limit_exceeded.store(true, SeqCst);
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.wasm_memory_used = desired;
|
||||
self.0.wasm_memory_used.store(desired, SeqCst);
|
||||
true
|
||||
}
|
||||
|
||||
fn table_growing(&self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
|
||||
fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HostMemoryLimiter {
|
||||
fn drop(&mut self) {
|
||||
self.0.borrow_mut().limiter_dropped = true;
|
||||
self.0.limiter_dropped.store(true, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_limiter() -> Result<()> {
|
||||
let mut config = Config::default();
|
||||
let engine = Engine::default();
|
||||
let mut linker = Linker::new(&engine);
|
||||
|
||||
// This approximates a function that would "allocate" resources that the host tracks.
|
||||
// Here this is a simple function that increments the current host memory "used".
|
||||
config.wrap_host_func("", "alloc", |caller: Caller, size: u32| -> u32 {
|
||||
if let Some(ctx) = caller.store().get::<Rc<RefCell<MemoryContext>>>() {
|
||||
let mut ctx = ctx.borrow_mut();
|
||||
linker.func_wrap(
|
||||
"",
|
||||
"alloc",
|
||||
|caller: Caller<'_, Arc<MemoryContext>>, size: u32| -> u32 {
|
||||
let ctx = caller.data();
|
||||
let size = size as usize;
|
||||
|
||||
if size + ctx.host_memory_used + ctx.wasm_memory_used <= ctx.memory_limit {
|
||||
ctx.host_memory_used += size;
|
||||
if size + ctx.host_memory_used.load(SeqCst) + ctx.wasm_memory_used.load(SeqCst)
|
||||
<= ctx.memory_limit
|
||||
{
|
||||
ctx.host_memory_used.fetch_add(size, SeqCst);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ctx.limit_exceeded = true;
|
||||
}
|
||||
ctx.limit_exceeded.store(true, SeqCst);
|
||||
|
||||
0
|
||||
});
|
||||
0
|
||||
},
|
||||
)?;
|
||||
|
||||
let engine = Engine::new(&config)?;
|
||||
let module = Module::new(
|
||||
&engine,
|
||||
r#"(module (import "" "alloc" (func $alloc (param i32) (result i32))) (memory (export "m") 0) (func (export "f") (param i32) (result i32) local.get 0 call $alloc))"#,
|
||||
)?;
|
||||
|
||||
let context = Rc::new(RefCell::new(MemoryContext {
|
||||
host_memory_used: 0,
|
||||
wasm_memory_used: 0,
|
||||
let context = Arc::new(MemoryContext {
|
||||
host_memory_used: AtomicUsize::new(0),
|
||||
wasm_memory_used: AtomicUsize::new(0),
|
||||
memory_limit: 1 << 20, // 16 wasm pages is the limit for both wasm + host memory
|
||||
limit_exceeded: false,
|
||||
limiter_dropped: false,
|
||||
}));
|
||||
limit_exceeded: AtomicBool::new(false),
|
||||
limiter_dropped: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
let store = Store::new_with_limits(&engine, HostMemoryLimiter(context.clone()));
|
||||
|
||||
assert!(store.set(context.clone()).is_ok());
|
||||
|
||||
let linker = Linker::new(&store);
|
||||
let instance = linker.instantiate(&module)?;
|
||||
let memory = instance.get_memory("m").unwrap();
|
||||
let mut store = Store::new(&engine, context.clone());
|
||||
store.limiter(HostMemoryLimiter(context.clone()));
|
||||
let instance = linker.instantiate(&mut store, &module)?;
|
||||
let memory = instance.get_memory(&mut store, "m").unwrap();
|
||||
|
||||
// Grow the memory by 640 KiB
|
||||
memory.grow(3)?;
|
||||
memory.grow(5)?;
|
||||
memory.grow(2)?;
|
||||
memory.grow(&mut store, 3)?;
|
||||
memory.grow(&mut store, 5)?;
|
||||
memory.grow(&mut store, 2)?;
|
||||
|
||||
assert!(!context.borrow().limit_exceeded);
|
||||
assert!(!context.limit_exceeded.load(SeqCst));
|
||||
|
||||
// Grow the host "memory" by 384 KiB
|
||||
let f = instance.get_typed_func::<u32, u32>("f")?;
|
||||
let f = instance.get_typed_func::<u32, u32, _>(&mut store, "f")?;
|
||||
|
||||
assert_eq!(f.call(1 * 0x10000).unwrap(), 1);
|
||||
assert_eq!(f.call(3 * 0x10000).unwrap(), 1);
|
||||
assert_eq!(f.call(2 * 0x10000).unwrap(), 1);
|
||||
assert_eq!(f.call(&mut store, 1 * 0x10000)?, 1);
|
||||
assert_eq!(f.call(&mut store, 3 * 0x10000)?, 1);
|
||||
assert_eq!(f.call(&mut store, 2 * 0x10000)?, 1);
|
||||
|
||||
// Memory is at the maximum, but the limit hasn't been exceeded
|
||||
assert!(!context.borrow().limit_exceeded);
|
||||
assert!(!context.limit_exceeded.load(SeqCst));
|
||||
|
||||
// Try to grow the memory again
|
||||
assert_eq!(
|
||||
memory.grow(1).map_err(|e| e.to_string()).unwrap_err(),
|
||||
memory
|
||||
.grow(&mut store, 1)
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_err(),
|
||||
"failed to grow memory by `1`"
|
||||
);
|
||||
|
||||
assert!(context.borrow().limit_exceeded);
|
||||
assert!(context.limit_exceeded.load(SeqCst));
|
||||
|
||||
// Try to grow the host "memory" again
|
||||
assert_eq!(f.call(1).unwrap(), 0);
|
||||
assert_eq!(f.call(&mut store, 1)?, 0);
|
||||
|
||||
assert!(context.borrow().limit_exceeded);
|
||||
assert!(context.limit_exceeded.load(SeqCst));
|
||||
|
||||
drop(f);
|
||||
drop(memory);
|
||||
drop(instance);
|
||||
drop(linker);
|
||||
drop(store);
|
||||
|
||||
assert!(context.borrow().limiter_dropped);
|
||||
assert!(context.limiter_dropped.load(SeqCst));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user