Add shared memories (#4187)
* Add shared memories This change adds the ability to use shared memories in Wasmtime when the [threads proposal] is enabled. Shared memories are annotated as `shared` in the WebAssembly syntax, e.g., `(memory 1 1 shared)`, and are protected from concurrent access during `memory.size` and `memory.grow`. [threads proposal]: https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md In order to implement this in Wasmtime, there are two main cases to cover: - a program may simply create a shared memory and possibly export it; this means that Wasmtime itself must be able to create shared memories - a user may create a shared memory externally and pass it in as an import during instantiation; this is the case when the program contains code like `(import "env" "memory" (memory 1 1 shared))`--this case is handled by a new Wasmtime API type--`SharedMemory` Because of the first case, this change allows any of the current memory-creation mechanisms to work as-is. Wasmtime can still create either static or dynamic memories in either on-demand or pooling modes, and any of these memories can be considered shared. When shared, the `Memory` runtime container will lock appropriately during `memory.size` and `memory.grow` operations; since all memories use this container, it is an ideal place for implementing the locking once and once only. The second case is covered by the new `SharedMemory` structure. It uses the same `Mmap` allocation under the hood as non-shared memories, but allows the user to perform the allocation externally to Wasmtime and share the memory across threads (via an `Arc`). The pointer address to the actual memory is carefully wired through and owned by the `SharedMemory` structure itself. This means that there are differing views of where to access the pointer (i.e., `VMMemoryDefinition`): for owned memories (the default), the `VMMemoryDefinition` is stored directly by the `VMContext`; in the `SharedMemory` case, however, this `VMContext` must point to this separate structure. To ensure that the `VMContext` can always point to the correct `VMMemoryDefinition`, this change alters the `VMContext` structure. Since a `SharedMemory` owns its own `VMMemoryDefinition`, the `defined_memories` table in the `VMContext` becomes a sequence of pointers--in the shared memory case, they point to the `VMMemoryDefinition` owned by the `SharedMemory` and in the owned memory case (i.e., not shared) they point to `VMMemoryDefinition`s stored in a new table, `owned_memories`. This change adds an additional indirection (through the `*mut VMMemoryDefinition` pointer) that could add overhead. Using an imported memory as a proxy, we measured a 1-3% overhead of this approach on the `pulldown-cmark` benchmark. To avoid this, Cranelift-generated code will special-case the owned memory access (i.e., load a pointer directly to the `owned_memories` entry) for `memory.size` so that only shared memories (and imported memories, as before) incur the indirection cost. * review: remove thread feature check * review: swap wasmtime-types dependency for existing wasmtime-environ use * review: remove unused VMMemoryUnion * review: reword cross-engine error message * review: improve tests * review: refactor to separate prevent Memory <-> SharedMemory conversion * review: into_shared_memory -> as_shared_memory * review: remove commented out code * review: limit shared min/max to 32 bits * review: skip imported memories * review: imported memories are not owned * review: remove TODO * review: document unsafe send + sync * review: add limiter assertion * review: remove TODO * review: improve tests * review: fix doc test * fix: fixes based on discussion with Alex This changes several key parts: - adds memory indexes to imports and exports - makes `VMMemoryDefinition::current_length` an atomic usize * review: add `Extern::SharedMemory` * review: remove TODO * review: atomically load from VMMemoryDescription in JIT-generated code * review: add test probing the last available memory slot across threads * fix: move assertion to new location due to rebase * fix: doc link * fix: add TODOs to c-api * fix: broken doc link * fix: modify pooling allocator messages in tests * review: make owned_memory_index panic instead of returning an option * review: clarify calculation of num_owned_memories * review: move 'use' to top of file * review: change '*const [u8]' to '*mut [u8]' * review: remove TODO * review: avoid hard-coding memory index * review: remove 'preallocation' parameter from 'Memory::_new' * fix: component model memory length * review: check that shared memory plans are static * review: ignore growth limits for shared memory * review: improve atomic store comment * review: add FIXME for memory growth failure * review: add comment about absence of bounds-checked 'memory.size' * review: make 'current_length()' doc comment more precise * review: more comments related to memory.size non-determinism * review: make 'vmmemory' unreachable for shared memory * review: move code around * review: thread plan through to 'wrap()' * review: disallow shared memory allocation with the pooling allocator
This commit is contained in:
@@ -128,7 +128,7 @@ impl Options {
|
||||
// is an optional configuration in canonical ABI options.
|
||||
unsafe {
|
||||
let memory = self.memory.unwrap().as_ref();
|
||||
std::slice::from_raw_parts(memory.base, memory.current_length)
|
||||
std::slice::from_raw_parts(memory.base, memory.current_length())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ impl Options {
|
||||
// See comments in `memory` about the unsafety
|
||||
unsafe {
|
||||
let memory = self.memory.unwrap().as_ref();
|
||||
std::slice::from_raw_parts_mut(memory.base, memory.current_length)
|
||||
std::slice::from_raw_parts_mut(memory.base, memory.current_length())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::store::{StoreData, StoreOpaque, Stored};
|
||||
use crate::trampoline::{generate_global_export, generate_table_export};
|
||||
use crate::{
|
||||
AsContext, AsContextMut, ExternRef, ExternType, Func, GlobalType, Memory, Mutability,
|
||||
TableType, Trap, Val, ValType,
|
||||
AsContext, AsContextMut, Engine, ExternRef, ExternType, Func, GlobalType, Memory, Mutability,
|
||||
SharedMemory, TableType, Trap, Val, ValType,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::mem;
|
||||
@@ -29,6 +29,9 @@ pub enum Extern {
|
||||
Table(Table),
|
||||
/// A WebAssembly linear memory.
|
||||
Memory(Memory),
|
||||
/// A WebAssembly shared memory; these are handled separately from
|
||||
/// [`Memory`].
|
||||
SharedMemory(SharedMemory),
|
||||
}
|
||||
|
||||
impl Extern {
|
||||
@@ -72,6 +75,17 @@ impl Extern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying `SharedMemory`, if this external is a shared
|
||||
/// memory.
|
||||
///
|
||||
/// Returns `None` if this is not a shared memory.
|
||||
pub fn into_shared_memory(self) -> Option<SharedMemory> {
|
||||
match self {
|
||||
Extern::SharedMemory(memory) => Some(memory),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type associated with this `Extern`.
|
||||
///
|
||||
/// The `store` argument provided must own this `Extern` and is used to look
|
||||
@@ -85,6 +99,7 @@ impl Extern {
|
||||
match self {
|
||||
Extern::Func(ft) => ExternType::Func(ft.ty(store)),
|
||||
Extern::Memory(ft) => ExternType::Memory(ft.ty(store)),
|
||||
Extern::SharedMemory(ft) => ExternType::Memory(ft.ty()),
|
||||
Extern::Table(tt) => ExternType::Table(tt.ty(store)),
|
||||
Extern::Global(gt) => ExternType::Global(gt.ty(store)),
|
||||
}
|
||||
@@ -99,7 +114,11 @@ impl Extern {
|
||||
Extern::Func(Func::from_wasmtime_function(f, store))
|
||||
}
|
||||
wasmtime_runtime::Export::Memory(m) => {
|
||||
Extern::Memory(Memory::from_wasmtime_memory(m, store))
|
||||
if m.memory.memory.shared {
|
||||
Extern::SharedMemory(SharedMemory::from_wasmtime_memory(m, store))
|
||||
} else {
|
||||
Extern::Memory(Memory::from_wasmtime_memory(m, store))
|
||||
}
|
||||
}
|
||||
wasmtime_runtime::Export::Global(g) => {
|
||||
Extern::Global(Global::from_wasmtime_global(g, store))
|
||||
@@ -115,6 +134,7 @@ impl Extern {
|
||||
Extern::Func(f) => f.comes_from_same_store(store),
|
||||
Extern::Global(g) => store.store_data().contains(g.0),
|
||||
Extern::Memory(m) => m.comes_from_same_store(store),
|
||||
Extern::SharedMemory(m) => Engine::same(m.engine(), store.engine()),
|
||||
Extern::Table(t) => store.store_data().contains(t.0),
|
||||
}
|
||||
}
|
||||
@@ -124,6 +144,7 @@ impl Extern {
|
||||
Extern::Func(_) => "function",
|
||||
Extern::Table(_) => "table",
|
||||
Extern::Memory(_) => "memory",
|
||||
Extern::SharedMemory(_) => "shared memory",
|
||||
Extern::Global(_) => "global",
|
||||
}
|
||||
}
|
||||
@@ -147,6 +168,12 @@ impl From<Memory> for Extern {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedMemory> for Extern {
|
||||
fn from(r: SharedMemory) -> Self {
|
||||
Extern::SharedMemory(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Table> for Extern {
|
||||
fn from(r: Table) -> Self {
|
||||
Extern::Table(r)
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::linker::Definition;
|
||||
use crate::store::{InstanceId, StoreOpaque, Stored};
|
||||
use crate::types::matching;
|
||||
use crate::{
|
||||
AsContextMut, Engine, Export, Extern, Func, Global, Memory, Module, StoreContextMut, Table,
|
||||
Trap, TypedFunc,
|
||||
AsContextMut, Engine, Export, Extern, Func, Global, Memory, Module, SharedMemory,
|
||||
StoreContextMut, Table, Trap, TypedFunc,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context, Error, Result};
|
||||
use std::mem;
|
||||
@@ -495,6 +495,23 @@ impl Instance {
|
||||
self.get_export(store, name)?.into_memory()
|
||||
}
|
||||
|
||||
/// Looks up an exported [`SharedMemory`] value by name.
|
||||
///
|
||||
/// Returns `None` if there was no export named `name`, or if there was but
|
||||
/// it wasn't a shared memory.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `store` does not own this instance.
|
||||
pub fn get_shared_memory(
|
||||
&self,
|
||||
mut store: impl AsContextMut,
|
||||
name: &str,
|
||||
) -> Option<SharedMemory> {
|
||||
let mut store = store.as_context_mut();
|
||||
self.get_export(&mut store, name)?.into_shared_memory()
|
||||
}
|
||||
|
||||
/// Looks up an exported [`Global`] value by name.
|
||||
///
|
||||
/// Returns `None` if there was no export named `name`, or if there was but
|
||||
@@ -566,6 +583,9 @@ impl OwnedImports {
|
||||
Extern::Memory(i) => {
|
||||
self.memories.push(i.vmimport(store));
|
||||
}
|
||||
Extern::SharedMemory(i) => {
|
||||
self.memories.push(i.vmimport(store));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +614,7 @@ impl OwnedImports {
|
||||
self.memories.push(VMMemoryImport {
|
||||
from: m.definition,
|
||||
vmctx: m.vmctx,
|
||||
index: m.index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ impl StoreLimitsBuilder {
|
||||
/// Provides limits for a [`Store`](crate::Store).
|
||||
///
|
||||
/// This type is created with a [`StoreLimitsBuilder`] and is typically used in
|
||||
/// conjuction with [`Store::limiter`](crate::Store::limiter).
|
||||
/// conjunction with [`Store::limiter`](crate::Store::limiter).
|
||||
///
|
||||
/// This is a convenience type included to avoid needing to implement the
|
||||
/// [`ResourceLimiter`] trait if your use case fits in the static configuration
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::store::{StoreData, StoreOpaque, Stored};
|
||||
use crate::trampoline::generate_memory_export;
|
||||
use crate::{AsContext, AsContextMut, MemoryType, StoreContext, StoreContextMut};
|
||||
use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreContextMut};
|
||||
use anyhow::{bail, Result};
|
||||
use std::convert::TryFrom;
|
||||
use std::slice;
|
||||
use wasmtime_environ::MemoryPlan;
|
||||
use wasmtime_runtime::{RuntimeLinearMemory, VMMemoryImport};
|
||||
|
||||
/// Error for out of bounds [`Memory`] access.
|
||||
#[derive(Debug)]
|
||||
@@ -227,7 +229,7 @@ impl Memory {
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(mut store: impl AsContextMut, ty: MemoryType) -> Result<Memory> {
|
||||
Memory::_new(store.as_context_mut().0, ty)
|
||||
Self::_new(store.as_context_mut().0, ty)
|
||||
}
|
||||
|
||||
#[cfg_attr(nightlydoc, doc(cfg(feature = "async")))]
|
||||
@@ -252,12 +254,13 @@ impl Memory {
|
||||
store.0.async_support(),
|
||||
"cannot use `new_async` without enabling async support on the config"
|
||||
);
|
||||
store.on_fiber(|store| Memory::_new(store.0, ty)).await?
|
||||
store.on_fiber(|store| Self::_new(store.0, ty)).await?
|
||||
}
|
||||
|
||||
/// Helper function for attaching the memory to a "frankenstein" instance
|
||||
fn _new(store: &mut StoreOpaque, ty: MemoryType) -> Result<Memory> {
|
||||
unsafe {
|
||||
let export = generate_memory_export(store, &ty)?;
|
||||
let export = generate_memory_export(store, &ty, None)?;
|
||||
Ok(Memory::from_wasmtime_memory(export, store))
|
||||
}
|
||||
}
|
||||
@@ -350,8 +353,9 @@ impl Memory {
|
||||
pub fn data<'a, T: 'a>(&self, store: impl Into<StoreContext<'a, T>>) -> &'a [u8] {
|
||||
unsafe {
|
||||
let store = store.into();
|
||||
let definition = *store[self.0].definition;
|
||||
slice::from_raw_parts(definition.base, definition.current_length)
|
||||
let definition = &*store[self.0].definition;
|
||||
debug_assert!(!self.ty(store).is_shared());
|
||||
slice::from_raw_parts(definition.base, definition.current_length())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,8 +370,9 @@ impl Memory {
|
||||
pub fn data_mut<'a, T: 'a>(&self, store: impl Into<StoreContextMut<'a, T>>) -> &'a mut [u8] {
|
||||
unsafe {
|
||||
let store = store.into();
|
||||
let definition = *store[self.0].definition;
|
||||
slice::from_raw_parts_mut(definition.base, definition.current_length)
|
||||
let definition = &*store[self.0].definition;
|
||||
debug_assert!(!self.ty(store).is_shared());
|
||||
slice::from_raw_parts_mut(definition.base, definition.current_length())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +437,7 @@ impl Memory {
|
||||
}
|
||||
|
||||
pub(crate) fn internal_data_size(&self, store: &StoreOpaque) -> usize {
|
||||
unsafe { (*store[self.0].definition).current_length }
|
||||
unsafe { (*store[self.0].definition).current_length() }
|
||||
}
|
||||
|
||||
/// Returns the size, in WebAssembly pages, of this wasm memory.
|
||||
@@ -453,7 +458,7 @@ impl Memory {
|
||||
/// This will attempt to add `delta` more pages of memory on to the end of
|
||||
/// this `Memory` instance. If successful this may relocate the memory and
|
||||
/// cause [`Memory::data_ptr`] to return a new value. Additionally any
|
||||
/// unsafetly constructed slices into this memory may no longer be valid.
|
||||
/// unsafely constructed slices into this memory may no longer be valid.
|
||||
///
|
||||
/// On success returns the number of pages this memory previously had
|
||||
/// before the growth succeeded.
|
||||
@@ -498,7 +503,7 @@ impl Memory {
|
||||
let store = store.as_context_mut().0;
|
||||
let mem = self.wasmtime_memory(store);
|
||||
unsafe {
|
||||
match (*mem).grow(delta, store)? {
|
||||
match (*mem).grow(delta, Some(store))? {
|
||||
Some(size) => {
|
||||
let vm = (*mem).vmmemory();
|
||||
*store[self.0].definition = vm;
|
||||
@@ -533,12 +538,12 @@ impl Memory {
|
||||
);
|
||||
store.on_fiber(|store| self.grow(store, delta)).await?
|
||||
}
|
||||
|
||||
fn wasmtime_memory(&self, store: &mut StoreOpaque) -> *mut wasmtime_runtime::Memory {
|
||||
unsafe {
|
||||
let export = &store[self.0];
|
||||
let mut handle = wasmtime_runtime::InstanceHandle::from_vmctx(export.vmctx);
|
||||
let idx = handle.memory_index(&*export.definition);
|
||||
handle.get_defined_memory(idx)
|
||||
handle.get_defined_memory(export.index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +563,7 @@ impl Memory {
|
||||
wasmtime_runtime::VMMemoryImport {
|
||||
from: export.definition,
|
||||
vmctx: export.vmctx,
|
||||
index: export.index,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +660,164 @@ pub unsafe trait MemoryCreator: Send + Sync {
|
||||
) -> Result<Box<dyn LinearMemory>, String>;
|
||||
}
|
||||
|
||||
/// A constructor for externally-created shared memory.
|
||||
///
|
||||
/// The [threads proposal] adds the concept of "shared memory" to WebAssembly.
|
||||
/// This is much the same as a Wasm linear memory (i.e., [`Memory`]), but can be
|
||||
/// used concurrently by multiple agents. Because these agents may execute in
|
||||
/// different threads, [`SharedMemory`] must be thread-safe.
|
||||
///
|
||||
/// When the threads proposal is enabled, there are multiple ways to construct
|
||||
/// shared memory:
|
||||
/// 1. for imported shared memory, e.g., `(import "env" "memory" (memory 1 1
|
||||
/// shared))`, the user must supply a [`SharedMemory`] with the
|
||||
/// externally-created memory as an import to the instance--e.g.,
|
||||
/// `shared_memory.into()`.
|
||||
/// 2. for private or exported shared memory, e.g., `(export "env" "memory"
|
||||
/// (memory 1 1 shared))`, Wasmtime will create the memory internally during
|
||||
/// instantiation--access using `Instance::get_shared_memory()`.
|
||||
///
|
||||
/// [threads proposal]:
|
||||
/// https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use wasmtime::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// let mut config = Config::new();
|
||||
/// config.wasm_threads(true);
|
||||
/// let engine = Engine::new(&config)?;
|
||||
/// let mut store = Store::new(&engine, ());
|
||||
///
|
||||
/// let shared_memory = SharedMemory::new(&engine, MemoryType::shared(1, 2))?;
|
||||
/// let module = Module::new(&engine, r#"(module (memory (import "" "") 1 2 shared))"#)?;
|
||||
/// let instance = Instance::new(&mut store, &module, &[shared_memory.into()])?;
|
||||
/// // ...
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct SharedMemory(wasmtime_runtime::SharedMemory, Engine);
|
||||
impl SharedMemory {
|
||||
/// Construct a [`SharedMemory`] by providing both the `minimum` and
|
||||
/// `maximum` number of 64K-sized pages. This call allocates the necessary
|
||||
/// pages on the system.
|
||||
pub fn new(engine: &Engine, ty: MemoryType) -> Result<Self> {
|
||||
if !ty.is_shared() {
|
||||
bail!("shared memory must have the `shared` flag enabled on its memory type")
|
||||
}
|
||||
debug_assert!(ty.maximum().is_some());
|
||||
|
||||
let tunables = &engine.config().tunables;
|
||||
let plan = MemoryPlan::for_memory(ty.wasmtime_memory().clone(), tunables);
|
||||
let memory = wasmtime_runtime::SharedMemory::new(plan)?;
|
||||
Ok(Self(memory, engine.clone()))
|
||||
}
|
||||
|
||||
/// Return the type of the shared memory.
|
||||
pub fn ty(&self) -> MemoryType {
|
||||
MemoryType::from_wasmtime_memory(&self.0.ty())
|
||||
}
|
||||
|
||||
/// Returns the size, in WebAssembly pages, of this wasm memory.
|
||||
pub fn size(&self) -> u64 {
|
||||
(self.data_size() / wasmtime_environ::WASM_PAGE_SIZE as usize) as u64
|
||||
}
|
||||
|
||||
/// Returns the byte length of this memory.
|
||||
///
|
||||
/// The returned value will be a multiple of the wasm page size, 64k.
|
||||
///
|
||||
/// For more information and examples see the documentation on the
|
||||
/// [`Memory`] type.
|
||||
pub fn data_size(&self) -> usize {
|
||||
self.0.byte_size()
|
||||
}
|
||||
|
||||
/// Return access to the available portion of the shared memory.
|
||||
///
|
||||
/// Because the memory is shared, it is possible that this memory is being
|
||||
/// modified in other threads--in other words, the data can change at any
|
||||
/// time. Users of this function must manage synchronization and locking to
|
||||
/// this region of memory themselves.
|
||||
///
|
||||
/// Not only can the data change, but the length of this region can change
|
||||
/// as well. Other threads can call `memory.grow` operations that will
|
||||
/// extend the region length but--importantly--this will not be reflected in
|
||||
/// the size of region returned by this function.
|
||||
pub fn data(&self) -> *mut [u8] {
|
||||
unsafe {
|
||||
let definition = &*self.0.vmmemory_ptr();
|
||||
slice::from_raw_parts_mut(definition.base, definition.current_length())
|
||||
}
|
||||
}
|
||||
|
||||
/// Grows this WebAssembly memory by `delta` pages.
|
||||
///
|
||||
/// This will attempt to add `delta` more pages of memory on to the end of
|
||||
/// this `Memory` instance. If successful this may relocate the memory and
|
||||
/// cause [`Memory::data_ptr`] to return a new value. Additionally any
|
||||
/// unsafely constructed slices into this memory may no longer be valid.
|
||||
///
|
||||
/// On success returns the number of pages this memory previously had
|
||||
/// before the growth succeeded.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if memory could not be grown, for example if it exceeds
|
||||
/// the maximum limits of this memory. A
|
||||
/// [`ResourceLimiter`](crate::ResourceLimiter) is another example of
|
||||
/// preventing a memory to grow.
|
||||
pub fn grow(&mut self, delta: u64) -> Result<u64> {
|
||||
match self.0.grow(delta, None)? {
|
||||
Some((old_size, _new_size)) => {
|
||||
// For shared memory, the `VMMemoryDefinition` is updated inside
|
||||
// the locked region.
|
||||
Ok(u64::try_from(old_size).unwrap() / u64::from(wasmtime_environ::WASM_PAGE_SIZE))
|
||||
}
|
||||
None => bail!("failed to grow memory by `{}`", delta),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the [`Engine`] used to configure the shared
|
||||
/// memory.
|
||||
pub(crate) fn engine(&self) -> &Engine {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Construct a single-memory instance to provide a way to import
|
||||
/// [`SharedMemory`] into other modules.
|
||||
pub(crate) fn vmimport(&self, store: &mut StoreOpaque) -> wasmtime_runtime::VMMemoryImport {
|
||||
let runtime_shared_memory = self.clone().0;
|
||||
let export_memory =
|
||||
generate_memory_export(store, &self.ty(), Some(runtime_shared_memory)).unwrap();
|
||||
VMMemoryImport {
|
||||
from: export_memory.definition,
|
||||
vmctx: export_memory.vmctx,
|
||||
index: export_memory.index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`SharedMemory`] from an [`ExportMemory`] definition. This
|
||||
/// function is available to handle the case in which a Wasm module exports
|
||||
/// shared memory and the user wants host-side access to it.
|
||||
pub(crate) unsafe fn from_wasmtime_memory(
|
||||
wasmtime_export: wasmtime_runtime::ExportMemory,
|
||||
store: &mut StoreOpaque,
|
||||
) -> Self {
|
||||
let mut handle = wasmtime_runtime::InstanceHandle::from_vmctx(wasmtime_export.vmctx);
|
||||
let memory = handle
|
||||
.get_defined_memory(wasmtime_export.index)
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
let shared_memory = memory
|
||||
.as_shared_memory()
|
||||
.expect("unable to convert from a shared memory");
|
||||
Self(shared_memory, store.engine().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
@@ -19,8 +19,8 @@ use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
use wasmtime_environ::{GlobalIndex, MemoryIndex, Module, SignatureIndex, TableIndex};
|
||||
use wasmtime_runtime::{
|
||||
Imports, InstanceAllocationRequest, InstanceAllocator, OnDemandInstanceAllocator, StorePtr,
|
||||
VMFunctionImport, VMSharedSignatureIndex,
|
||||
Imports, InstanceAllocationRequest, InstanceAllocator, OnDemandInstanceAllocator, SharedMemory,
|
||||
StorePtr, VMFunctionImport, VMSharedSignatureIndex,
|
||||
};
|
||||
|
||||
fn create_handle(
|
||||
@@ -68,8 +68,9 @@ pub fn generate_global_export(
|
||||
pub fn generate_memory_export(
|
||||
store: &mut StoreOpaque,
|
||||
m: &MemoryType,
|
||||
preallocation: Option<SharedMemory>,
|
||||
) -> Result<wasmtime_runtime::ExportMemory> {
|
||||
let instance = create_memory(store, m)?;
|
||||
let instance = create_memory(store, m, preallocation)?;
|
||||
Ok(store
|
||||
.instance_mut(instance)
|
||||
.get_exported_memory(MemoryIndex::from_u32(0)))
|
||||
|
||||
@@ -1,28 +1,84 @@
|
||||
use crate::memory::{LinearMemory, MemoryCreator};
|
||||
use crate::module::BareModuleInfo;
|
||||
use crate::store::{InstanceId, StoreOpaque};
|
||||
use crate::trampoline::create_handle;
|
||||
use crate::MemoryType;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
use wasmtime_environ::{EntityIndex, MemoryPlan, MemoryStyle, Module, WASM_PAGE_SIZE};
|
||||
use wasmtime_runtime::{
|
||||
MemoryImage, RuntimeLinearMemory, RuntimeMemoryCreator, VMMemoryDefinition,
|
||||
allocate_single_memory_instance, DefaultMemoryCreator, Imports, InstanceAllocationRequest,
|
||||
InstantiationError, Memory, MemoryImage, RuntimeLinearMemory, RuntimeMemoryCreator,
|
||||
SharedMemory, StorePtr, VMMemoryDefinition,
|
||||
};
|
||||
|
||||
pub fn create_memory(store: &mut StoreOpaque, memory: &MemoryType) -> Result<InstanceId> {
|
||||
/// Create a "frankenstein" instance with a single memory.
|
||||
///
|
||||
/// This separate instance is necessary because Wasm objects in Wasmtime must be
|
||||
/// attached to instances (versus the store, e.g.) and some objects exist
|
||||
/// outside: a host-provided memory import, shared memory.
|
||||
pub fn create_memory(
|
||||
store: &mut StoreOpaque,
|
||||
memory_ty: &MemoryType,
|
||||
preallocation: Option<SharedMemory>,
|
||||
) -> Result<InstanceId> {
|
||||
let mut module = Module::new();
|
||||
|
||||
let memory_plan = wasmtime_environ::MemoryPlan::for_memory(
|
||||
memory.wasmtime_memory().clone(),
|
||||
// Create a memory plan for the memory, though it will never be used for
|
||||
// constructing a memory with an allocator: instead the memories are either
|
||||
// preallocated (i.e., shared memory) or allocated manually below.
|
||||
let plan = wasmtime_environ::MemoryPlan::for_memory(
|
||||
memory_ty.wasmtime_memory().clone(),
|
||||
&store.engine().config().tunables,
|
||||
);
|
||||
let memory_id = module.memory_plans.push(memory_plan);
|
||||
let memory_id = module.memory_plans.push(plan.clone());
|
||||
|
||||
let memory = match &preallocation {
|
||||
// If we are passing in a pre-allocated shared memory, we can clone its
|
||||
// `Arc`. We know that a preallocated memory *must* be shared--it could
|
||||
// be used by several instances.
|
||||
Some(shared_memory) => shared_memory.clone().as_memory(),
|
||||
// If we do not have a pre-allocated memory, then we create it here and
|
||||
// associate it with the "frankenstein" instance, which now owns it.
|
||||
None => {
|
||||
let creator = &DefaultMemoryCreator;
|
||||
let store = unsafe {
|
||||
store
|
||||
.traitobj()
|
||||
.as_mut()
|
||||
.expect("the store pointer cannot be null here")
|
||||
};
|
||||
Memory::new_dynamic(&plan, creator, store, None)
|
||||
.map_err(|err| InstantiationError::Resource(err.into()))?
|
||||
}
|
||||
};
|
||||
|
||||
// Since we have only associated a single memory with the "frankenstein"
|
||||
// instance, it will be exported at index 0.
|
||||
debug_assert_eq!(memory_id.as_u32(), 0);
|
||||
module
|
||||
.exports
|
||||
.insert(String::new(), EntityIndex::Memory(memory_id));
|
||||
|
||||
create_handle(module, store, Box::new(()), &[], None)
|
||||
// We create an instance in the on-demand allocator when creating handles
|
||||
// associated with external objects. The configured instance allocator
|
||||
// should only be used when creating module instances as we don't want host
|
||||
// objects to count towards instance limits.
|
||||
let runtime_info = &BareModuleInfo::maybe_imported_func(Arc::new(module), None).into_traitobj();
|
||||
let host_state = Box::new(());
|
||||
let imports = Imports::default();
|
||||
let request = InstanceAllocationRequest {
|
||||
imports,
|
||||
host_state,
|
||||
store: StorePtr::new(store.traitobj()),
|
||||
runtime_info,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let handle = allocate_single_memory_instance(request, memory)?;
|
||||
let instance_id = store.add_instance(handle.clone(), true);
|
||||
Ok(instance_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LinearMemoryProxy {
|
||||
@@ -45,7 +101,7 @@ impl RuntimeLinearMemory for LinearMemoryProxy {
|
||||
fn vmmemory(&mut self) -> VMMemoryDefinition {
|
||||
VMMemoryDefinition {
|
||||
base: self.mem.as_ptr(),
|
||||
current_length: self.mem.byte_size(),
|
||||
current_length: self.mem.byte_size().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +109,6 @@ impl RuntimeLinearMemory for LinearMemoryProxy {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "pooling-allocator")]
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -373,6 +373,25 @@ impl MemoryType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new descriptor for shared WebAssembly memory given the
|
||||
/// specified limits of the memory.
|
||||
///
|
||||
/// The `minimum` and `maximum` values here are specified in units of
|
||||
/// WebAssembly pages, which are 64k.
|
||||
///
|
||||
/// Note that shared memories are part of the threads proposal for
|
||||
/// WebAssembly which is not standardized yet.
|
||||
pub fn shared(minimum: u32, maximum: u32) -> MemoryType {
|
||||
MemoryType {
|
||||
ty: Memory {
|
||||
memory64: false,
|
||||
shared: true,
|
||||
minimum: minimum.into(),
|
||||
maximum: Some(maximum.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this is a 64-bit memory or not.
|
||||
///
|
||||
/// Note that 64-bit memories are part of the memory64 proposal for
|
||||
@@ -381,6 +400,14 @@ impl MemoryType {
|
||||
self.ty.memory64
|
||||
}
|
||||
|
||||
/// Returns whether this is a shared memory or not.
|
||||
///
|
||||
/// Note that shared memories are part of the threads proposal for
|
||||
/// WebAssembly which is not standardized yet.
|
||||
pub fn is_shared(&self) -> bool {
|
||||
self.ty.shared
|
||||
}
|
||||
|
||||
/// Returns minimum number of WebAssembly pages this memory must have.
|
||||
///
|
||||
/// Note that the return value, while a `u64`, will always fit into a `u32`
|
||||
|
||||
@@ -35,6 +35,10 @@ impl MatchCx<'_> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn shared_memory(&self, expected: &Memory, actual: &crate::SharedMemory) -> Result<()> {
|
||||
memory_ty(expected, actual.ty().wasmtime_memory(), Some(actual.size()))
|
||||
}
|
||||
|
||||
pub fn func(&self, expected: SignatureIndex, actual: &crate::Func) -> Result<()> {
|
||||
self.vmshared_signature_index(expected, actual.sig_index(self.store.store_data()))
|
||||
}
|
||||
@@ -87,6 +91,7 @@ impl MatchCx<'_> {
|
||||
},
|
||||
EntityType::Memory(expected) => match actual {
|
||||
Extern::Memory(actual) => self.memory(expected, actual),
|
||||
Extern::SharedMemory(actual) => self.shared_memory(expected, actual),
|
||||
_ => bail!("expected memory, but found {}", actual.desc()),
|
||||
},
|
||||
EntityType::Function(expected) => match actual {
|
||||
|
||||
Reference in New Issue
Block a user