Stream: wasi-threads

Topic: ✔ accessing shared memory from rust wasm


view this post on Zulip Steve Schoettler (Dec 05 2022 at 23:30):

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
    }
};

view this post on Zulip Andrew Brown (Dec 06 2022 at 00:21):

@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.

A fast and secure runtime for WebAssembly. Contribute to bytecodealliance/wasmtime development by creating an account on GitHub.
Several of us (@loganek, @sunfishcode, @sunfishcode, @haraldh) have been working towards implementing all of the pieces to demonstrate an end-to-end wasi-threads example. The current direction is t...

view this post on Zulip Andrew Brown (Dec 06 2022 at 00:23):

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.

It is currently impossible to generate a Wasm module that imports/exports a shared memory using a Rust program. Here is the example program I'm using (though this is not a terribly important de...

view this post on Zulip Steve Schoettler (Dec 06 2022 at 17:56):

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?

view this post on Zulip Andrew Brown (Dec 07 2022 at 00:52):

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?

Threads and Atomics in WebAssembly. Contribute to WebAssembly/threads development by creating an account on GitHub.
A fast and secure runtime for WebAssembly. Contribute to bytecodealliance/wasmtime development by creating an account on GitHub.

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:28):

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.

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:34):

hmm. I could test this theory by doing a post-processing step to edit the imports of the wasm binary using wasmparser/wasm-encoder

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:34):

or wasm2wat | awk | wat2wasm

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:37):

(I know, super hacky .. but it would tell us if the tooling _could_ be changed to support this kind of shared memory)

view this post on Zulip Alex Crichton (Dec 07 2022 at 01:45):

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

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:53):

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)))

view this post on Zulip Steve Schoettler (Dec 07 2022 at 01:59):

@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

view this post on Zulip Steve Schoettler (Dec 07 2022 at 02:01):

(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).

view this post on Zulip Alex Crichton (Dec 07 2022 at 04:41):

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

view this post on Zulip Steve Schoettler (Dec 07 2022 at 09:01):

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))

view this post on Zulip Alex Crichton (Dec 07 2022 at 14:20):

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

view this post on Zulip Alex Crichton (Dec 07 2022 at 14:21):

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

view this post on Zulip Andrew Brown (Dec 08 2022 at 18:10):

Also, your issues with wasm2wat might be due to not passing the --enable-threads flag.

view this post on Zulip Steve Schoettler (Dec 09 2022 at 22:55):

@Andrew Brown and @Alex Crichton thanks for you help clearing that up for me.

view this post on Zulip Notification Bot (Dec 12 2022 at 07:23):

Steve Schoettler has marked this topic as resolved.


Last updated: Dec 23 2024 at 12:05 UTC