I want to create a SharedMemory in a wasmtime host, (to be shared with other modules) and pass a pointer into that shared memory space as an i32 parameter to an exported wasm function, where it will be cast to a slice. I know a module can only have a single linear shared memory (unless multiple memories is enabled), but IIUC an _imported_ memory can be different from the main linear memory of the module, in other words, a runtime like wasmtime maps both memories into the same 32-bit address space.
The only way I could find to associate a memory with a module is with Linker.define()
but the docs for that method say that it needs to match an import in the wasm module, and I couldn't figure out how to declare that in Rust.
Apparently wat
code to declare a shared memory import would look something like this:
(import "host" "memory" (memory 1 2 shared))
so the goal is to compile the rust to wasm and verify the import by running wasm2wat | grep import
. I didn't find any combination of flags for rustc, llvm, or linker that could generate this import in the wasm. I tried -C link-arg=--shared-memory -C target-feature=+atomics,+bulk-memory
but that didn't work. (prompted to rebuild std lib with atomics, but that still doesn't solve the problem that the generated wasm has no import).
I think it's possible in javascript ..
// Initializing the memory with 20 pages (20 * 64KiB = 1.25 MiB)
const memory = new WebAssembly.Memory({
initial: 1, maximum: 10, shared: true,
});
const imports = {
env: {
memory: memory
}
};
@Steve Schoettler, what you are describing in JS is the setup using an embedding API, just like one could use Wasmtime's Rust embedding API to create a shared memory and import it. That part of this story should work, even if there may be rough edges here or there--try it out and create issues for anything that doesn't seem right in the embedding API! The toolchain story is still a work in progress, though, and I wouldn't expect to be able to just "compile a threads example in Rust" and have it all work yet. In fact, the tracking issue has a task to teach all of the toolchains how to import the shared memory in the right way (@Dan Gohman had some ideas about this). In the meantime, I think you may have to manually alter the Wasm file so that it imports the shared memory.
Also related: the particular Rust toolchain flags you mention were broken up until recently when I filed and fixed this issue: https://github.com/rust-lang/rust/issues/102157. That issue and the accompanying PR have some comments about how one might build Wasm modules with atomics and shared memory enabled. I don't know what version of rustc
you are using but if you see errors like the ones in that issue, a nightly build of Rust should have my fix.
Thanks @Andrew Brown The tooling was a means to an end. If there were a way to pass a pointer to shared memory into wasm code, it wouldn't depend on the rust tooling. That could work for the use case of host allocating memory to use for message passing, separate from the guest's linear memory. Wasmtime's Module
contains ModuleMemoryImages
, a hashmap of MemoryImage
. A MemoryImage
can be static/shared, so its base address never changes. The path I started on was trying to add something to that hashmap, but the only public method I could find was Linker.define(). I'm wondering if I pr'd another method to add a memory to Module or Linker, could that work? I have a few known-unknowns: how to map a usize host memory pointer to an i32, since I can't offset it by the guest's base; and how to make sure that a pointer from a shared memory block isn't determined to be invalid by the wasm's memory safety controls.
Seprately, why is shared memory support dependent on the threads proposal? It would be useful in single-threaded scenarios as described above, where the host manages access to the memory. Is it because of the desire to put atomics into the shared memory block?
Seprately, why is shared memory support dependent on the threads proposal?
I think this is because the threads proposal was the original proposer of shared memory.
Thinking about what you're trying to do, I would take the high-level approach of using a shared memory in the embedding API (e.g.) and passing it in to the instantiation of each module you want to message pass with. I would not try the route of hacking on the Wasmtime runtime; seems too complicated. Does this example that I'm linking to make sense?
unfortunately, no workee. All those examples import the shared memory with a wat statement, as in
let wat = r#"(module (import "env" "memory" (memory 1 5 shared)))"#;
What i want to do is start with a wasm file that doesn't declare a shared memory import (because I can't in rust). If we replace the wat in one of those tests with
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
then the test fails with expected 0 imports, found 1
because the imports in that Instance
constructor don't match what was declared in the module. So I think that gets us back to - it needs to be declared, but the tooling from Rust doesn't support that.
hmm. I could test this theory by doing a post-processing step to edit the imports of the wasm binary using wasmparser/wasm-encoder
or wasm2wat | awk | wat2wasm
(I know, super hacky .. but it would tell us if the tooling _could_ be changed to support this kind of shared memory)
How are you compiling Rust? I'm seeing shared memory with RUSTFLAGS='-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals -Clink-arg=--import-memory' cargo +nightly build -Zbuild-std=panic_abort,std --target wasm32-unknown-unknown --release
oh that's interesting.. that builds.
wasm2wat gives me
000007d: error: memory may not be shared: threads not allowed
but wasm-dis foo.wasm | grep import
shows:
(import "env" "memory" (memory $mimport$0 (shared 17 16384)))
@Alex Crichton Please let me know if this is right: that creates a maximum shared space of 1GB (16k pages of 65536) .. does that get mapped into the same i32 address space as the guest's linear memory? Presumably the guest memory starts at 0 and this one starts at 1GB? That would go all the way to i32::MAX
(I realize there would be another api to get the offset pointer - I'm just wondering how it gets mapped into the i32 address space (if 64-bit wasm isn't enabled).
the shared memory there is the guest's linear memory so there's no other memory for the guest to use, and it starts as 17 pages large and goes up to a maximum of 16384 pages, 1GB. You can configure the maximum size with more linker flags as well.
Generally shared
doesn't actually change any Rust semantics, it's just a really low-level detail of how the module works. You've still only got one linear memory and it works the same as before, it's just shared
which tells the engine it can be shared on multiple threads
I thought a wasm module could have one main linear memory and one imported one. Did I misread that?
Isn't that what this wat statement does?
(import "host" "memory" (memory 1 2 shared))
While you're technically correct importing and defining a memory requires the multi memory proposal for wasm which is not stable. Additionally LLVM based languages like Rust do not support multi memory so it's either imported or defined locally, never both
I'm not sure what you may be seeing, but note that an export is not a definition, and it's possible to both import and then export, which still only means one linear memory
Also, your issues with wasm2wat
might be due to not passing the --enable-threads
flag.
@Andrew Brown and @Alex Crichton thanks for you help clearing that up for me.
Steve Schoettler has marked this topic as resolved.
Last updated: Dec 23 2024 at 12:05 UTC