From 7e88888981de69d74f7f3660bd84b154dd118f58 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 16 Sep 2025 09:46:05 +0100 Subject: [PATCH 01/13] Initial work --- Cargo.lock | 134 +++----- Cargo.toml | 120 +++++-- crates/cli-flags/src/lib.rs | 12 +- crates/component-macro/src/bindgen.rs | 6 +- crates/cranelift/src/compiler/component.rs | 102 +++++- crates/environ/src/component.rs | 18 +- crates/environ/src/component/dfg.rs | 55 ++- crates/environ/src/component/info.rs | 50 ++- crates/environ/src/component/translate.rs | 86 ++++- .../environ/src/component/translate/adapt.rs | 1 + .../environ/src/component/translate/inline.rs | 101 +++++- crates/environ/src/fact.rs | 2 + crates/environ/src/trap_encoding.rs | 6 + crates/fuzzing/src/generators/config.rs | 15 +- crates/fuzzing/src/generators/module.rs | 6 +- .../tests/scenario/coop_threads.rs | 19 + .../tests/scenario/mod.rs | 1 + .../tests/scenario/util.rs | 3 +- .../component-async-tests/tests/test_all.rs | 1 + .../misc/component-async-tests/wit/test.wit | 13 + crates/test-programs/src/async_.rs | 11 + .../src/bin/async_coop_threads_callee.rs | 25 ++ .../src/bin/async_coop_threads_caller.rs | 25 ++ crates/test-util/src/wasmtime_wast.rs | 9 +- crates/test-util/src/wast.rs | 3 +- crates/wasmtime/src/config.rs | 20 +- .../src/runtime/component/concurrent.rs | 324 ++++++++++++++++-- .../src/runtime/vm/component/libcalls.rs | 75 +++- tests/all/async_functions.rs | 6 +- tests/all/component_model/async.rs | 14 +- tests/all/component_model/func.rs | 2 +- tests/all/pooling_allocator.rs | 6 +- tests/all/pulley.rs | 3 +- .../component-model-async/fused.wast | 2 +- .../component-model-async/futures.wast | 2 +- .../component-model-async/lift.wast | 2 +- .../component-model-async/streams.wast | 2 +- .../component-model-async/task-builtins.wast | 14 +- 38 files changed, 1023 insertions(+), 273 deletions(-) create mode 100644 crates/misc/component-async-tests/tests/scenario/coop_threads.rs create mode 100644 crates/test-programs/src/bin/async_coop_threads_callee.rs create mode 100644 crates/test-programs/src/bin/async_coop_threads_caller.rs diff --git a/Cargo.lock b/Cargo.lock index 38079579b622..e93aaff40448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,7 +625,7 @@ dependencies = [ "test-programs-artifacts", "tokio", "wasm-compose", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime", "wasmtime-wasi", ] @@ -2174,14 +2174,12 @@ dependencies = [ [[package]] name = "json-from-wast" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a4c2a6cda223c0434456dcc4b159a41299eff6bce6393e08ccd79fb021fe611" +version = "0.238.0" dependencies = [ "anyhow", "serde", "serde_derive", - "wast 237.0.0", + "wast 238.0.0", ] [[package]] @@ -3658,7 +3656,7 @@ dependencies = [ "wasmtime", "wasmtime-test-util", "wat", - "wit-component 0.237.0", + "wit-component 0.238.0", ] [[package]] @@ -4079,7 +4077,7 @@ name = "verify-component-adapter" version = "37.0.0" dependencies = [ "anyhow", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wat", ] @@ -4189,7 +4187,7 @@ dependencies = [ "byte-array-literals", "object 0.37.3", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-encoder 0.237.0", + "wasm-encoder 0.238.0", "wit-bindgen-rust-macro", ] @@ -4250,9 +4248,7 @@ checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "wasm-compose" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4691ea85473e81ea13f91088854186eb792710d7c1af8b07bc1ad1ab3a02404f" +version = "0.238.0" dependencies = [ "anyhow", "heck 0.4.1", @@ -4264,8 +4260,8 @@ dependencies = [ "serde_derive", "serde_yaml", "smallvec", - "wasm-encoder 0.237.0", - "wasmparser 0.237.0", + "wasm-encoder 0.238.0", + "wasmparser 0.238.0", "wat", ] @@ -4281,12 +4277,10 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe92d1321afa53ffc88a57c497bb7330c3cf84c98ffdba4a4caf6a0684fad3c" +version = "0.238.0" dependencies = [ "leb128fmt", - "wasmparser 0.237.0", + "wasmparser 0.238.0", ] [[package]] @@ -4303,42 +4297,36 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc0b0a0c4f35ca6efa7a797671372915d4e9659dba2d59edc6fafc931d19997" +version = "0.238.0" dependencies = [ "anyhow", "indexmap 2.7.0", - "wasm-encoder 0.237.0", - "wasmparser 0.237.0", + "wasm-encoder 0.238.0", + "wasmparser 0.238.0", ] [[package]] name = "wasm-mutate" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab74bae702909617bb0553dfd70bf2ad35665deba920df1c6b9c4273d1a5ca33" +version = "0.238.0" dependencies = [ "egg", "log", "rand 0.9.2", "thiserror 2.0.12", - "wasm-encoder 0.237.0", - "wasmparser 0.237.0", + "wasm-encoder 0.238.0", + "wasmparser 0.238.0", ] [[package]] name = "wasm-smith" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a46a42b04950b2853391ae55a9cede42a52b886bfcbb4471c6553eb9842d720" +version = "0.238.0" dependencies = [ "anyhow", "arbitrary", "flagset", "serde", "serde_derive", - "wasm-encoder 0.237.0", + "wasm-encoder 0.238.0", "wat", ] @@ -4352,14 +4340,12 @@ dependencies = [ [[package]] name = "wasm-wave" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1934753e86295ac90adeac4a7195b3ef1121e8954ffbdf1a2107088abc88ee4c" +version = "0.238.0" dependencies = [ "indexmap 2.7.0", "logos", "thiserror 2.0.12", - "wit-parser 0.237.0", + "wit-parser 0.238.0", ] [[package]] @@ -4430,9 +4416,7 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2a40ca0d2bdf4b0bf36c13a737d0b2c58e4c8aaefe1c57f336dd75369ca250" +version = "0.238.0" dependencies = [ "bitflags 2.6.0", "hashbrown 0.15.2", @@ -4443,13 +4427,11 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00c979bd801d8d7e4b40de564bcb27526fcbaf58e3aff15fe2df7e135f5b397" +version = "0.238.0" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.237.0", + "wasmparser 0.238.0", ] [[package]] @@ -4494,9 +4476,9 @@ dependencies = [ "target-lexicon", "tempfile", "tokio", - "wasm-encoder 0.237.0", + "wasm-encoder 0.238.0", "wasm-wave", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime-environ", "wasmtime-internal-asm-macros", "wasmtime-internal-cache", @@ -4605,8 +4587,8 @@ dependencies = [ "tracing", "walkdir", "wasi-common", - "wasm-encoder 0.237.0", - "wasmparser 0.237.0", + "wasm-encoder 0.238.0", + "wasmparser 0.238.0", "wasmtime", "wasmtime-cli-flags", "wasmtime-environ", @@ -4625,10 +4607,10 @@ dependencies = [ "wasmtime-wasi-threads", "wasmtime-wasi-tls", "wasmtime-wast", - "wast 237.0.0", + "wast 238.0.0", "wat", "windows-sys 0.60.2", - "wit-component 0.237.0", + "wit-component 0.238.0", ] [[package]] @@ -4667,8 +4649,8 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasm-encoder 0.237.0", - "wasmparser 0.237.0", + "wasm-encoder 0.238.0", + "wasmparser 0.238.0", "wasmprinter", "wasmtime-internal-component-util", "wat", @@ -4681,7 +4663,7 @@ dependencies = [ "arbitrary", "env_logger 0.11.5", "libfuzzer-sys", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmprinter", "wasmtime-environ", "wasmtime-test-util", @@ -4714,7 +4696,7 @@ dependencies = [ "rand 0.9.2", "smallvec", "target-lexicon", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime", "wasmtime-fuzzing", "wasmtime-test-util", @@ -4738,12 +4720,12 @@ dependencies = [ "target-lexicon", "tempfile", "v8", - "wasm-encoder 0.237.0", + "wasm-encoder 0.238.0", "wasm-mutate", "wasm-smith", "wasm-spec-interpreter", "wasmi", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmprinter", "wasmtime", "wasmtime-cli-flags", @@ -4805,7 +4787,7 @@ dependencies = [ "wasmtime", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.237.0", + "wit-parser 0.238.0", ] [[package]] @@ -4831,7 +4813,7 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.12", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime-environ", "wasmtime-internal-math", "wasmtime-internal-unwinder", @@ -4929,7 +4911,7 @@ dependencies = [ "log", "object 0.37.3", "target-lexicon", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -4943,7 +4925,7 @@ dependencies = [ "bitflags 2.6.0", "heck 0.5.0", "indexmap 2.7.0", - "wit-parser 0.237.0", + "wit-parser 0.238.0", ] [[package]] @@ -5151,9 +5133,9 @@ dependencies = [ "object 0.37.3", "serde_json", "tokio", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime", - "wast 237.0.0", + "wast 238.0.0", ] [[package]] @@ -5167,25 +5149,21 @@ dependencies = [ [[package]] name = "wast" -version = "237.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf66f545acbd55082485cb9a6daab54579cb8628a027162253e8e9f5963c767" +version = "238.0.0" dependencies = [ "bumpalo", "gimli 0.31.1", "leb128fmt", "memchr", "unicode-width 0.2.0", - "wasm-encoder 0.237.0", + "wasm-encoder 0.238.0", ] [[package]] name = "wat" -version = "1.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27975186f549e4b8d6878b627be732863883c72f7bf4dcf8f96e5f8242f73da9" +version = "1.238.0" dependencies = [ - "wast 237.0.0", + "wast 238.0.0", ] [[package]] @@ -5315,7 +5293,7 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.12", - "wasmparser 0.237.0", + "wasmparser 0.238.0", "wasmtime-environ", "wasmtime-internal-cranelift", "wasmtime-internal-math", @@ -5646,9 +5624,7 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb7674f76c10e82fe00b256a9d4ffb2b8d037d42ab8e9a83ebb3be35c9d0bf6" +version = "0.238.0" dependencies = [ "anyhow", "bitflags 2.6.0", @@ -5657,10 +5633,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.237.0", - "wasm-metadata 0.237.0", - "wasmparser 0.237.0", - "wit-parser 0.237.0", + "wasm-encoder 0.238.0", + "wasm-metadata 0.238.0", + "wasmparser 0.238.0", + "wit-parser 0.238.0", ] [[package]] @@ -5683,9 +5659,7 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.237.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2596a5bc7c24cc965b56ad6ff9e32394c4e401764f89620a888519c6e849ab" +version = "0.238.0" dependencies = [ "anyhow", "id-arena", @@ -5696,7 +5670,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.237.0", + "wasmparser 0.238.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 902dd5fbf452..4f13eb7bd23e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,10 @@ wasmtime-cranelift = { workspace = true, optional = true } wasmtime-environ = { workspace = true } wasmtime-explorer = { workspace = true, optional = true } wasmtime-wast = { workspace = true, optional = true } -wasi-common = { workspace = true, default-features = true, features = ["exit", "tokio"], optional = true } +wasi-common = { workspace = true, default-features = true, features = [ + "exit", + "tokio", +], optional = true } wasmtime-wasi = { workspace = true, default-features = true, optional = true } wasmtime-wasi-nn = { workspace = true, optional = true } wasmtime-wasi-config = { workspace = true, optional = true } @@ -82,7 +85,7 @@ pulley-interpreter = { workspace = true, optional = true } async-trait = { workspace = true } bytes = { workspace = true } cfg-if = { workspace = true } -tokio = { workspace = true, optional = true, features = [ "signal", "macros" ] } +tokio = { workspace = true, optional = true, features = ["signal", "macros"] } hyper = { workspace = true, optional = true } http = { workspace = true, optional = true } http-body-util = { workspace = true, optional = true } @@ -92,12 +95,25 @@ rustix = { workspace = true, features = ["mm", "process"] } [dev-dependencies] # depend again on wasmtime to activate its default features for tests -wasmtime = { workspace = true, features = ['default', 'winch', 'pulley', 'all-arch', 'call-hook', 'memory-protection-keys', 'component-model-async'] } +wasmtime = { workspace = true, features = [ + 'default', + 'winch', + 'pulley', + 'all-arch', + 'call-hook', + 'memory-protection-keys', + 'component-model-async', +] } env_logger = { workspace = true } log = { workspace = true } filecheck = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["rt", "time", "macros", "rt-multi-thread"] } +tokio = { workspace = true, features = [ + "rt", + "time", + "macros", + "rt-multi-thread", +] } wast = { workspace = true } criterion = { workspace = true } num_cpus = "1.13.0" @@ -107,7 +123,10 @@ wat = { workspace = true } rayon = "1.5.0" wasmtime-wast = { workspace = true, features = ['component-model'] } wasmtime-component-util = { workspace = true } -wasmtime-test-util = { workspace = true, features = ['wasmtime-wast', 'component'] } +wasmtime-test-util = { workspace = true, features = [ + 'wasmtime-wast', + 'component', +] } bstr = "1.6.0" libc = { workspace = true } serde = { workspace = true } @@ -117,7 +136,11 @@ test-programs-artifacts = { workspace = true } bytesize = "2.0.1" wit-component = { workspace = true } cranelift-filetests = { workspace = true } -cranelift-codegen = { workspace = true, features = ["disas", "trace-log", "timing"] } +cranelift-codegen = { workspace = true, features = [ + "disas", + "trace-log", + "timing", +] } cranelift-reader = { workspace = true } toml = { workspace = true } similar = { workspace = true } @@ -170,9 +193,7 @@ members = [ "fuzz", "winch/codegen", ] -exclude = [ - 'docs/rust_wasi_markdown_parser', -] +exclude = ['docs/rust_wasi_markdown_parser'] [workspace.package] version = "37.0.0" @@ -253,8 +274,8 @@ wasmtime-wmemcheck = { path = "crates/wmemcheck", version = "=37.0.0", package = wasmtime-c-api-macros = { path = "crates/c-api-macros", version = "=37.0.0", package = 'wasmtime-internal-c-api-macros' } wasmtime-cache = { path = "crates/cache", version = "=37.0.0", package = 'wasmtime-internal-cache' } wasmtime-cranelift = { path = "crates/cranelift", version = "=37.0.0", package = 'wasmtime-internal-cranelift' } -wasmtime-winch = { path = "crates/winch", version = "=37.0.0", package = 'wasmtime-internal-winch' } -wasmtime-explorer = { path = "crates/explorer", version = "=37.0.0", package = 'wasmtime-internal-explorer' } +wasmtime-winch = { path = "crates/winch", version = "=37.0.0", package = 'wasmtime-internal-winch' } +wasmtime-explorer = { path = "crates/explorer", version = "=37.0.0", package = 'wasmtime-internal-explorer' } wasmtime-fiber = { path = "crates/fiber", version = "=37.0.0", package = 'wasmtime-internal-fiber' } wasmtime-jit-debug = { path = "crates/jit-debug", version = "=37.0.0", package = 'wasmtime-internal-jit-debug' } wasmtime-component-util = { path = "crates/component-util", version = "=37.0.0", package = 'wasmtime-internal-component-util' } @@ -264,7 +285,7 @@ wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", ve wasmtime-slab = { path = "crates/slab", version = "=37.0.0", package = 'wasmtime-internal-slab' } wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=37.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=37.0.0", package = 'wasmtime-internal-wit-bindgen' } -wasmtime-math = { path = "crates/math", version = "=37.0.0", package = 'wasmtime-internal-math' } +wasmtime-math = { path = "crates/math", version = "=37.0.0", package = 'wasmtime-internal-math' } wasmtime-unwinder = { path = "crates/unwinder", version = "=37.0.0", package = 'wasmtime-internal-unwinder' } # Miscellaneous crates without a `wasmtime-*` prefix in their name but still @@ -278,7 +299,10 @@ pulley-macros = { path = 'pulley/macros', version = "=37.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.124.0" } -cranelift-codegen = { path = "cranelift/codegen", version = "0.124.0", default-features = false, features = ["std", "unwind"] } +cranelift-codegen = { path = "cranelift/codegen", version = "0.124.0", default-features = false, features = [ + "std", + "unwind", +] } cranelift-frontend = { path = "cranelift/frontend", version = "0.124.0" } cranelift-entity = { path = "cranelift/entity", version = "0.124.0" } cranelift-native = { path = "cranelift/native", version = "0.124.0" } @@ -330,32 +354,40 @@ wit-bindgen-rt = { version = "0.43.0", default-features = false } wit-bindgen-rust-macro = { version = "0.43.0", default-features = false } # wasm-tools family: -wasmparser = { version = "0.237.0", default-features = false, features = ['simd'] } -wat = "1.237.0" -wast = "237.0.0" -wasmprinter = "0.237.0" -wasm-encoder = "0.237.0" -wasm-smith = "0.237.0" -wasm-mutate = "0.237.0" -wit-parser = "0.237.0" -wit-component = "0.237.0" -wasm-wave = "0.237.0" -wasm-compose = "0.237.0" -json-from-wast = "0.237.0" +wasmparser = { path = "../wasm-tools/crates/wasmparser", default-features = false, features = [ + 'simd', +] } +wat = { path = "../wasm-tools/crates/wat" } +wast = { path = "../wasm-tools/crates/wast" } +wasmprinter = { path = "../wasm-tools/crates/wasmprinter" } +wasm-encoder = { path = "../wasm-tools/crates/wasm-encoder" } +wasm-smith = { path = "../wasm-tools/crates/wasm-smith" } +wasm-mutate = { path = "../wasm-tools/crates/wasm-mutate" } +wit-parser = { path = "../wasm-tools/crates/wit-parser" } +wit-component = { path = "../wasm-tools/crates/wit-component" } +wasm-wave = { path = "../wasm-tools/crates/wasm-wave" } +wasm-compose = { path = "../wasm-tools/crates/wasm-compose" } +json-from-wast = { path = "../wasm-tools/crates/json-from-wast" } # Non-Bytecode Alliance maintained dependencies: # -------------------------- arbitrary = "1.4.0" mutatis = "0.3.2" cc = "1.0" -object = { version = "0.37.3", default-features = false, features = ['read_core', 'elf'] } +object = { version = "0.37.3", default-features = false, features = [ + 'read_core', + 'elf', +] } gimli = { version = "0.32.0", default-features = false, features = ['read'] } addr2line = { version = "0.25.0", default-features = false } anyhow = { version = "1.0.93", default-features = false } windows-sys = "0.60.0" env_logger = "0.11.5" log = { version = "0.4.27", default-features = false } -clap = { version = "4.5.17", default-features = false, features = ["std", "derive"] } +clap = { version = "4.5.17", default-features = false, features = [ + "std", + "derive", +] } clap_complete = "4.4.7" hashbrown = { version = "0.15", default-features = false } capstone = "0.13.0" @@ -384,7 +416,7 @@ tempfile = "3.1.0" filecheck = "0.5.0" libc = { version = "0.2.112", default-features = true } file-per-thread-logger = "0.2.0" -tokio = { version = "1.43.0", features = [ "rt", "time" ] } +tokio = { version = "1.43.0", features = ["rt", "time"] } hyper = "1.0.1" http = "1.0.0" http-body = "1.0.0" @@ -396,10 +428,18 @@ syn = "2.0.25" quote = "1.0" proc-macro2 = "1.0" test-log = { version = "0.2", default-features = false, features = ["trace"] } -tracing-subscriber = { version = "0.3.1", default-features = false, features = ['fmt', 'env-filter', 'ansi', 'tracing-log'] } +tracing-subscriber = { version = "0.3.1", default-features = false, features = [ + 'fmt', + 'env-filter', + 'ansi', + 'tracing-log', +] } url = "2.3.1" postcard = { version = "1.0.8", default-features = false, features = ['alloc'] } -criterion = { version = "0.6.0", default-features = false, features = ["html_reports", "rayon"] } +criterion = { version = "0.6.0", default-features = false, features = [ + "html_reports", + "rayon", +] } rustc-hash = "2.0.0" libtest-mimic = "0.8.1" semver = { version = "1.0.17", default-features = false } @@ -505,14 +545,23 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"] wasi-nn = ["dep:wasmtime-wasi-nn"] wasi-tls = ["dep:wasmtime-wasi-tls"] wasi-threads = ["dep:wasmtime-wasi-threads", "threads"] -wasi-http = ["component-model", "dep:wasmtime-wasi-http", "dep:tokio", "dep:hyper", "wasmtime-wasi-http/default-send-request"] +wasi-http = [ + "component-model", + "dep:wasmtime-wasi-http", + "dep:tokio", + "dep:hyper", + "wasmtime-wasi-http/default-send-request", +] wasi-config = ["dep:wasmtime-wasi-config"] wasi-keyvalue = ["dep:wasmtime-wasi-keyvalue"] -pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"] +pooling-allocator = [ + "wasmtime/pooling-allocator", + "wasmtime-cli-flags/pooling-allocator", +] component-model = [ "wasmtime/component-model", "wasmtime-wast?/component-model", - "wasmtime-cli-flags/component-model" + "wasmtime-cli-flags/component-model", ] wat = ["dep:wat", "wasmtime/wat"] cache = ["dep:wasmtime-cache", "wasmtime-cli-flags/cache"] @@ -529,7 +578,10 @@ gc = ["wasmtime-cli-flags/gc", "wasmtime/gc"] gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"] gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"] pulley = ["wasmtime-cli-flags/pulley"] -stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"] +stack-switching = [ + "wasmtime/stack-switching", + "wasmtime-cli-flags/stack-switching", +] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index f0cea981c550..a12698c8af1f 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -375,12 +375,9 @@ wasmtime_option_group! { pub component_model: Option, /// Component model support for async lifting/lowering. pub component_model_async: Option, - /// Component model support for async lifting/lowering: this corresponds - /// to the ๐Ÿš emoji in the component model specification. - pub component_model_async_builtins: Option, - /// Component model support for async lifting/lowering: this corresponds - /// to the ๐ŸšŸ emoji in the component model specification. - pub component_model_async_stackful: Option, + /// Component model support for threading: this corresponds + /// to the ๐Ÿงต emoji in the component model specification. + pub component_model_threading: Option, /// Component model support for `error-context`: this corresponds /// to the ๐Ÿ“ emoji in the component model specification. pub component_model_error_context: Option, @@ -1050,8 +1047,7 @@ impl CommonOptions { handle_conditionally_compiled! { ("component-model", component_model, wasm_component_model) ("component-model-async", component_model_async, wasm_component_model_async) - ("component-model-async", component_model_async_builtins, wasm_component_model_async_builtins) - ("component-model-async", component_model_async_stackful, wasm_component_model_async_stackful) + ("component-model-async", component_model_threading, wasm_component_model_threading) ("component-model", component_model_error_context, wasm_component_model_error_context) ("threads", threads, wasm_threads) ("gc", gc, wasm_gc) diff --git a/crates/component-macro/src/bindgen.rs b/crates/component-macro/src/bindgen.rs index c97832e97d35..bc258e096829 100644 --- a/crates/component-macro/src/bindgen.rs +++ b/crates/component-macro/src/bindgen.rs @@ -234,7 +234,7 @@ fn select_world( world: Option<&str>, ) -> anyhow::Result { if pkgs.len() == 1 { - resolve.select_world(pkgs[0], world) + resolve.select_world(pkgs, world) } else { assert!(!pkgs.is_empty()); match world { @@ -248,12 +248,12 @@ fn select_world( // This will ignore the package argument due to the fully // qualified name being used. - resolve.select_world(pkgs[0], world) + resolve.select_world(pkgs, world) } None => { let worlds = pkgs .iter() - .filter_map(|p| resolve.select_world(*p, None).ok()) + .filter_map(|p| resolve.select_world(pkgs, None).ok()) .collect::>(); match &worlds[..] { [] => anyhow::bail!("no packages have a world"), diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index c99908bac22a..6ed34409ac7b 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -271,23 +271,39 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::WaitableSetWait { options } => { + Trampoline::WaitableSetWait { + options, + cancellable, + } => { self.translate_libcall( host::waitable_set_wait, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { params.push(me.index_value(*options)); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); }, ); } - Trampoline::WaitableSetPoll { options } => { + Trampoline::WaitableSetPoll { + options, + cancellable, + } => { self.translate_libcall( host::waitable_set_poll, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { params.push(me.index_value(*options)); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); }, ); } @@ -311,13 +327,17 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::Yield { async_ } => { + Trampoline::ThreadYield { cancellable } => { self.translate_libcall( - host::yield_, + host::thread_yield, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { - params.push(me.builder.ins().iconst(ir::types::I8, i64::from(*async_))); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); }, ); } @@ -693,6 +713,78 @@ impl<'a> TrampolineCompiler<'a> { }, ); } + Trampoline::ThreadIndex => { + self.translate_libcall( + host::thread_index, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |_, _| {}, + ); + } + Trampoline::ThreadNewIndirect { + start_func_table_idx, + start_func_ty_idx, + } => { + self.translate_libcall( + host::thread_new_indirect, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*start_func_table_idx)); + params.push(me.index_value(*start_func_ty_idx)); + }, + ); + } + Trampoline::ThreadSwitchTo { cancellable } => { + self.translate_libcall( + host::thread_switch_to, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } + Trampoline::ThreadSuspend { cancellable } => { + self.translate_libcall( + host::thread_suspend, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } + Trampoline::ThreadResumeLater => { + self.translate_libcall( + host::thread_resume_later, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |_, _| {}, + ); + } + Trampoline::ThreadYieldTo { cancellable } => { + self.translate_libcall( + host::thread_yield_to, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } } } diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 4576f9c0f31c..a7e407f667bb 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -106,15 +106,15 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] waitable_set_new(vmctx: vmctx, caller_instance: u32) -> u64; #[cfg(feature = "component-model-async")] - waitable_set_wait(vmctx: vmctx, options: u32, set: u32, payload: u32) -> u64; + waitable_set_wait(vmctx: vmctx, options: u32, cancellable: u8, set: u32, payload: u32) -> u64; #[cfg(feature = "component-model-async")] - waitable_set_poll(vmctx: vmctx, options: u32, set: u32, payload: u32) -> u64; + waitable_set_poll(vmctx: vmctx, options: u32, cancellable: u8, set: u32, payload: u32) -> u64; #[cfg(feature = "component-model-async")] waitable_set_drop(vmctx: vmctx, caller_instance: u32, set: u32) -> bool; #[cfg(feature = "component-model-async")] waitable_join(vmctx: vmctx, caller_instance: u32, set: u32, waitable: u32) -> bool; #[cfg(feature = "component-model-async")] - yield_(vmctx: vmctx, async_: u8) -> u32; + thread_yield(vmctx: vmctx, cancellable: u8) -> u32; #[cfg(feature = "component-model-async")] subtask_drop(vmctx: vmctx, caller_instance: u32, task_id: u32) -> bool; #[cfg(feature = "component-model-async")] @@ -185,6 +185,18 @@ macro_rules! foreach_builtin_component_function { context_get(vmctx: vmctx, slot: u32) -> u64; #[cfg(feature = "component-model-async")] context_set(vmctx: vmctx, slot: u32, val: u32) -> bool; + #[cfg(feature = "component-model-async")] + thread_index(vmctx: vmctx) -> u64; + #[cfg(feature = "component-model-async")] + thread_new_indirect(vmctx: vmctx, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; + #[cfg(feature = "component-model-async")] + thread_switch_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u64; + #[cfg(feature = "component-model-async")] + thread_suspend(vmctx: vmctx, cancellable: u8) -> u64; + #[cfg(feature = "component-model-async")] + thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; + #[cfg(feature = "component-model-async")] + thread_yield_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u64; trap(vmctx: vmctx, code: u8) -> bool; diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index cbdb7be53c58..e1314fd926cb 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -334,9 +334,11 @@ pub enum Trampoline { }, WaitableSetWait { options: OptionsId, + cancellable: bool, }, WaitableSetPoll { options: OptionsId, + cancellable: bool, }, WaitableSetDrop { instance: RuntimeComponentInstanceIndex, @@ -344,8 +346,8 @@ pub enum Trampoline { WaitableJoin { instance: RuntimeComponentInstanceIndex, }, - Yield { - async_: bool, + ThreadYield { + cancellable: bool, }, SubtaskDrop { instance: RuntimeComponentInstanceIndex, @@ -434,6 +436,21 @@ pub enum Trampoline { ErrorContextTransfer, ContextGet(u32), ContextSet(u32), + ThreadIndex, + ThreadNewIndirect { + start_func_ty_idx: ComponentTypeIndex, + start_func_table_idx: TableIndex, + }, + ThreadSwitchTo { + cancellable: bool, + }, + ThreadSuspend { + cancellable: bool, + }, + ThreadResumeLater, + ThreadYieldTo { + cancellable: bool, + }, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] @@ -868,11 +885,19 @@ impl LinearizeDfg<'_> { Trampoline::WaitableSetNew { instance } => info::Trampoline::WaitableSetNew { instance: *instance, }, - Trampoline::WaitableSetWait { options } => info::Trampoline::WaitableSetWait { + Trampoline::WaitableSetWait { + options, + cancellable, + } => info::Trampoline::WaitableSetWait { options: self.options(*options), + cancellable: *cancellable, }, - Trampoline::WaitableSetPoll { options } => info::Trampoline::WaitableSetPoll { + Trampoline::WaitableSetPoll { + options, + cancellable, + } => info::Trampoline::WaitableSetPoll { options: self.options(*options), + cancellable: *cancellable, }, Trampoline::WaitableSetDrop { instance } => info::Trampoline::WaitableSetDrop { instance: *instance, @@ -880,7 +905,9 @@ impl LinearizeDfg<'_> { Trampoline::WaitableJoin { instance } => info::Trampoline::WaitableJoin { instance: *instance, }, - Trampoline::Yield { async_ } => info::Trampoline::Yield { async_: *async_ }, + Trampoline::ThreadYield { cancellable } => info::Trampoline::ThreadYield { + cancellable: *cancellable, + }, Trampoline::SubtaskDrop { instance } => info::Trampoline::SubtaskDrop { instance: *instance, }, @@ -967,6 +994,24 @@ impl LinearizeDfg<'_> { Trampoline::ErrorContextTransfer => info::Trampoline::ErrorContextTransfer, Trampoline::ContextGet(i) => info::Trampoline::ContextGet(*i), Trampoline::ContextSet(i) => info::Trampoline::ContextSet(*i), + Trampoline::ThreadIndex => info::Trampoline::ThreadIndex, + Trampoline::ThreadNewIndirect { + start_func_ty_idx, + start_func_table_idx, + } => info::Trampoline::ThreadNewIndirect { + start_func_ty_idx: *start_func_ty_idx, + start_func_table_idx: *start_func_table_idx, + }, + Trampoline::ThreadSwitchTo { cancellable } => info::Trampoline::ThreadSwitchTo { + cancellable: *cancellable, + }, + Trampoline::ThreadSuspend { cancellable } => info::Trampoline::ThreadSuspend { + cancellable: *cancellable, + }, + Trampoline::ThreadResumeLater => info::Trampoline::ThreadResumeLater, + Trampoline::ThreadYieldTo { cancellable } => info::Trampoline::ThreadYieldTo { + cancellable: *cancellable, + }, }; let i1 = self.trampolines.push(*signature); let i2 = self.trampoline_defs.push(trampoline); diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index b939b12b466e..055a381470a7 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -761,6 +761,8 @@ pub enum Trampoline { WaitableSetWait { /// Configuration options for this intrinsic call. options: OptionsIndex, + /// If `true`, indicates the caller instance maybe reentered. + cancellable: bool, }, /// A `waitable-set.poll` intrinsic, which checks whether any outstanding @@ -769,6 +771,8 @@ pub enum Trampoline { WaitableSetPoll { /// Configuration options for this intrinsic call. options: OptionsIndex, + /// If `true`, indicates the caller instance maybe reentered. + cancellable: bool, }, /// A `waitable-set.drop` intrinsic. @@ -783,11 +787,11 @@ pub enum Trampoline { instance: RuntimeComponentInstanceIndex, }, - /// A `yield` intrinsic, which yields control to the host so that other + /// A `thread.yield` intrinsic, which yields control to the host so that other /// tasks are able to make progress, if any. - Yield { + ThreadYield { /// If `true`, indicates the caller instance maybe reentered. - async_: bool, + cancellable: bool, }, /// A `subtask.drop` intrinsic to drop a specified task which has completed. @@ -1038,6 +1042,38 @@ pub enum Trampoline { /// The payload here represents that this is accessing the Nth slot of local /// storage. ContextSet(u32), + + /// Intrinsic used to implement the `thread.index` component model builtin. + ThreadIndex, + + /// Intrinsic used to implement the `thread.new_indirect` component model builtin. + ThreadNewIndirect { + /// The type index for the start function of the thread. + start_func_ty_idx: ComponentTypeIndex, + /// The index of the table that stores the start function. + start_func_table_idx: TableIndex, + }, + + /// Intrinsic used to implement the `thread.switch-to` component model builtin. + ThreadSwitchTo { + /// If `true`, indicates the caller instance maybe reentered. + cancellable: bool, + }, + + /// Intrinsic used to implement the `thread.suspend` component model builtin. + ThreadSuspend { + /// If `true`, indicates the caller instance maybe reentered. + cancellable: bool, + }, + + /// Intrinsic used to implement the `thread.resume-later` component model builtin. + ThreadResumeLater, + + /// Intrinsic used to implement the `thread.yield-to` component model builtin. + ThreadYieldTo { + /// If `true`, indicates the caller instance maybe reentered. + cancellable: bool, + }, } impl Trampoline { @@ -1069,7 +1105,7 @@ impl Trampoline { WaitableSetPoll { .. } => format!("waitable-set-poll"), WaitableSetDrop { .. } => format!("waitable-set-drop"), WaitableJoin { .. } => format!("waitable-join"), - Yield { .. } => format!("yield"), + ThreadYield { .. } => format!("thread-yield"), SubtaskDrop { .. } => format!("subtask-drop"), SubtaskCancel { .. } => format!("subtask-cancel"), StreamNew { .. } => format!("stream-new"), @@ -1101,6 +1137,12 @@ impl Trampoline { ErrorContextTransfer => format!("error-context-transfer"), ContextGet(_) => format!("context-get"), ContextSet(_) => format!("context-set"), + ThreadIndex => format!("thread-index"), + ThreadNewIndirect { .. } => format!("thread-new-indirect"), + ThreadSwitchTo { .. } => format!("thread-switch-to"), + ThreadSuspend { .. } => format!("thread-suspend"), + ThreadResumeLater => format!("thread-resume-later"), + ThreadYieldTo { .. } => format!("thread-yield-to"), } } } diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 3ff2bb4e70c2..363af6307543 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -205,9 +205,11 @@ enum LocalInitializer<'data> { }, WaitableSetWait { options: LocalCanonicalOptions, + cancellable: bool, }, WaitableSetPoll { options: LocalCanonicalOptions, + cancellable: bool, }, WaitableSetDrop { func: ModuleInternedTypeIndex, @@ -215,9 +217,9 @@ enum LocalInitializer<'data> { WaitableJoin { func: ModuleInternedTypeIndex, }, - Yield { + ThreadYield { func: ModuleInternedTypeIndex, - async_: bool, + cancellable: bool, }, SubtaskDrop { func: ModuleInternedTypeIndex, @@ -303,6 +305,29 @@ enum LocalInitializer<'data> { func: ModuleInternedTypeIndex, i: u32, }, + ThreadIndex { + func: ModuleInternedTypeIndex, + }, + ThreadNewIndirect { + func: ModuleInternedTypeIndex, + start_func_ty: ComponentTypeIndex, + start_func_table_idx: TableIndex, + }, + ThreadSwitchTo { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, + ThreadSuspend { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, + ThreadResumeLater { + func: ModuleInternedTypeIndex, + }, + ThreadYieldTo { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, // core wasm modules ModuleStatic(StaticModuleIndex, ComponentCoreModuleTypeId), @@ -839,13 +864,16 @@ impl<'a, 'data> Translator<'a, 'data> { core_func_index += 1; LocalInitializer::WaitableSetNew { func } } - wasmparser::CanonicalFunction::WaitableSetWait { async_, memory } => { + wasmparser::CanonicalFunction::WaitableSetWait { + cancellable, + memory, + } => { let core_type = self.core_func_signature(core_func_index)?; core_func_index += 1; LocalInitializer::WaitableSetWait { options: LocalCanonicalOptions { core_type, - async_, + async_: false, data_model: LocalDataModel::LinearMemory { memory: Some(MemoryIndex::from_u32(memory)), realloc: None, @@ -854,15 +882,19 @@ impl<'a, 'data> Translator<'a, 'data> { callback: None, string_encoding: StringEncoding::Utf8, }, + cancellable, } } - wasmparser::CanonicalFunction::WaitableSetPoll { async_, memory } => { + wasmparser::CanonicalFunction::WaitableSetPoll { + cancellable, + memory, + } => { let core_type = self.core_func_signature(core_func_index)?; core_func_index += 1; LocalInitializer::WaitableSetPoll { options: LocalCanonicalOptions { core_type, - async_, + async_: false, data_model: LocalDataModel::LinearMemory { memory: Some(MemoryIndex::from_u32(memory)), realloc: None, @@ -871,6 +903,7 @@ impl<'a, 'data> Translator<'a, 'data> { callback: None, string_encoding: StringEncoding::Utf8, }, + cancellable, } } wasmparser::CanonicalFunction::WaitableSetDrop => { @@ -883,10 +916,10 @@ impl<'a, 'data> Translator<'a, 'data> { core_func_index += 1; LocalInitializer::WaitableJoin { func } } - wasmparser::CanonicalFunction::Yield { async_ } => { + wasmparser::CanonicalFunction::ThreadYield { cancellable } => { let func = self.core_func_signature(core_func_index)?; core_func_index += 1; - LocalInitializer::Yield { func, async_ } + LocalInitializer::ThreadYield { func, cancellable } } wasmparser::CanonicalFunction::SubtaskDrop => { let func = self.core_func_signature(core_func_index)?; @@ -1063,6 +1096,43 @@ impl<'a, 'data> Translator<'a, 'data> { core_func_index += 1; LocalInitializer::ContextSet { i, func } } + wasmparser::CanonicalFunction::ThreadIndex => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadIndex { func } + } + wasmparser::CanonicalFunction::ThreadNewIndirect { + func_ty_index, + table_index, + } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadNewIndirect { + func, + start_func_ty: ComponentTypeIndex::from_u32(func_ty_index), + start_func_table_idx: TableIndex::from_u32(table_index), + } + } + wasmparser::CanonicalFunction::ThreadSwitchTo { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadSwitchTo { func, cancellable } + } + wasmparser::CanonicalFunction::ThreadSuspend { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadSuspend { func, cancellable } + } + wasmparser::CanonicalFunction::ThreadResumeLater => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadResumeLater { func } + } + wasmparser::CanonicalFunction::ThreadYieldTo { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadYieldTo { func, cancellable } + } }; self.result.initializers.push(init); } diff --git a/crates/environ/src/component/translate/adapt.rs b/crates/environ/src/component/translate/adapt.rs index c66fa47d19a1..c6dde50a4028 100644 --- a/crates/environ/src/component/translate/adapt.rs +++ b/crates/environ/src/component/translate/adapt.rs @@ -342,6 +342,7 @@ fn fact_import_to_core_def( fact::Import::ErrorContextTransfer => { simple_intrinsic(dfg::Trampoline::ErrorContextTransfer) } + fact::Import::ThreadIndex => simple_intrinsic(dfg::Trampoline::ThreadIndex), } } diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 95de5bcc305f..0ac69306efd4 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -714,24 +714,36 @@ impl<'a> Inliner<'a> { )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } - WaitableSetWait { options } => { + WaitableSetWait { + options, + cancellable, + } => { let func = options.core_type; let options = self.adapter_options(frame, types, options); let options = self.canonical_options(options); - let index = self - .result - .trampolines - .push((func, dfg::Trampoline::WaitableSetWait { options })); + let index = self.result.trampolines.push(( + func, + dfg::Trampoline::WaitableSetWait { + options, + cancellable: *cancellable, + }, + )); frame.funcs.push((func, dfg::CoreDef::Trampoline(index))); } - WaitableSetPoll { options } => { + WaitableSetPoll { + options, + cancellable, + } => { let func = options.core_type; let options = self.adapter_options(frame, types, options); let options = self.canonical_options(options); - let index = self - .result - .trampolines - .push((func, dfg::Trampoline::WaitableSetPoll { options })); + let index = self.result.trampolines.push(( + func, + dfg::Trampoline::WaitableSetPoll { + options, + cancellable: *cancellable, + }, + )); frame.funcs.push((func, dfg::CoreDef::Trampoline(index))); } WaitableSetDrop { func } => { @@ -752,11 +764,13 @@ impl<'a> Inliner<'a> { )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } - Yield { func, async_ } => { - let index = self - .result - .trampolines - .push((*func, dfg::Trampoline::Yield { async_: *async_ })); + ThreadYield { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadYield { + cancellable: *cancellable, + }, + )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } SubtaskDrop { func } => { @@ -1014,7 +1028,62 @@ impl<'a> Inliner<'a> { .push((*func, dfg::Trampoline::ContextSet(*i))); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } - + ThreadIndex { func } => { + let index = self + .result + .trampolines + .push((*func, dfg::Trampoline::ThreadIndex)); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadNewIndirect { + func, + start_func_table_idx, + start_func_ty, + } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadNewIndirect { + start_func_ty_idx: *start_func_ty, + start_func_table_idx: *start_func_table_idx, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadSwitchTo { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadSwitchTo { + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadSuspend { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadSuspend { + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadResumeLater { func } => { + let index = self + .result + .trampolines + .push((*func, dfg::Trampoline::ThreadResumeLater)); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadYieldTo { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadYieldTo { + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + // A static module is being defined within this component, so it's ModuleStatic(idx, ty) => { frame.modules.push(ModuleDef::Static(*idx, *ty)); } diff --git a/crates/environ/src/fact.rs b/crates/environ/src/fact.rs index 35643649edf6..bfded1b59749 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -868,6 +868,8 @@ pub enum Import { /// An intrinisic used by FACT-generated modules to (partially or entirely) transfer /// ownership of an `error-context`. ErrorContextTransfer, + /// Get the index of the current thread. + ThreadIndex, } impl Options { diff --git a/crates/environ/src/trap_encoding.rs b/crates/environ/src/trap_encoding.rs index 169a27212d7a..38a097412b07 100644 --- a/crates/environ/src/trap_encoding.rs +++ b/crates/environ/src/trap_encoding.rs @@ -89,6 +89,11 @@ pub enum Trap { /// triggering a trap instead. CannotEnterComponent, + /// When the `component-model` feature is enabled this trap represents a + /// scenario where one component tried to call an import at a time when it + /// was not legal to do so. + CannotLeaveComponent, + /// Async-lifted export failed to produce a result by calling `task.return` /// before returning `STATUS_DONE` and/or after all host tasks completed. NoAsyncResult, @@ -178,6 +183,7 @@ impl fmt::Display for Trap { AllocationTooLarge => "allocation size too large", CastFailure => "cast failure", CannotEnterComponent => "cannot enter component instance", + CannotLeaveComponent => "cannot leave component instance", NoAsyncResult => "async-lifted export failed to produce a result", UnhandledTag => "unhandled tag", ContinuationAlreadyConsumed => "continuation already consumed", diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index 92dbf82a0644..6224e822676d 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -135,8 +135,7 @@ impl Config { extended_const, wide_arithmetic, component_model_async, - component_model_async_builtins, - component_model_async_stackful, + component_model_threading component_model_error_context, component_model_gc, simd, @@ -155,10 +154,8 @@ impl Config { self.module_config.function_references_enabled = function_references.or(gc).unwrap_or(false); self.module_config.component_model_async = component_model_async.unwrap_or(false); - self.module_config.component_model_async_builtins = - component_model_async_builtins.unwrap_or(false); - self.module_config.component_model_async_stackful = - component_model_async_stackful.unwrap_or(false); + self.module_config.component_model_threading = + component_model_threading.unwrap_or(false); self.module_config.component_model_error_context = component_model_error_context.unwrap_or(false); self.module_config.component_model_gc = component_model_gc.unwrap_or(false); @@ -279,10 +276,8 @@ impl Config { cfg.wasm.async_stack_zeroing = Some(self.wasmtime.async_stack_zeroing); cfg.wasm.bulk_memory = Some(true); cfg.wasm.component_model_async = Some(self.module_config.component_model_async); - cfg.wasm.component_model_async_builtins = - Some(self.module_config.component_model_async_builtins); - cfg.wasm.component_model_async_stackful = - Some(self.module_config.component_model_async_stackful); + cfg.wasm.component_model_threading = + Some(self.module_config.component_model_threading); cfg.wasm.component_model_error_context = Some(self.module_config.component_model_error_context); cfg.wasm.component_model_gc = Some(self.module_config.component_model_gc); diff --git a/crates/fuzzing/src/generators/module.rs b/crates/fuzzing/src/generators/module.rs index 7b65c92c4c31..0606c8e5a47a 100644 --- a/crates/fuzzing/src/generators/module.rs +++ b/crates/fuzzing/src/generators/module.rs @@ -17,8 +17,7 @@ pub struct ModuleConfig { // config-to-`wasmtime::Config` translation. pub function_references_enabled: bool, pub component_model_async: bool, - pub component_model_async_builtins: bool, - pub component_model_async_stackful: bool, + pub component_model_threading: bool, pub component_model_error_context: bool, pub component_model_gc: bool, pub legacy_exceptions: bool, @@ -68,8 +67,7 @@ impl<'a> Arbitrary<'a> for ModuleConfig { Ok(ModuleConfig { component_model_async: false, - component_model_async_builtins: false, - component_model_async_stackful: false, + component_model_threading: false, component_model_error_context: false, component_model_gc: false, legacy_exceptions: false, diff --git a/crates/misc/component-async-tests/tests/scenario/coop_threads.rs b/crates/misc/component-async-tests/tests/scenario/coop_threads.rs new file mode 100644 index 000000000000..d9ce940b2482 --- /dev/null +++ b/crates/misc/component-async-tests/tests/scenario/coop_threads.rs @@ -0,0 +1,19 @@ +use anyhow::Result; + +use super::util::test_run; + +// No-op function; we only test this by composing it in `async_coop_threads_caller` +#[allow( + dead_code, + reason = "here only to make the `assert_test_exists` macro happy" +)] +pub fn async_coop_threads_callee() {} + +#[tokio::test] +pub async fn async_coop_threads_caller() -> Result<()> { + test_run(&[ + test_programs_artifacts::ASYNC_COOP_THREADS_CALLER_COMPONENT, + test_programs_artifacts::ASYNC_COOP_THREADS_CALLEE_COMPONENT, + ]) + .await +} diff --git a/crates/misc/component-async-tests/tests/scenario/mod.rs b/crates/misc/component-async-tests/tests/scenario/mod.rs index e3481c18bf58..aab3fd3f69b2 100644 --- a/crates/misc/component-async-tests/tests/scenario/mod.rs +++ b/crates/misc/component-async-tests/tests/scenario/mod.rs @@ -2,6 +2,7 @@ mod util; pub mod backpressure; pub mod borrowing; +pub mod coop_threads; pub mod error_context; pub mod post_return; pub mod read_resource_stream; diff --git a/crates/misc/component-async-tests/tests/scenario/util.rs b/crates/misc/component-async-tests/tests/scenario/util.rs index b972fa810028..356f535ecadc 100644 --- a/crates/misc/component-async-tests/tests/scenario/util.rs +++ b/crates/misc/component-async-tests/tests/scenario/util.rs @@ -34,8 +34,7 @@ pub fn config() -> Config { } config.wasm_component_model(true); config.wasm_component_model_async(true); - config.wasm_component_model_async_builtins(true); - config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config.async_support(true); config diff --git a/crates/misc/component-async-tests/tests/test_all.rs b/crates/misc/component-async-tests/tests/test_all.rs index 0553254d34a5..7946de0bdb3d 100644 --- a/crates/misc/component-async-tests/tests/test_all.rs +++ b/crates/misc/component-async-tests/tests/test_all.rs @@ -13,6 +13,7 @@ mod scenario; use scenario::backpressure::{async_backpressure_callee, async_backpressure_caller}; use scenario::borrowing::{async_borrowing_callee, async_borrowing_caller}; +use scenario::coop_threads::{async_coop_threads_callee, async_coop_threads_caller}; use scenario::error_context::{ async_error_context, async_error_context_callee, async_error_context_caller, }; diff --git a/crates/misc/component-async-tests/wit/test.wit b/crates/misc/component-async-tests/wit/test.wit index f074e9bc3167..f979cf4920cc 100644 --- a/crates/misc/component-async-tests/wit/test.wit +++ b/crates/misc/component-async-tests/wit/test.wit @@ -307,3 +307,16 @@ world intertask-communication { import intertask; export run; } + +interface coop { + get-index: async func() -> u32; +} + +world coop-threads-callee { + export coop; +} + +world coop-threads-caller { + import coop; + export run; +} \ No newline at end of file diff --git a/crates/test-programs/src/async_.rs b/crates/test-programs/src/async_.rs index 1ec58158d27d..96fe914883d1 100644 --- a/crates/test-programs/src/async_.rs +++ b/crates/test-programs/src/async_.rs @@ -114,6 +114,17 @@ pub unsafe fn context_set(_: u32) { unreachable!() } +#[cfg(target_arch = "wasm32")] +#[link(wasm_import_module = "$root")] +unsafe extern "C" { + #[link_name = "[thread-index]"] + pub fn thread_index() -> u32; +} +#[cfg(not(target_arch = "wasm32"))] +pub unsafe fn thread_index() -> u32 { + unreachable!() +} + #[cfg(target_arch = "wasm32")] #[link(wasm_import_module = "[export]$root")] unsafe extern "C" { diff --git a/crates/test-programs/src/bin/async_coop_threads_callee.rs b/crates/test-programs/src/bin/async_coop_threads_callee.rs new file mode 100644 index 000000000000..0b1c8b871f34 --- /dev/null +++ b/crates/test-programs/src/bin/async_coop_threads_callee.rs @@ -0,0 +1,25 @@ +mod bindings { + wit_bindgen::generate!({ + path: "../misc/component-async-tests/wit", + world: "coop-threads-callee", + }); + + use super::Component; + export!(Component); +} + +use { + bindings::exports::local::local::coop::Guest as CoopThreads, + test_programs::async_::thread_index, +}; + +struct Component; + +impl CoopThreads for Component { + async fn get_index() -> u32 { + unsafe { thread_index() } + } +} + +// Unused function; required since this file is built as a `bin`: +fn main() {} diff --git a/crates/test-programs/src/bin/async_coop_threads_caller.rs b/crates/test-programs/src/bin/async_coop_threads_caller.rs new file mode 100644 index 000000000000..12e00b1cca56 --- /dev/null +++ b/crates/test-programs/src/bin/async_coop_threads_caller.rs @@ -0,0 +1,25 @@ +mod bindings { + wit_bindgen::generate!({ + path: "../misc/component-async-tests/wit", + world: "coop-threads-caller", + }); + + use super::Component; + export!(Component); +} + +use { + crate::bindings::local::local::coop::get_index, + bindings::exports::local::local::run::Guest as Run, +}; + +struct Component; + +impl Run for Component { + async fn run() { + assert_eq!(get_index().await, 10) + } +} + +// Unused function; required since this file is built as a `bin`: +fn main() {} diff --git a/crates/test-util/src/wasmtime_wast.rs b/crates/test-util/src/wasmtime_wast.rs index feec31e2146e..d6e82c057305 100644 --- a/crates/test-util/src/wasmtime_wast.rs +++ b/crates/test-util/src/wasmtime_wast.rs @@ -38,8 +38,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { extended_const, wide_arithmetic, component_model_async, - component_model_async_builtins, - component_model_async_stackful, + component_model_threading, component_model_error_context, component_model_gc, nan_canonicalization, @@ -65,8 +64,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { let extended_const = extended_const.unwrap_or(false); let wide_arithmetic = wide_arithmetic.unwrap_or(false); let component_model_async = component_model_async.unwrap_or(false); - let component_model_async_builtins = component_model_async_builtins.unwrap_or(false); - let component_model_async_stackful = component_model_async_stackful.unwrap_or(false); + let component_model_threading = component_model_threading.unwrap_or(false); let component_model_error_context = component_model_error_context.unwrap_or(false); let component_model_gc = component_model_gc.unwrap_or(false); let nan_canonicalization = nan_canonicalization.unwrap_or(false); @@ -100,8 +98,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { .wasm_extended_const(extended_const) .wasm_wide_arithmetic(wide_arithmetic) .wasm_component_model_async(component_model_async) - .wasm_component_model_async_builtins(component_model_async_builtins) - .wasm_component_model_async_stackful(component_model_async_stackful) + .wasm_component_model_threading(component_model_threading) .wasm_component_model_error_context(component_model_error_context) .wasm_component_model_gc(component_model_gc) .wasm_exceptions(exceptions) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index c719843eaa1e..d71df855df1c 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -193,8 +193,7 @@ macro_rules! foreach_config_option { hogs_memory nan_canonicalization component_model_async - component_model_async_builtins - component_model_async_stackful + component_model_threading component_model_error_context component_model_gc simd diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 693865c4e9ad..21673b6fe2e5 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1137,28 +1137,16 @@ impl Config { self } - /// This corresponds to the ๐Ÿš emoji in the component model specification. + /// This corresponds to the ๐Ÿงต emoji in the component model specification. /// /// Please note that Wasmtime's support for this feature is _very_ /// incomplete. /// /// [proposal]: - /// https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md - #[cfg(feature = "component-model-async")] - pub fn wasm_component_model_async_builtins(&mut self, enable: bool) -> &mut Self { - self.wasm_feature(WasmFeatures::CM_ASYNC_BUILTINS, enable); - self - } - - /// This corresponds to the ๐ŸšŸ emoji in the component model specification. - /// - /// Please note that Wasmtime's support for this feature is _very_ - /// incomplete. - /// - /// [proposal]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md + /// https://github.com/WebAssembly/component-model/pull/557 #[cfg(feature = "component-model-async")] - pub fn wasm_component_model_async_stackful(&mut self, enable: bool) -> &mut Self { - self.wasm_feature(WasmFeatures::CM_ASYNC_STACKFUL, enable); + pub fn wasm_component_model_threading(&mut self, enable: bool) -> &mut Self { + self.wasm_feature(WasmFeatures::CM_THREADING, enable); self } diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 8cffc1c5be7e..b746f4d231fd 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -50,21 +50,26 @@ use crate::component::func::{self, Func, Options}; use crate::component::{ Component, ComponentInstanceId, HasData, HasSelf, Instance, Resource, ResourceTable, - ResourceTableError, + ResourceTableError, store, }; use crate::fiber::{self, StoreFiber, StoreFiberYield}; use crate::store::{StoreInner, StoreOpaque, StoreToken}; use crate::vm::component::{ CallContext, ComponentInstance, InstanceFlags, ResourceTables, TransmitLocalState, }; -use crate::vm::{AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; -use crate::{AsContext, AsContextMut, StoreContext, StoreContextMut, ValRaw}; +use crate::vm::{self, AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; +use crate::{ + AsContext, AsContextMut, FuncType, StoreContext, StoreContextMut, Table, ValRaw, ValType, + runtime, +}; use anyhow::{Context as _, Result, anyhow, bail}; use error_contexts::GlobalErrorContextRefCount; use futures::channel::oneshot; use futures::future::{self, Either, FutureExt}; use futures::stream::{FuturesUnordered, StreamExt}; use futures_and_streams::{FlatAbi, ReturnCode, TransmitHandle, TransmitIndex}; +use log::trace; +use mach2::vm_sync::vm_sync_t; use std::any::Any; use std::borrow::ToOwned; use std::boxed::Box; @@ -79,15 +84,17 @@ use std::pin::{Pin, pin}; use std::ptr::{self, NonNull}; use std::slice; use std::task::{Context, Poll, Waker}; +use std::thread::current; use std::vec::Vec; use table::{TableDebug, TableId}; use wasmtime_environ::component::{ - CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, - OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, - RuntimeComponentInstanceIndex, StringEncoding, TypeComponentGlobalErrorContextTableIndex, - TypeComponentLocalErrorContextTableIndex, TypeFutureTableIndex, TypeStreamTableIndex, - TypeTupleIndex, + CanonicalOptions, CanonicalOptionsDataModel, CoreDef, ExportIndex, MAX_FLAT_PARAMS, + MAX_FLAT_RESULTS, OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, + RuntimeComponentInstanceIndex, RuntimeTableIndex, StringEncoding, + TypeComponentGlobalErrorContextTableIndex, TypeComponentLocalErrorContextTableIndex, + TypeFuncIndex, TypeFutureTableIndex, TypeStreamTableIndex, TypeTupleIndex, }; +use wasmtime_environ::{FuncIndex, TableIndex}; pub use abort::JoinHandle; pub use futures_and_streams::{ @@ -588,7 +595,15 @@ enum SuspendReason { NeedWork, /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. - Yielding { task: TableId }, + Yielding { + task: TableId, + to: Option>, + }, + /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. + ExplicitlySuspending { + task: TableId, + to: Option>, + }, } /// Represents a pending call into guest code for a given guest task. @@ -1379,7 +1394,11 @@ impl Instance { /// Resume the specified fiber, giving it exclusive access to the specified /// store. - async fn resume_fiber(self, store: &mut StoreOpaque, fiber: StoreFiber<'static>) -> Result<()> { + async fn resume_fiber( + self, + store: &mut StoreContextMut, + fiber: StoreFiber<'static>, + ) -> Result<()> { let old_task = self.concurrent_state_mut(store).guest_task; log::trace!("resume_fiber: save current task {old_task:?}"); @@ -1392,16 +1411,24 @@ impl Instance { if let Some(mut fiber) = fiber { // See the `SuspendReason` documentation for what each case means. - match state.suspend_reason.take().unwrap() { + let next = match state.suspend_reason.take().unwrap() { SuspendReason::NeedWork => { if state.worker.is_none() { state.worker = Some(fiber); } else { fiber.dispose(store); } + None } - SuspendReason::Yielding { .. } => { + SuspendReason::Yielding { to, .. } => { state.push_low_priority(WorkItem::ResumeFiber(fiber)); + to + } + SuspendReason::ExplicitlySuspending { to, task } => { + state + .unscheduled_guest_tasks + .insert(task, UnscheduledTaskState::Started(fiber)); + to } SuspendReason::Waiting { set, task } => { let old = state @@ -1409,7 +1436,18 @@ impl Instance { .waiting .insert(task, WaitMode::Fiber(fiber)); assert!(old.is_none()); + None } + }; + if let Some(next) = next { + self.handle_work_item( + store.as_context_mut(), + WorkItem::GuestCall(GuestCall { + task: next, + kind: GuestCallKind::DeliverEvent { set: None }, + }), + ) + .await?; } } @@ -1513,7 +1551,9 @@ impl Instance { // pop the call context which manages resource borrows before suspending // and then push it again once we've resumed. let task = match &reason { - SuspendReason::Yielding { task } | SuspendReason::Waiting { task, .. } => Some(*task), + SuspendReason::Yielding { task, .. } + | SuspendReason::Waiting { task, .. } + | SuspendReason::ExplicitlySuspending { task, .. } => Some(*task), SuspendReason::NeedWork => None, }; @@ -2675,18 +2715,18 @@ impl Instance { self, store: &mut dyn VMStore, options: OptionsIndex, + cancellable: bool, set: u32, payload: u32, ) -> Result { let opts = self.concurrent_state_mut(store).options(options); - let async_ = opts.async_; let caller_instance = opts.instance; let rep = self.id().get_mut(store).guest_tables().0[caller_instance].waitable_set_rep(set)?; self.waitable_check( store, - async_, + cancellable, WaitableCheck::Wait(WaitableCheckParams { set: TableId::new(rep), options, @@ -2700,18 +2740,18 @@ impl Instance { self, store: &mut dyn VMStore, options: OptionsIndex, + cancellable: bool, set: u32, payload: u32, ) -> Result { let opts = self.concurrent_state_mut(store).options(options); - let async_ = opts.async_; let caller_instance = opts.instance; let rep = self.id().get_mut(store).guest_tables().0[caller_instance].waitable_set_rep(set)?; self.waitable_check( store, - async_, + cancellable, WaitableCheck::Poll(WaitableCheckParams { set: TableId::new(rep), options, @@ -2720,32 +2760,215 @@ impl Instance { ) } - /// Implements the `yield` intrinsic. - pub(crate) fn yield_(self, store: &mut dyn VMStore, async_: bool) -> Result { - self.waitable_check(store, async_, WaitableCheck::Yield) - .map(|_| { - let state = self.concurrent_state_mut(store); - let task = state.guest_task.unwrap(); - if let Some(event) = state.get_mut(task).unwrap().event.take() { - assert!(matches!(event, Event::Cancelled)); - true - } else { - false + /// Implements the `thread.yield` and `thread.yield-to` intrinsics. + pub(crate) fn thread_yield_to( + self, + store: &mut dyn VMStore, + cancellable: bool, + task: Option>, + ) -> Result { + let check = if let Some(task) = task { + // TODO: validation + WaitableCheck::YieldTo { task } + } else { + WaitableCheck::Yield + }; + self.waitable_check(store, cancellable, check).map(|_| { + let state = self.concurrent_state_mut(store); + let task = state.guest_task.unwrap(); + if let Some(event) = state.get_mut(task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + true + } else { + false + } + }) + } + + pub(crate) fn thread_switch_to( + self, + store: &mut dyn VMStore, + _cancellable: bool, + task: TableId, + ) -> Result<()> { + let state = self.concurrent_state_mut(store); + let current_task = state.guest_task.unwrap(); + self.suspend( + store, + SuspendReason::ExplicitlySuspending { + task: current_task, + to: Some(task), + }, + ) + } + + unsafe fn read_funcref_from_table( + self, + store: &mut dyn VMStore, + table_idx: RuntimeTableIndex, + func_idx: u64, + ) -> Result> { + let table_import = self.id().get_mut(store).runtime_table(table_idx); + let vmctx = table_import.vmctx.as_non_null(); + // SAFETY: `vmctx` is a valid pointer, and the `Instance` is + // located immediately before the `vmctx`. See `vm::Instance::sibling_vmctx`, + // which we can't call here as we don't have a `vm::Instance` to bind the lifetime to. + let mut instance_ptr = unsafe { + vmctx + .byte_sub(mem::size_of::()) + .cast::() + }; + // SAFETY: We just constructed `instance_ptr` from a valid pointer. This pointer won't leave + // this call, so we don't need a lifetime to bind it to. + let instance = unsafe { Pin::new_unchecked(instance_ptr.as_mut()) }; + let table = instance.get_defined_table_with_lazy_init(table_import.index, [func_idx]); + match table.get_func(func_idx) { + // SAFETY: The `VMFuncRef` is not null, properly aligned, and points to a valid + // `VMFuncRef` because it came from a `Table` in a `vm::Instance`. + // We clone it here so that we don't need to worry about its lifetime. + Ok(Some(func)) => Ok(func), + Ok(None) => Err(anyhow!("function index {func_idx} out of bounds")), + Err(e) => Err(anyhow!("failed to get function from table: {e}")), + } + } + + /// Implements the `thread.new_indirect` intrinsic. + pub(crate) fn thread_new_indirect( + self, + store: &mut dyn VMStore, + _func_ty_idx: TypeFuncIndex, // currently unused + table_idx: RuntimeTableIndex, + func_idx: FuncIndex, + context: i32, + ) -> Result { + log::trace!("creating new thread"); + + let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); + let funcref = + unsafe { self.read_funcref_from_table(store, table_idx, func_idx.as_u32() as u64) }?; + if unsafe { funcref.as_ref().type_index } != start_func_ty.type_index() { + bail!( + "start function does not match expected type (currently only `(i32) -> ()` is supported)" + ); + } + + let state = self.concurrent_state_mut(store); + let current_task = state.guest_task.unwrap(); + let instance = state.get_mut(current_task)?.instance; + + // TODO check can_leave? + + let memory = state + .get_mut(current_task)? + .lift_result + .as_ref() + .and_then(|lift| lift.memory.map(|v| v.as_ptr())) + .ok_or_else(|| anyhow!("missing memory for thread start function"))?; + let new_task = GuestTask::new( + state, + Box::new(move |store, instance, params| { + if params.len() != 1 { + return Err(anyhow!("expected 1 parameter for thread start function")); } - }) + params[0].write(ValRaw::i32(context)); + Ok(()) + }), + LiftResult { + lift: Box::new(|store, instance, result| { + Ok(Box::new(DummyResult) as Box) + }), + ty: TypeTupleIndex::from_u32(0), + memory: NonNull::new(memory).map(SendSyncPtr::new), + string_encoding: StringEncoding::Utf8, + }, + Caller::Guest { + task: current_task, + instance: instance, + }, + None, + instance, + )?; + + let guest_task = state.push(new_task)?; + state + .unscheduled_guest_tasks + .insert(guest_task, UnscheduledTaskState::NotStarted(funcref)); + + state.get_mut(current_task)?.subtasks.insert(guest_task); + + log::trace!("new thread with index {} created", guest_task.rep()); + + Ok(guest_task.rep()) + } + + pub(crate) fn thread_resume_later( + self, + store: &mut dyn VMStore, + task: TableId, + ) -> Result<()> { + let state = self.concurrent_state_mut(store); + let guest_task = state.get_mut(task)?; + let suspended_task = state.unscheduled_guest_tasks.get_mut(&task); + if !suspended_task.is_some() { + bail!("can only resume a thread which is currently suspended"); + } + + match suspended_task.unwrap() { + UnscheduledTaskState::NotStarted(callee) => { + log::trace!("resuming thread {task:?} which was not started"); + if !state.may_enter(task) { + bail!(crate::Trap::CannotEnterComponent); + } + unsafe { + self.start_call( + StoreContextMut(self), + ptr::null_mut(), + ptr::null_mut(), + callee.as_mut(), + 1, + 0, + 0, //TODO maybe async + None, + ); + } + } + UnscheduledTaskState::Started(fiber) => { + log::trace!("resuming thread {task:?} which was suspended"); + self.resume_fiber(store, fiber); + } + } + + Ok(()) + } + + pub(crate) fn thread_suspend(self, store: &mut dyn VMStore, cancellable: bool) -> Result<()> { + if cancellable { + bail!("todo: cancellable `thread.suspend` not implemented yet"); + } + + let guest_task = self.concurrent_state_mut(store).guest_task.unwrap(); + self.suspend( + store, + SuspendReason::ExplicitlySuspending { + task: guest_task, + to: None, + }, + )?; + + Ok(()) } /// Helper function for the `waitable-set.wait`, `waitable-set.poll`, and - /// `yield` intrinsics. + /// `thread.yield` intrinsics. fn waitable_check( self, store: &mut dyn VMStore, - async_: bool, + cancellable: bool, check: WaitableCheck, ) -> Result { - if async_ { + if cancellable { bail!( - "todo: async `waitable-set.wait`, `waitable-set.poll`, and `yield` not yet implemented" + "todo: cancellable `waitable-set.wait`, `waitable-set.poll`, and `thread.yield` not yet implemented" ); } @@ -2755,10 +2978,17 @@ impl Instance { WaitableCheck::Wait(params) => (true, Some(params.set)), WaitableCheck::Poll(params) => (false, Some(params.set)), WaitableCheck::Yield => (false, None), + WaitableCheck::YieldTo { task } => (false, None), }; // First, suspend this fiber, allowing any other tasks to run. - self.suspend(store, SuspendReason::Yielding { task: guest_task })?; + self.suspend( + store, + SuspendReason::Yielding { + task: guest_task, + to: None, + }, + )?; log::trace!("waitable check for {guest_task:?}; set {set:?}"); @@ -2828,6 +3058,7 @@ impl Instance { Ok(ordinal) } WaitableCheck::Yield => Ok(0), + WaitableCheck::YieldTo { .. } => Ok(0), }; result @@ -2919,7 +3150,13 @@ impl Instance { }; concurrent_state.push_high_priority(item); - self.suspend(store, SuspendReason::Yielding { task: caller })?; + self.suspend( + store, + SuspendReason::Yielding { + task: caller, + to: None, + }, + )?; } let concurrent_state = self.concurrent_state_mut(store); @@ -3936,11 +4173,19 @@ struct InstanceState { pending: BTreeMap, GuestCallKind>, } +enum UnscheduledTaskState { + Started(StoreFiber<'static>), + NotStarted(NonNull), +} + /// Represents the Component Model Async state of a top-level component instance /// (i.e. a `super::ComponentInstance`). pub struct ConcurrentState { /// The currently running guest task, if any. guest_task: Option>, + + unscheduled_guest_tasks: BTreeMap, UnscheduledTaskState>, + /// The set of pending host and background tasks, if any. /// /// See `ComponentInstance::poll_until` for where we temporarily take this @@ -3956,7 +4201,7 @@ pub struct ConcurrentState { instance_states: HashMap, /// The "high priority" work queue for this instance's event loop. high_priority: Vec, - /// The "high priority" work queue for this instance's event loop. + /// The "low priority" work queue for this instance's event loop. low_priority: Vec, /// A place to stash the reason a fiber is suspending so that the code which /// resumed it will know under what conditions the fiber should be resumed @@ -3993,6 +4238,7 @@ impl ConcurrentState { pub(crate) fn new(component: &Component) -> Self { Self { guest_task: None, + unscheduled_guest_tasks: BTreeMap::new(), table: AlwaysMut::new(ResourceTable::new()), futures: AlwaysMut::new(Some(FuturesUnordered::new())), instance_states: HashMap::new(), @@ -4229,6 +4475,11 @@ impl ConcurrentState { Ok(()) } + /// Implements the `thread.index` intrinsic. + pub(crate) fn thread_index(&self) -> Result { + Ok(self.guest_task.unwrap().rep()) + } + fn options(&self, options: OptionsIndex) -> &CanonicalOptions { &self.component.env_component().options[options] } @@ -4319,6 +4570,7 @@ enum WaitableCheck { Wait(WaitableCheckParams), Poll(WaitableCheckParams), Yield, + YieldTo { task: TableId }, } /// Represents a guest task called from the host, prepared using `prepare_call`. diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 30fd688bc825..56bca2e85c08 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -712,10 +712,17 @@ fn waitable_set_wait( store: &mut dyn VMStore, instance: Instance, options: u32, + cancellable: u8, set: u32, payload: u32, ) -> Result { - instance.waitable_set_wait(store, OptionsIndex::from_u32(options), set, payload) + instance.waitable_set_wait( + store, + OptionsIndex::from_u32(options), + cancellable != 0, + set, + payload, + ) } #[cfg(feature = "component-model-async")] @@ -723,10 +730,17 @@ fn waitable_set_poll( store: &mut dyn VMStore, instance: Instance, options: u32, + cancellable: u8, set: u32, payload: u32, ) -> Result { - instance.waitable_set_poll(store, OptionsIndex::from_u32(options), set, payload) + instance.waitable_set_poll( + store, + OptionsIndex::from_u32(options), + cancellable != 0, + set, + payload, + ) } #[cfg(feature = "component-model-async")] @@ -758,8 +772,8 @@ fn waitable_join( } #[cfg(feature = "component-model-async")] -fn yield_(store: &mut dyn VMStore, instance: Instance, async_: u8) -> Result { - instance.yield_(store, async_ != 0) +fn thread_yield(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { + instance.thread_yield(store, cancellable != 0) } #[cfg(feature = "component-model-async")] @@ -1232,3 +1246,56 @@ fn context_get(store: &mut dyn VMStore, instance: Instance, slot: u32) -> Result fn context_set(store: &mut dyn VMStore, instance: Instance, slot: u32, val: u32) -> Result<()> { instance.concurrent_state_mut(store).context_set(slot, val) } + +#[cfg(feature = "component-model-async")] +fn thread_index(store: &mut dyn VMStore, instance: Instance) -> Result { + instance.concurrent_state_mut(store).thread_index() +} + +#[cfg(feature = "component-model-async")] +fn thread_new_indirect( + store: &mut dyn VMStore, + instance: Instance, + func_ty_id: u32, + func_table_idx: u32, + func_idx: u32, + context: u32, +) -> Result { + instance.thread_new_indirect( + store, + TypeFuncIndex::from_u32(func_ty_id), + func_table_idx, + func_idx, + context, + ) +} + +#[cfg(feature = "component-model-async")] +fn thread_switch_to( + store: &mut dyn VMStore, + instance: Instance, + cancellable: u8, + thread_idx: u32, +) -> Result { + todo!() +} + +#[cfg(feature = "component-model-async")] +fn thread_suspend(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { + todo!() +} + +#[cfg(feature = "component-model-async")] +fn thread_resume_later(store: &mut dyn VMStore, instance: Instance, thread_idx: u32) -> Result<()> { + todo!() +} + +#[cfg(feature = "component-model-async")] +fn thread_yield_to( + store: &mut dyn VMStore, + instance: Instance, + cancellable: u8, + thread_idx: u32, +) -> Result { + todo!() +} diff --git a/tests/all/async_functions.rs b/tests/all/async_functions.rs index 06a657fd1ef3..3dbeb856cf66 100644 --- a/tests/all/async_functions.rs +++ b/tests/all/async_functions.rs @@ -819,10 +819,10 @@ async fn non_stacky_async_activations() -> Result<()> { &engine, r#" (module $m2 - (import "" "yield" (func $yield)) + (import "" "thread-yield" (func $thread-yield)) (func $run_async (export "run_async") - call $yield + call $thread-yield ) ) "#, @@ -852,7 +852,7 @@ async fn non_stacky_async_activations() -> Result<()> { let mut store2 = Store::new(caller.engine(), ()); let mut linker2 = Linker::<()>::new(caller.engine()); linker2 - .func_wrap_async("", "yield", { + .func_wrap_async("", "thread-yield", { let stacks = stacks.clone(); move |caller, _: ()| { let stacks = stacks.clone(); diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index 18b509b000cb..dee8ed514501 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -112,32 +112,32 @@ async fn resume_separate_thread() -> Result<()> { let component = format!( r#" (component - (import "yield" (func $yield (result (list u8)))) + (import "thread-yield" (func $thread-yield (result (list u8)))) (core module $libc (memory (export "memory") 1) {REALLOC_AND_FREE} ) (core instance $libc (instantiate $libc)) - (core func $yield + (core func $thread-yield (canon lower - (func $yield) + (func $thread-yield) (memory $libc "memory") (realloc (func $libc "realloc")) ) ) (core module $m - (import "" "yield" (func $yield (param i32))) + (import "" "thread-yield" (func $thread-yield (param i32))) (import "libc" "memory" (memory 0)) (func $start i32.const 8 - call $yield + call $thread-yield ) (start $start) ) (core instance (instantiate $m - (with "" (instance (export "yield" (func $yield)))) + (with "" (instance (export "thread-yield" (func $thread-yield)))) (with "libc" (instance $libc)) )) ) @@ -147,7 +147,7 @@ async fn resume_separate_thread() -> Result<()> { let mut linker = Linker::new(&engine); linker .root() - .func_wrap_async("yield", |_: StoreContextMut<()>, _: ()| { + .func_wrap_async("thread-yield", |_: StoreContextMut<()>, _: ()| { Box::new(async { tokio::task::yield_now().await; Ok((vec![1u8, 2u8],)) diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index a74dc929f5c6..a6f2134c00eb 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -1039,7 +1039,7 @@ async fn task_return_string_encoding_mismatch() -> Result<()> { async fn task_return_trap(component: &str, substring: &str) -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); - config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.async_support(true); let engine = &Engine::new(&config)?; let component = Component::new(&engine, component)?; diff --git a/tests/all/pooling_allocator.rs b/tests/all/pooling_allocator.rs index fddd26bf64e8..7f9412a725e3 100644 --- a/tests/all/pooling_allocator.rs +++ b/tests/all/pooling_allocator.rs @@ -920,7 +920,7 @@ async fn total_stacks_limit() -> Result<()> { let mut linker = Linker::new(&engine); linker.func_new_async( "async", - "yield", + "thread-yield", FuncType::new(&engine, [], []), |_caller, _params, _results| { Box::new(async { @@ -934,9 +934,9 @@ async fn total_stacks_limit() -> Result<()> { &engine, r#" (module - (import "async" "yield" (func $yield)) + (import "async" "thread-yield" (func $thread-yield)) (func (export "run") - call $yield + call $thread-yield ) (func $empty) diff --git a/tests/all/pulley.rs b/tests/all/pulley.rs index abc99a4752cc..05dad0f081a8 100644 --- a/tests/all/pulley.rs +++ b/tests/all/pulley.rs @@ -85,8 +85,7 @@ fn provenance_test_config() -> Config { config.memory_guard_size(0); config.signals_based_traps(false); config.wasm_component_model_async(true); - config.wasm_component_model_async_stackful(true); - config.wasm_component_model_async_builtins(true); + config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config } diff --git a/tests/misc_testsuite/component-model-async/fused.wast b/tests/misc_testsuite/component-model-async/fused.wast index 00e8a8d9d3d2..4979b39d1b9d 100644 --- a/tests/misc_testsuite/component-model-async/fused.wast +++ b/tests/misc_testsuite/component-model-async/fused.wast @@ -1,5 +1,5 @@ ;;! component_model_async = true -;;! component_model_async_stackful = true +;;! component_model_threading = true ;;! reference_types = true ;;! gc_types = true ;;! multi_memory = true diff --git a/tests/misc_testsuite/component-model-async/futures.wast b/tests/misc_testsuite/component-model-async/futures.wast index ec5c3457c7d0..8e7ce277acac 100644 --- a/tests/misc_testsuite/component-model-async/futures.wast +++ b/tests/misc_testsuite/component-model-async/futures.wast @@ -1,5 +1,5 @@ ;;! component_model_async = true -;;! component_model_async_builtins = true +;;! component_model_threading = true ;; future.new (component diff --git a/tests/misc_testsuite/component-model-async/lift.wast b/tests/misc_testsuite/component-model-async/lift.wast index f1175b0e89a4..7088ed435b10 100644 --- a/tests/misc_testsuite/component-model-async/lift.wast +++ b/tests/misc_testsuite/component-model-async/lift.wast @@ -1,5 +1,5 @@ ;;! component_model_async = true -;;! component_model_async_stackful = true +;;! component_model_threading = true ;; async lift; no callback (component diff --git a/tests/misc_testsuite/component-model-async/streams.wast b/tests/misc_testsuite/component-model-async/streams.wast index fa0b2e02c0a6..6efadded4993 100644 --- a/tests/misc_testsuite/component-model-async/streams.wast +++ b/tests/misc_testsuite/component-model-async/streams.wast @@ -1,5 +1,5 @@ ;;! component_model_async = true -;;! component_model_async_builtins = true +;;! component_model_threading = true ;; stream.new (component diff --git a/tests/misc_testsuite/component-model-async/task-builtins.wast b/tests/misc_testsuite/component-model-async/task-builtins.wast index 441d4d8394cd..4964cefb7861 100644 --- a/tests/misc_testsuite/component-model-async/task-builtins.wast +++ b/tests/misc_testsuite/component-model-async/task-builtins.wast @@ -1,5 +1,5 @@ ;;! component_model_async = true -;;! component_model_async_stackful = true +;;! component_model_threading = true ;; backpressure.set (component @@ -26,7 +26,7 @@ (core module $m (import "" "waitable-set.wait" (func $waitable-set-wait (param i32 i32) (result i32))) ) - (core func $waitable-set-wait (canon waitable-set.wait async (memory $libc "memory"))) + (core func $waitable-set-wait (canon waitable-set.wait cancellable (memory $libc "memory"))) (core instance $i (instantiate $m (with "" (instance (export "waitable-set.wait" (func $waitable-set-wait)))))) ) @@ -37,17 +37,17 @@ (core module $m (import "" "waitable-set.poll" (func $waitable-set-poll (param i32 i32) (result i32))) ) - (core func $waitable-set-poll (canon waitable-set.poll async (memory $libc "memory"))) + (core func $waitable-set-poll (canon waitable-set.poll cancellable (memory $libc "memory"))) (core instance $i (instantiate $m (with "" (instance (export "waitable-set.poll" (func $waitable-set-poll)))))) ) -;; yield +;; thread.yield (component (core module $m - (import "" "yield" (func $yield (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) ) - (core func $yield (canon yield async)) - (core instance $i (instantiate $m (with "" (instance (export "yield" (func $yield)))))) + (core func $thread-yield (canon thread.yield cancellable)) + (core instance $i (instantiate $m (with "" (instance (export "thread.yield" (func $thread-yield)))))) ) ;; subtask.drop From 275344e191c5e03d758d1a03d8297416dfeeff3b Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 16 Sep 2025 11:52:04 +0100 Subject: [PATCH 02/13] Almost compiling --- crates/environ/src/component.rs | 2 +- .../src/runtime/component/concurrent.rs | 84 +++++++++---------- .../src/runtime/vm/component/libcalls.rs | 11 +-- 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index a7e407f667bb..dae7d37d0fc3 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -196,7 +196,7 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; #[cfg(feature = "component-model-async")] - thread_yield_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u64; + thread_yield_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u32; trap(vmctx: vmctx, code: u8) -> bool; diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index b746f4d231fd..855a5f14b195 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -50,7 +50,7 @@ use crate::component::func::{self, Func, Options}; use crate::component::{ Component, ComponentInstanceId, HasData, HasSelf, Instance, Resource, ResourceTable, - ResourceTableError, store, + ResourceTableError, }; use crate::fiber::{self, StoreFiber, StoreFiberYield}; use crate::store::{StoreInner, StoreOpaque, StoreToken}; @@ -58,18 +58,13 @@ use crate::vm::component::{ CallContext, ComponentInstance, InstanceFlags, ResourceTables, TransmitLocalState, }; use crate::vm::{self, AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; -use crate::{ - AsContext, AsContextMut, FuncType, StoreContext, StoreContextMut, Table, ValRaw, ValType, - runtime, -}; +use crate::{AsContext, AsContextMut, FuncType, StoreContext, StoreContextMut, ValRaw, ValType}; use anyhow::{Context as _, Result, anyhow, bail}; use error_contexts::GlobalErrorContextRefCount; use futures::channel::oneshot; use futures::future::{self, Either, FutureExt}; use futures::stream::{FuturesUnordered, StreamExt}; use futures_and_streams::{FlatAbi, ReturnCode, TransmitHandle, TransmitIndex}; -use log::trace; -use mach2::vm_sync::vm_sync_t; use std::any::Any; use std::borrow::ToOwned; use std::boxed::Box; @@ -84,17 +79,15 @@ use std::pin::{Pin, pin}; use std::ptr::{self, NonNull}; use std::slice; use std::task::{Context, Poll, Waker}; -use std::thread::current; use std::vec::Vec; use table::{TableDebug, TableId}; use wasmtime_environ::component::{ - CanonicalOptions, CanonicalOptionsDataModel, CoreDef, ExportIndex, MAX_FLAT_PARAMS, - MAX_FLAT_RESULTS, OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, + CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, + OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, RuntimeComponentInstanceIndex, RuntimeTableIndex, StringEncoding, TypeComponentGlobalErrorContextTableIndex, TypeComponentLocalErrorContextTableIndex, TypeFuncIndex, TypeFutureTableIndex, TypeStreamTableIndex, TypeTupleIndex, }; -use wasmtime_environ::{FuncIndex, TableIndex}; pub use abort::JoinHandle; pub use futures_and_streams::{ @@ -109,7 +102,7 @@ pub(crate) use futures_and_streams::{ mod abort; mod error_contexts; mod futures_and_streams; -mod table; +pub(crate) mod table; pub(crate) mod tls; /// Constant defined in the Component Model spec to indicate that the async @@ -1394,11 +1387,7 @@ impl Instance { /// Resume the specified fiber, giving it exclusive access to the specified /// store. - async fn resume_fiber( - self, - store: &mut StoreContextMut, - fiber: StoreFiber<'static>, - ) -> Result<()> { + async fn resume_fiber(self, store: &mut StoreOpaque, fiber: StoreFiber<'static>) -> Result<()> { let old_task = self.concurrent_state_mut(store).guest_task; log::trace!("resume_fiber: save current task {old_task:?}"); @@ -1439,15 +1428,12 @@ impl Instance { None } }; + if let Some(next) = next { - self.handle_work_item( - store.as_context_mut(), - WorkItem::GuestCall(GuestCall { - task: next, - kind: GuestCallKind::DeliverEvent { set: None }, - }), - ) - .await?; + state.push_high_priority(WorkItem::GuestCall(GuestCall { + task: next, + kind: GuestCallKind::DeliverEvent { set: None }, + })); } } @@ -2838,14 +2824,13 @@ impl Instance { store: &mut dyn VMStore, _func_ty_idx: TypeFuncIndex, // currently unused table_idx: RuntimeTableIndex, - func_idx: FuncIndex, + func_idx: u32, context: i32, ) -> Result { log::trace!("creating new thread"); let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); - let funcref = - unsafe { self.read_funcref_from_table(store, table_idx, func_idx.as_u32() as u64) }?; + let funcref = unsafe { self.read_funcref_from_table(store, table_idx, func_idx as u64) }?; if unsafe { funcref.as_ref().type_index } != start_func_ty.type_index() { bail!( "start function does not match expected type (currently only `(i32) -> ()` is supported)" @@ -2866,7 +2851,7 @@ impl Instance { .ok_or_else(|| anyhow!("missing memory for thread start function"))?; let new_task = GuestTask::new( state, - Box::new(move |store, instance, params| { + Box::new(move |_store, _instance, params| { if params.len() != 1 { return Err(anyhow!("expected 1 parameter for thread start function")); } @@ -2874,7 +2859,7 @@ impl Instance { Ok(()) }), LiftResult { - lift: Box::new(|store, instance, result| { + lift: Box::new(|_store, _instance, _result| { Ok(Box::new(DummyResult) as Box) }), ty: TypeTupleIndex::from_u32(0), @@ -2890,9 +2875,13 @@ impl Instance { )?; let guest_task = state.push(new_task)?; - state - .unscheduled_guest_tasks - .insert(guest_task, UnscheduledTaskState::NotStarted(funcref)); + state.unscheduled_guest_tasks.insert( + guest_task, + UnscheduledTaskState::NotStarted { + table_idx, + func_idx, + }, + ); state.get_mut(current_task)?.subtasks.insert(guest_task); @@ -2906,29 +2895,31 @@ impl Instance { store: &mut dyn VMStore, task: TableId, ) -> Result<()> { - let state = self.concurrent_state_mut(store); - let guest_task = state.get_mut(task)?; - let suspended_task = state.unscheduled_guest_tasks.get_mut(&task); + let suspended_task = self + .concurrent_state_mut(store) + .unscheduled_guest_tasks + .remove(&task); if !suspended_task.is_some() { bail!("can only resume a thread which is currently suspended"); } - match suspended_task.unwrap() { - UnscheduledTaskState::NotStarted(callee) => { + UnscheduledTaskState::NotStarted { + table_idx, + func_idx, + } => { log::trace!("resuming thread {task:?} which was not started"); - if !state.may_enter(task) { - bail!(crate::Trap::CannotEnterComponent); - } + let mut callee = + unsafe { self.read_funcref_from_table(store, table_idx, func_idx as u64)? }; + // TODO check can_enter? unsafe { - self.start_call( - StoreContextMut(self), + store.component_async_store().async_start( + self, ptr::null_mut(), ptr::null_mut(), callee.as_mut(), 1, 0, 0, //TODO maybe async - None, ); } } @@ -4175,7 +4166,10 @@ struct InstanceState { enum UnscheduledTaskState { Started(StoreFiber<'static>), - NotStarted(NonNull), + NotStarted { + table_idx: RuntimeTableIndex, + func_idx: u32, + }, } /// Represents the Component Model Async state of a top-level component instance diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 56bca2e85c08..33f6454bfe41 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -4,6 +4,7 @@ use crate::component::Instance; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; +use crate::runtime::component::concurrent::table::TableId; use crate::runtime::vm::component::{ComponentInstance, VMComponentContext}; use crate::runtime::vm::{HostResultHasUnwindSentinel, VMStore, VmSafe}; use core::cell::Cell; @@ -773,7 +774,7 @@ fn waitable_join( #[cfg(feature = "component-model-async")] fn thread_yield(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { - instance.thread_yield(store, cancellable != 0) + instance.thread_yield_to(store, cancellable != 0, None) } #[cfg(feature = "component-model-async")] @@ -1264,9 +1265,9 @@ fn thread_new_indirect( instance.thread_new_indirect( store, TypeFuncIndex::from_u32(func_ty_id), - func_table_idx, + RuntimeTableIndex::from_u32(func_table_idx), func_idx, - context, + context as i32, ) } @@ -1296,6 +1297,6 @@ fn thread_yield_to( instance: Instance, cancellable: u8, thread_idx: u32, -) -> Result { - todo!() +) -> Result { + instance.thread_yield_to(store, cancellable != 0, Some(TableId::new(thread_idx))) } From 9cce52dc1baacd1f05071fbe55712e65153dedf1 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 19 Sep 2025 10:53:31 +0100 Subject: [PATCH 03/13] Partially working --- Cargo.lock | 12 +- Cargo.toml | 7 +- crates/cranelift/src/compiler/component.rs | 2 +- crates/environ/src/component.rs | 4 +- crates/environ/src/component/dfg.rs | 20 +- crates/environ/src/component/info.rs | 2 +- crates/environ/src/component/translate.rs | 7 +- .../environ/src/component/translate/inline.rs | 16 +- crates/wasmtime/Cargo.toml | 1 + crates/wasmtime/src/runtime/.DS_Store | Bin 0 -> 8196 bytes .../wasmtime/src/runtime/component/.DS_Store | Bin 0 -> 6148 bytes .../src/runtime/component/concurrent.rs | 924 +++++++++++------- .../src/runtime/component/func/typed.rs | 4 +- .../src/runtime/component/instance.rs | 1 + crates/wasmtime/src/runtime/gc/.DS_Store | Bin 0 -> 6148 bytes crates/wasmtime/src/runtime/vm/.DS_Store | Bin 0 -> 8196 bytes crates/wasmtime/src/runtime/vm/component.rs | 7 +- .../src/runtime/vm/component/libcalls.rs | 25 +- crates/wasmtime/src/runtime/vm/sys/.DS_Store | Bin 0 -> 6148 bytes tests/all/component_model.rs | 1 + tests/all/component_model/threading.rs | 102 ++ .../component-model-threading/test.wast | 71 ++ 22 files changed, 842 insertions(+), 364 deletions(-) create mode 100644 crates/wasmtime/src/runtime/.DS_Store create mode 100644 crates/wasmtime/src/runtime/component/.DS_Store create mode 100644 crates/wasmtime/src/runtime/gc/.DS_Store create mode 100644 crates/wasmtime/src/runtime/vm/.DS_Store create mode 100644 crates/wasmtime/src/runtime/vm/sys/.DS_Store create mode 100644 tests/all/component_model/threading.rs create mode 100644 tests/misc_testsuite/component-model-threading/test.wast diff --git a/Cargo.lock b/Cargo.lock index e93aaff40448..844c84f757cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4476,6 +4476,7 @@ dependencies = [ "target-lexicon", "tempfile", "tokio", + "tracing-subscriber", "wasm-encoder 0.238.0", "wasm-wave", "wasmparser 0.238.0", @@ -4585,6 +4586,7 @@ dependencies = [ "tokio", "toml", "tracing", + "tracing-subscriber", "walkdir", "wasi-common", "wasm-encoder 0.238.0", @@ -5525,8 +5527,6 @@ dependencies = [ [[package]] name = "wit-bindgen" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" dependencies = [ "wit-bindgen-rt 0.43.0", "wit-bindgen-rust-macro", @@ -5535,8 +5535,6 @@ dependencies = [ [[package]] name = "wit-bindgen-core" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0" dependencies = [ "anyhow", "heck 0.5.0", @@ -5564,8 +5562,6 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd734226eac1fd7c450956964e3a9094c9cee65e9dafdf126feef8c0096db65" dependencies = [ "bitflags 2.6.0", "futures", @@ -5575,8 +5571,6 @@ dependencies = [ [[package]] name = "wit-bindgen-rust" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ebfcec48e56473805285febdb450e270fa75b2dacb92816861d0473b4c15f" dependencies = [ "anyhow", "heck 0.5.0", @@ -5591,8 +5585,6 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7852bf8a9d1ea80884d26b864ddebd7b0c7636697c6ca10f4c6c93945e023966" dependencies = [ "anyhow", "prettyplease", diff --git a/Cargo.toml b/Cargo.toml index 4f13eb7bd23e..20241691c443 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ tokio = { workspace = true, optional = true, features = ["signal", "macros"] } hyper = { workspace = true, optional = true } http = { workspace = true, optional = true } http-body-util = { workspace = true, optional = true } +tracing-subscriber.workspace = true [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["mm", "process"] } @@ -349,9 +350,9 @@ io-lifetimes = { version = "2.0.3", default-features = false } io-extras = "0.18.1" rustix = "1.0.3" # wit-bindgen: -wit-bindgen = { version = "0.43.0", default-features = false } -wit-bindgen-rt = { version = "0.43.0", default-features = false } -wit-bindgen-rust-macro = { version = "0.43.0", default-features = false } +wit-bindgen = { version = "0.43.0", path = "../wit-bindgen/crates/guest-rust", default-features = false } +wit-bindgen-rt = { version = "0.43.0", path = "../wit-bindgen/crates/guest-rust/rt", default-features = false } +wit-bindgen-rust-macro = { version = "0.43.0", path = "../wit-bindgen/crates/guest-rust/macro", default-features = false } # wasm-tools family: wasmparser = { path = "../wasm-tools/crates/wasmparser", default-features = false, features = [ diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 6ed34409ac7b..96594478e32a 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -766,7 +766,7 @@ impl<'a> TrampolineCompiler<'a> { Trampoline::ThreadResumeLater => { self.translate_libcall( host::thread_resume_later, - TrapSentinel::NegativeOne, + TrapSentinel::Falsy, WasmArgs::InRegisters, |_, _| {}, ); diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index dae7d37d0fc3..9278f7bcc9c9 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -190,9 +190,9 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] thread_new_indirect(vmctx: vmctx, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; #[cfg(feature = "component-model-async")] - thread_switch_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u64; + thread_switch_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u32; #[cfg(feature = "component-model-async")] - thread_suspend(vmctx: vmctx, cancellable: u8) -> u64; + thread_suspend(vmctx: vmctx, cancellable: u8) -> u32; #[cfg(feature = "component-model-async")] thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; #[cfg(feature = "component-model-async")] diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index e1314fd926cb..a0ef312ea9a1 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -64,9 +64,12 @@ pub struct ComponentDfg { /// Same as `reallocs`, but for post-return. pub post_returns: Intern, - /// Same as `reallocs`, but for post-return. + /// Same as `reallocs`, but for memories. pub memories: Intern>, + /// Same as `reallocs`, but for tables. + pub tables: Intern>, + /// Metadata about identified fused adapters. /// /// Note that this list is required to be populated in-order where the @@ -439,7 +442,7 @@ pub enum Trampoline { ThreadIndex, ThreadNewIndirect { start_func_ty_idx: ComponentTypeIndex, - start_func_table_idx: TableIndex, + start_func_table_id: TableId, }, ThreadSwitchTo { cancellable: bool, @@ -796,6 +799,15 @@ impl LinearizeDfg<'_> { ) } + fn runtime_table(&mut self, table: TableId) -> RuntimeTableIndex { + self.intern( + table, + |me| &mut me.runtime_tables, + |me, table| me.core_export(&me.dfg.tables[table]), + |index, export| GlobalInitializer::ExtractTable(ExtractTable { index, export }), + ) + } + fn runtime_realloc(&mut self, realloc: ReallocId) -> RuntimeReallocIndex { self.intern( realloc, @@ -997,10 +1009,10 @@ impl LinearizeDfg<'_> { Trampoline::ThreadIndex => info::Trampoline::ThreadIndex, Trampoline::ThreadNewIndirect { start_func_ty_idx, - start_func_table_idx, + start_func_table_id, } => info::Trampoline::ThreadNewIndirect { start_func_ty_idx: *start_func_ty_idx, - start_func_table_idx: *start_func_table_idx, + start_func_table_idx: self.runtime_table(*start_func_table_id), }, Trampoline::ThreadSwitchTo { cancellable } => info::Trampoline::ThreadSwitchTo { cancellable: *cancellable, diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 055a381470a7..2603af292dda 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -1051,7 +1051,7 @@ pub enum Trampoline { /// The type index for the start function of the thread. start_func_ty_idx: ComponentTypeIndex, /// The index of the table that stores the start function. - start_func_table_idx: TableIndex, + start_func_table_idx: RuntimeTableIndex, }, /// Intrinsic used to implement the `thread.switch-to` component model builtin. diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 363af6307543..9f05788b5ad0 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -1,5 +1,6 @@ use crate::ScopeVec; use crate::component::dfg::AbstractInstantiations; +use crate::component::dfg::TableId; use crate::component::*; use crate::prelude::*; use crate::{ @@ -311,7 +312,7 @@ enum LocalInitializer<'data> { ThreadNewIndirect { func: ModuleInternedTypeIndex, start_func_ty: ComponentTypeIndex, - start_func_table_idx: TableIndex, + start_func_table_index: TableIndex, }, ThreadSwitchTo { func: ModuleInternedTypeIndex, @@ -1103,14 +1104,14 @@ impl<'a, 'data> Translator<'a, 'data> { } wasmparser::CanonicalFunction::ThreadNewIndirect { func_ty_index, - table_index, + table_id, } => { let func = self.core_func_signature(core_func_index)?; core_func_index += 1; LocalInitializer::ThreadNewIndirect { func, start_func_ty: ComponentTypeIndex::from_u32(func_ty_index), - start_func_table_idx: TableIndex::from_u32(table_index), + start_func_table_index: TableIndex::from_u32(table_id), } } wasmparser::CanonicalFunction::ThreadSwitchTo { cancellable } => { diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 0ac69306efd4..44949c3dd143 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1037,14 +1037,26 @@ impl<'a> Inliner<'a> { } ThreadNewIndirect { func, - start_func_table_idx, + start_func_table_index, start_func_ty, } => { + let table_export = frame.tables[*start_func_table_index] + .clone() + .map_index(|i| match i { + EntityIndex::Table(i) => i, + _ => unreachable!(), + }); + + let table_id = self.result.tables.push(table_export); + println!( + "Table index={:?}, id={:?}", + start_func_table_index, table_id + ); let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadNewIndirect { start_func_ty_idx: *start_func_ty, - start_func_table_idx: *start_func_table_idx, + start_func_table_id: table_id, }, )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 7d99b62612ed..5b1269a14831 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -62,6 +62,7 @@ hashbrown = { workspace = true, features = ["default-hasher"] } bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } bytes = { workspace = true, optional = true } +tracing-subscriber.workspace = true [target.'cfg(target_os = "windows")'.dependencies.windows-sys] workspace = true diff --git a/crates/wasmtime/src/runtime/.DS_Store b/crates/wasmtime/src/runtime/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..070e0289cbc150ef11f6bfc5e3d0397f0710836c GIT binary patch literal 8196 zcmeHMziSjh6n=Br$VE|v6b2i&u&^{D*hE;vMGMP7!0yc5c?!GkkzAs7`v;`3u`{iO zt%?W%V<9#cg4$d72lx*Ne)A){Z+2(ZN+fv$Gw+!9-hSVFJG;x9BO^E2sG{2z0o>u<^rrjK2c8hX@ova$Bk&25Cud5Q9u+B1w?`WK>>WTb?df#_pMh( zQ9u+pk_zbeLyWF%;%s59d39hgB>-%MY18l#&n70} zEJ=GorThih$-!@rE2+-1n8erN)7I8%5?Q;ZupTewXrZKh)u zMSE-ED9*3{`o){?mmI&w^|HL;_xnhXAAQpNBDzCcU^ehsrDwU1b9wx}-3#x=}n{wFZaJjZ)WFAvu4eAQ~}J`X4QhRe}0?2Vj;SYt{fe~nk3*8Lp!-MJFi zy9QrtTa3rUegtM|6T2YCE~v5CeGV4P%Q<|$QF8DiDh0w3KK?!c=s3_fu>b6nlF~hW z+U4tU{YhIK=hV4XatdCP>+`2rpQqUuv*7s%PxGk$oLpW4JLct_uBIg?FQU>Z9O2_9 z0Kop+rp(i2e*fmdJjXo$@l~}ik;B(r_jx#H`Wh#q2e=#C^?R^3~hA1EkgcMLwZ?(4!kE^X4F}l`{(fjDS>A1pL plY&8q=y_9)!vqVqxPo71b5Z>bIy16`EwNah{*KkX^*H&L>(w&u#Tohc${@X0%zF*8av07CN!fH zw6_c1YWR%|@Y!`~L>D->l>GA>QcfjhILqs7+8d0Eyp#=G9;_0R7fCY8iV=J}&%2LL z@B6QZtHq_?iYw3k9N(}MZP1ts>}yPOy26j&&6~zvg5lmWKR^HN<2wJ=IrHK(=dXMl zc?J)hXGmwr5P3FpySw+t{Cvt;olhO>=g>CZT=#|7RZ#|%f&a?@?rfIMrl7XUfHI&A z)C}F^q)69y!0*VI?Tx zWL)uK+{(sPD8{ai`6KI2CKl9I8BhkA46Mmxm+$|h<@J9vNS~AeW#CUSV8UdO^syy( yw=Qgs@7f4@3T0uxO0W(=$6m$o<*RrHY6bR)JHXgsB?t>d{|Hzbv{43rlz|Th7J^&= literal 0 HcmV?d00001 diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 855a5f14b195..0044bf1f3fc6 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -581,7 +581,7 @@ enum SuspendReason { /// waitable set or task. Waiting { set: TableId, - task: TableId, + thread: InstanceGuestThreadIndex, }, /// The fiber has finished handling its most recent work item and is waiting /// for another (or to be dropped if it is no longer needed). @@ -589,13 +589,13 @@ enum SuspendReason { /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. Yielding { - task: TableId, - to: Option>, + thread: InstanceGuestThreadIndex, + to: Option, }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. ExplicitlySuspending { - task: TableId, - to: Option>, + thread: InstanceGuestThreadIndex, + to: Option, }, } @@ -627,7 +627,7 @@ impl fmt::Debug for GuestCallKind { /// Represents a pending call into guest code for a given guest task. #[derive(Debug)] struct GuestCall { - task: TableId, + thread: InstanceGuestThreadIndex, kind: GuestCallKind, } @@ -642,7 +642,8 @@ impl GuestCall { /// - the call is for a not-yet started task and the (sub-)component /// instance to be called has backpressure enabled fn is_ready(&self, state: &mut ConcurrentState) -> Result { - let task_instance = state.get_mut(self.task)?.instance; + return Ok(true); + let task_instance = state.get_mut(self.thread.task)?.instance; let state = state.instance_state(task_instance); let ready = match &self.kind { GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, @@ -667,8 +668,8 @@ enum WorkerItem { /// or `CallbackCode.POLL`). #[derive(Debug)] struct PollParams { - /// Identifies the polling task. - task: TableId, + /// Identifies the polling thread. + thread: InstanceGuestThreadIndex, /// The waitable set being polled. set: TableId, } @@ -708,17 +709,17 @@ impl ComponentInstance { /// async-lifted export; otherwise, it was received from its callback. fn handle_callback_code( mut self: Pin<&mut Self>, - guest_task: TableId, + guest_thread: InstanceGuestThreadIndex, runtime_instance: RuntimeComponentInstanceIndex, code: u32, initial_call: bool, ) -> Result<()> { let (code, set) = unpack_callback_code(code); - log::trace!("received callback code from {guest_task:?}: {code} (set: {set})"); + log::trace!("received callback code from {guest_thread:?}: {code} (set: {set})"); let state = self.as_mut().concurrent_state_mut(); - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if task.lift_result.is_some() { if code == callback_code::EXIT { @@ -727,7 +728,7 @@ impl ComponentInstance { if initial_call { // Notify any current or future waiters that this subtask has // started. - Waitable::Guest(guest_task).set_event( + Waitable::Guest(guest_thread.task).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -748,15 +749,18 @@ impl ComponentInstance { match code { callback_code::EXIT => { - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; match &task.caller { Caller::Host { remove_task_automatically, .. } => { if *remove_task_automatically { - log::trace!("handle_callback_code will delete task {guest_task:?}"); - Waitable::Guest(guest_task).delete_from(state)?; + log::trace!( + "handle_callback_code will delete task {:?}", + guest_thread.task + ); + Waitable::Guest(guest_thread.task).delete_from(state)?; } } Caller::Guest { .. } => { @@ -766,13 +770,13 @@ impl ComponentInstance { } } callback_code::YIELD => { - // Push this task onto the "low priority" queue so it runs after - // any other tasks have had a chance to run. - let task = state.get_mut(guest_task)?; + // Push this thread onto the "low priority" queue so it runs after + // any other threads have had a chance to run. + let task = state.get_mut(guest_thread.task)?; assert!(task.event.is_none()); task.event = Some(Event::None); state.push_low_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, + thread: guest_thread, kind: GuestCallKind::DeliverEvent { set: None }, })); } @@ -780,12 +784,12 @@ impl ComponentInstance { let set = get_set(self.as_mut(), set)?; let state = self.concurrent_state_mut(); - if state.get_mut(guest_task)?.event.is_some() + if state.get_mut(guest_thread.task)?.event.is_some() || !state.get_mut(set)?.ready.is_empty() { // An event is immediately available; deliver it ASAP. state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, + thread: guest_thread, kind: GuestCallKind::DeliverEvent { set: Some(set) }, })); } else { @@ -795,7 +799,7 @@ impl ComponentInstance { // We're polling, so just yield and check whether an // event has arrived after that. state.push_low_priority(WorkItem::Poll(PollParams { - task: guest_task, + thread: guest_thread, set, })); } @@ -806,12 +810,15 @@ impl ComponentInstance { // Here we also set `GuestTask::wake_on_cancel` // which allows `subtask.cancel` to interrupt the // wait. - let old = state.get_mut(guest_task)?.wake_on_cancel.replace(set); + let old = state + .get_thread_mut(&guest_thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); let old = state .get_mut(set)? .waiting - .insert(guest_task, WaitMode::Callback); + .insert(guest_thread, WaitMode::Callback); assert!(old.is_none()); } _ => unreachable!(), @@ -1003,7 +1010,7 @@ impl Instance { ); assert!(state.high_priority.is_empty()); assert!(state.low_priority.is_empty()); - assert!(state.guest_task.is_none()); + assert!(state.guest_thread.is_none()); assert!(state.futures.get_mut().as_ref().unwrap().is_empty()); assert!( state @@ -1332,11 +1339,11 @@ impl Instance { self.run_on_worker(store, WorkerItem::GuestCall(call)) .await?; } else { - let task = state.get_mut(call.task)?; + let task = state.get_mut(call.thread.task)?; if !task.starting_sent { task.starting_sent = true; if let GuestCallKind::Start(_) = &call.kind { - Waitable::Guest(call.task).set_event( + Waitable::Guest(call.thread.task).set_event( state, Some(Event::Subtask { status: Status::Starting, @@ -1345,22 +1352,22 @@ impl Instance { } } - let runtime_instance = state.get_mut(call.task)?.instance; + let runtime_instance = state.get_mut(call.thread.task)?.instance; state .instance_state(runtime_instance) .pending - .insert(call.task, call.kind); + .insert(call.thread, call.kind); } } WorkItem::Poll(params) => { let state = self.concurrent_state_mut(store.0); - if state.get_mut(params.task)?.event.is_some() + if state.get_mut(params.thread.task)?.event.is_some() || !state.get_mut(params.set)?.ready.is_empty() { // There's at least one event immediately available; deliver // it to the guest ASAP. state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: params.task, + thread: params.thread, kind: GuestCallKind::DeliverEvent { set: Some(params.set), }, @@ -1368,9 +1375,9 @@ impl Instance { } else { // There are no events immediately available; deliver // `Event::None` to the guest. - state.get_mut(params.task)?.event = Some(Event::None); + state.get_mut(params.thread.task)?.event = Some(Event::None); state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: params.task, + thread: params.thread, kind: GuestCallKind::DeliverEvent { set: Some(params.set), }, @@ -1388,15 +1395,15 @@ impl Instance { /// Resume the specified fiber, giving it exclusive access to the specified /// store. async fn resume_fiber(self, store: &mut StoreOpaque, fiber: StoreFiber<'static>) -> Result<()> { - let old_task = self.concurrent_state_mut(store).guest_task; - log::trace!("resume_fiber: save current task {old_task:?}"); + let old_thread = self.concurrent_state_mut(store).guest_thread; + log::trace!("resume_fiber: save current thread {old_thread:?}"); let fiber = fiber::resolve_or_release(store, fiber).await?; let state = self.concurrent_state_mut(store); - state.guest_task = old_task; - log::trace!("resume_fiber: restore current task {old_task:?}"); + state.guest_thread = old_thread; + log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { // See the `SuspendReason` documentation for what each case means. @@ -1409,32 +1416,32 @@ impl Instance { } None } - SuspendReason::Yielding { to, .. } => { + SuspendReason::Yielding { to, thread } => { state.push_low_priority(WorkItem::ResumeFiber(fiber)); - to + to.map(|next_thread| InstanceGuestThreadIndex { + task: thread.task, + thread: next_thread, + }) } - SuspendReason::ExplicitlySuspending { to, task } => { + SuspendReason::ExplicitlySuspending { to, thread } => { state - .unscheduled_guest_tasks - .insert(task, UnscheduledTaskState::Started(fiber)); - to + .get_mut(thread.task)? + .suspended_threads + .insert(thread.thread, SuspendedThreadState::Started(fiber)); + to.map(|next_thread| InstanceGuestThreadIndex { + task: thread.task, + thread: next_thread, + }) } - SuspendReason::Waiting { set, task } => { + SuspendReason::Waiting { set, thread } => { let old = state .get_mut(set)? .waiting - .insert(task, WaitMode::Fiber(fiber)); + .insert(thread, WaitMode::Fiber(fiber)); assert!(old.is_none()); None } }; - - if let Some(next) = next { - state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: next, - kind: GuestCallKind::DeliverEvent { set: None }, - })); - } } Ok(()) @@ -1472,50 +1479,54 @@ impl Instance { fn handle_guest_call(self, store: &mut dyn VMStore, call: GuestCall) -> Result<()> { match call.kind { GuestCallKind::DeliverEvent { set } => { - let (event, waitable) = - self.id().get_mut(store).get_event(call.task, set)?.unwrap(); + let (event, waitable) = self + .id() + .get_mut(store) + .get_event(call.thread.task, set)? + .unwrap(); let state = self.concurrent_state_mut(store); - let task = state.get_mut(call.task)?; + let task = state.get_mut(call.thread.task)?; let runtime_instance = task.instance; let handle = waitable.map(|(_, v)| v).unwrap_or(0); log::trace!( "use callback to deliver event {event:?} to {:?} for {waitable:?}", - call.task, + call.thread, ); - let old_task = state.guest_task.replace(call.task); + let old_thread = state.guest_thread.replace(call.thread); log::trace!( - "GuestCallKind::DeliverEvent: replaced {old_task:?} with {:?} as current task", - call.task + "GuestCallKind::DeliverEvent: replaced {old_thread:?} with {:?} as current thread", + call.thread ); - self.maybe_push_call_context(store.store_opaque_mut(), call.task)?; + self.maybe_push_call_context(store.store_opaque_mut(), call.thread.task)?; let state = self.concurrent_state_mut(store); state.enter_instance(runtime_instance); - let callback = state.get_mut(call.task)?.callback.take().unwrap(); + let callback = state.get_mut(call.thread.task)?.callback.take().unwrap(); let code = callback(store, self, runtime_instance, event, handle)?; let state = self.concurrent_state_mut(store); - state.get_mut(call.task)?.callback = Some(callback); - + state.get_mut(call.thread.task)?.callback = Some(callback); state.exit_instance(runtime_instance)?; - self.maybe_pop_call_context(store.store_opaque_mut(), call.task)?; + self.maybe_pop_call_context(store.store_opaque_mut(), call.thread.task)?; self.id().get_mut(store).handle_callback_code( - call.task, + call.thread, runtime_instance, code, false, )?; - self.concurrent_state_mut(store).guest_task = old_task; - log::trace!("GuestCallKind::DeliverEvent: restored {old_task:?} as current task"); + self.concurrent_state_mut(store).guest_thread = old_thread; + log::trace!( + "GuestCallKind::DeliverEvent: restored {old_thread:?} as current thread" + ); } GuestCallKind::Start(fun) => { fun(store, self)?; @@ -1533,19 +1544,19 @@ impl Instance { fn suspend(self, store: &mut dyn VMStore, reason: SuspendReason) -> Result<()> { log::trace!("suspend fiber: {reason:?}"); - // If we're yielding or waiting on behalf of a guest task, we'll need to + // If we're yielding or waiting on behalf of a guest thread, we'll need to // pop the call context which manages resource borrows before suspending // and then push it again once we've resumed. let task = match &reason { - SuspendReason::Yielding { task, .. } - | SuspendReason::Waiting { task, .. } - | SuspendReason::ExplicitlySuspending { task, .. } => Some(*task), + SuspendReason::Yielding { thread, .. } + | SuspendReason::Waiting { thread, .. } + | SuspendReason::ExplicitlySuspending { thread, .. } => Some(thread.task), SuspendReason::NeedWork => None, }; - let old_guest_task = if let Some(task) = task { + let old_guest_thread = if let Some(task) = task { self.maybe_pop_call_context(store, task)?; - self.concurrent_state_mut(store).guest_task + self.concurrent_state_mut(store).guest_thread } else { None }; @@ -1557,7 +1568,7 @@ impl Instance { store.with_blocking(|_, cx| cx.suspend(StoreFiberYield::ReleaseStore))?; if let Some(task) = task { - self.concurrent_state_mut(store).guest_task = old_guest_task; + self.concurrent_state_mut(store).guest_thread = old_guest_thread; self.maybe_push_call_context(store, task)?; } @@ -1613,7 +1624,7 @@ impl Instance { unsafe fn queue_call( self, mut store: StoreContextMut, - guest_task: TableId, + guest_thread: InstanceGuestThreadIndex, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1638,7 +1649,7 @@ impl Instance { /// the returned closure is called. unsafe fn make_call( store: StoreContextMut, - guest_task: TableId, + guest_thread: InstanceGuestThreadIndex, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1654,7 +1665,9 @@ impl Instance { let token = StoreToken::new(store); move |store: &mut dyn VMStore, instance: Instance| { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; - let task = instance.concurrent_state_mut(store).get_mut(guest_task)?; + let task = instance + .concurrent_state_mut(store) + .get_mut(guest_thread.task)?; let may_enter_after_call = task.call_post_return_automatically(); let lower = task.lower_params.take().unwrap(); @@ -1668,6 +1681,7 @@ impl Instance { if let Some(mut flags) = flags { flags.set_may_enter(false); } + log::trace!("calling {callee:p} from guest thread {guest_thread:?}"); crate::Func::call_unchecked_raw( &mut store, callee.as_non_null(), @@ -1692,7 +1706,7 @@ impl Instance { let call = unsafe { make_call( store.as_context_mut(), - guest_task, + guest_thread, callee, param_count, result_count, @@ -1702,21 +1716,21 @@ impl Instance { let callee_instance = self .concurrent_state_mut(store.0) - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .instance; let fun = if callback.is_some() { assert!(async_); Box::new(move |store: &mut dyn VMStore, instance: Instance| { - let old_task = instance + let old_thread = instance .concurrent_state_mut(store) - .guest_task - .replace(guest_task); + .guest_thread + .replace(guest_thread); log::trace!( - "stackless call: replaced {old_task:?} with {guest_task:?} as current task" + "stackless call: replaced {old_thread:?} with {guest_thread:?} as current thread" ); - instance.maybe_push_call_context(store.store_opaque_mut(), guest_task)?; + instance.maybe_push_call_context(store.store_opaque_mut(), guest_thread.task)?; instance .concurrent_state_mut(store) @@ -1734,18 +1748,18 @@ impl Instance { .concurrent_state_mut(store) .exit_instance(callee_instance)?; - instance.maybe_pop_call_context(store.store_opaque_mut(), guest_task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; let state = instance.concurrent_state_mut(store); - state.guest_task = old_task; - log::trace!("stackless call: restored {old_task:?} as current task"); + state.guest_thread = old_thread; + log::trace!("stackless call: restored {old_thread:?} as current thread"); // SAFETY: `wasmparser` will have validated that the callback // function returns a `i32` result. let code = unsafe { storage[0].assume_init() }.get_i32() as u32; instance.id().get_mut(store).handle_callback_code( - guest_task, + guest_thread, callee_instance, code, true, @@ -1757,17 +1771,17 @@ impl Instance { } else { let token = StoreToken::new(store.as_context_mut()); Box::new(move |store: &mut dyn VMStore, instance: Instance| { - let old_task = instance + let old_thread = instance .concurrent_state_mut(store) - .guest_task - .replace(guest_task); + .guest_thread + .replace(guest_thread); log::trace!( - "stackful call: replaced {old_task:?} with {guest_task:?} as current task", + "stackful call: replaced {old_thread:?} with {guest_thread:?} as current thread", ); let mut flags = instance.id().get(store).instance_flags(callee_instance); - instance.maybe_push_call_context(store.store_opaque_mut(), guest_task)?; + instance.maybe_push_call_context(store.store_opaque_mut(), guest_thread.task)?; // Unless this is a callback-less (i.e. stackful) // async-lifted export, we need to record that the instance @@ -1793,7 +1807,7 @@ impl Instance { // been called. if instance .concurrent_state_mut(store) - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .lift_result .is_some() { @@ -1809,9 +1823,13 @@ impl Instance { let state = instance.concurrent_state_mut(store); state.exit_instance(callee_instance)?; - assert!(state.get_mut(guest_task)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); - state.get_mut(guest_task)?.lift_result.take().unwrap() + state + .get_mut(guest_thread.task)? + .lift_result + .take() + .unwrap() }; // SAFETY: `result_count` represents the number of core Wasm @@ -1832,7 +1850,7 @@ impl Instance { if instance .concurrent_state_mut(store) - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .call_post_return_automatically() { unsafe { flags.set_needs_post_return(false) } @@ -1859,16 +1877,18 @@ impl Instance { instance.task_complete( store, - guest_task, + guest_thread.task, result, Status::Returned, post_return_arg, )?; } - instance.maybe_pop_call_context(store.store_opaque_mut(), guest_task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; - let task = instance.concurrent_state_mut(store).get_mut(guest_task)?; + let task = instance + .concurrent_state_mut(store) + .get_mut(guest_thread.task)?; match &task.caller { Caller::Host { @@ -1876,7 +1896,7 @@ impl Instance { .. } => { if *remove_task_automatically { - Waitable::Guest(guest_task) + Waitable::Guest(guest_thread.task) .delete_from(instance.concurrent_state_mut(store))?; } } @@ -1891,7 +1911,7 @@ impl Instance { self.concurrent_state_mut(store.0) .push_high_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, + thread: guest_thread, kind: GuestCallKind::Start(fun), })); @@ -1957,7 +1977,7 @@ impl Instance { let return_ = SendSyncPtr::new(NonNull::new(return_).unwrap()); let token = StoreToken::new(store.as_context_mut()); let state = self.concurrent_state_mut(store.0); - let old_task = state.guest_task.take(); + let old_thread = state.guest_thread.take(); let new_task = GuestTask::new( state, Box::new(move |store, instance, dst| { @@ -2002,7 +2022,7 @@ impl Instance { } dst.copy_from_slice(&src[..dst.len()]); let state = instance.concurrent_state_mut(store.0); - let task = state.guest_task.unwrap(); + let task = state.guest_thread.unwrap().task; Waitable::Guest(task).set_event( state, Some(Event::Subtask { @@ -2034,9 +2054,9 @@ impl Instance { )?; } let state = instance.concurrent_state_mut(store.0); - let task = state.guest_task.unwrap(); + let thread = state.guest_thread.unwrap(); if sync_caller { - state.get_mut(task)?.sync_result = + state.get_mut(thread.task)?.sync_result = Some(if let ResultInfo::Stack { result_count } = &result_info { match result_count { 0 => None, @@ -2054,7 +2074,7 @@ impl Instance { string_encoding: StringEncoding::from_u8(string_encoding).unwrap(), }, Caller::Guest { - task: old_task.unwrap(), + thread: old_thread.unwrap(), instance: caller_instance, }, None, @@ -2062,19 +2082,28 @@ impl Instance { )?; let guest_task = state.push(new_task)?; + state + .get_mut(guest_task)? + .threads + .get_mut(&MAIN_GUEST_THREAD_INDEX) + .unwrap() + .parent_task = Some(guest_task); - if let Some(old_task) = old_task { + if let Some(old_thread) = old_thread { if !state.may_enter(guest_task) { bail!(crate::Trap::CannotEnterComponent); } - state.get_mut(old_task)?.subtasks.insert(guest_task); + state.get_mut(old_thread.task)?.subtasks.insert(guest_task); }; // Make the new task the current one so that `Self::start_call` knows // which one to start. - state.guest_task = Some(guest_task); - log::trace!("pushed {guest_task:?} as current task; old task was {old_task:?}"); + state.guest_thread = Some(InstanceGuestThreadIndex { + task: guest_task, + thread: MAIN_GUEST_THREAD_INDEX, + }); + log::trace!("pushed {guest_task:?} as current task; old thread was {old_thread:?}"); Ok(()) } @@ -2142,7 +2171,11 @@ impl Instance { let token = StoreToken::new(store.as_context_mut()); let async_caller = storage.is_none(); let state = self.concurrent_state_mut(store.0); - let guest_task = state.guest_task.unwrap(); + let guest_thread = state.guest_thread.unwrap(); + // start_call should only be called for the main thread of a guest task. + assert_eq!(guest_thread.thread, MAIN_GUEST_THREAD_INDEX); + + let guest_task = guest_thread.task; let may_enter_after_call = state.get_mut(guest_task)?.call_post_return_automatically(); let callee = SendSyncPtr::new(NonNull::new(callee).unwrap()); let param_count = usize::try_from(param_count).unwrap(); @@ -2174,7 +2207,7 @@ impl Instance { } let Caller::Guest { - task: caller, + thread: caller, instance: runtime_instance, } = &task.caller else { @@ -2197,7 +2230,7 @@ impl Instance { unsafe { self.queue_call( store.as_context_mut(), - guest_task, + guest_thread, callee, param_count, result_count, @@ -2214,7 +2247,7 @@ impl Instance { // the subtask... let guest_waitable = Waitable::Guest(guest_task); let old_set = guest_waitable.common(state)?.set; - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; guest_waitable.join(state, Some(set))?; // ... and suspend this fiber temporarily while we wait for it to start. @@ -2233,7 +2266,13 @@ impl Instance { // before committing to such an optimization. And again, we'd need to // update the spec to allow that. let (status, waitable) = loop { - self.suspend(store.0, SuspendReason::Waiting { set, task: caller })?; + self.suspend( + store.0, + SuspendReason::Waiting { + set, + thread: caller, + }, + )?; let state = self.concurrent_state_mut(store.0); @@ -2286,9 +2325,9 @@ impl Instance { } } - // Reset the current task to point to the caller as it resumes control. - state.guest_task = Some(caller); - log::trace!("popped current task {guest_task:?}; new task is {caller:?}"); + // Reset the current thread to point to the caller as it resumes control. + state.guest_thread = Some(caller); + log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); Ok(status.pack(waitable)) } @@ -2337,7 +2376,7 @@ impl Instance { ) -> Result> { let token = StoreToken::new(store.as_context_mut()); let state = self.concurrent_state_mut(store.0); - let caller = state.guest_task.unwrap(); + let caller = state.guest_thread.unwrap(); // Create an abortable future which hooks calls to poll and manages call // context state for the future. @@ -2469,7 +2508,7 @@ impl Instance { // If there is no current guest task set, that means the host function // was registered using e.g. `LinkerInstance::func_wrap`, in which case // it should complete immediately. - let Some(caller) = state.guest_task else { + let Some(caller) = state.guest_thread else { return match pin!(future).poll(&mut Context::from_waker(&Waker::noop())) { Poll::Ready(result) => result, Poll::Pending => { @@ -2481,7 +2520,7 @@ impl Instance { // Save any existing result stashed in `GuestTask::result` so we can // replace it with the new result. let old_result = state - .get_mut(caller) + .get_mut(caller.task) .with_context(|| format!("bad handle: {caller:?}"))? .result .take(); @@ -2499,7 +2538,7 @@ impl Instance { let mut future = Box::pin(future.map(move |result| { HostTaskOutput::Function(Box::new(move |store, instance| { let state = instance.concurrent_state_mut(store); - state.get_mut(caller)?.result = Some(Box::new(result?) as _); + state.get_mut(caller.task)?.result = Some(Box::new(result?) as _); Waitable::Host(task).set_event( state, @@ -2539,16 +2578,25 @@ impl Instance { let state = self.concurrent_state_mut(store); state.push_future(future); - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; Waitable::Host(task).join(state, Some(set))?; - self.suspend(store, SuspendReason::Waiting { set, task: caller })?; + self.suspend( + store, + SuspendReason::Waiting { + set, + thread: caller, + }, + )?; } } // Retrieve and return the result. Ok(*mem::replace( - &mut self.concurrent_state_mut(store).get_mut(caller)?.result, + &mut self + .concurrent_state_mut(store) + .get_mut(caller.task)? + .result, old_result, ) .unwrap() @@ -2571,15 +2619,15 @@ impl Instance { data_model, .. } = *state.options(options); - let guest_task = state.guest_task.unwrap(); + let guest_thread = state.guest_thread.unwrap(); let lift = state - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .lift_result .take() .ok_or_else(|| { anyhow!("`task.return` or `task.cancel` called more than once for current task") })?; - assert!(state.get_mut(guest_task)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); let invalid = ty != lift.ty || string_encoding != lift.string_encoding @@ -2602,11 +2650,17 @@ impl Instance { bail!("invalid `task.return` signature and/or options for current task"); } - log::trace!("task.return for {guest_task:?}"); + log::trace!("task.return for {guest_thread:?}"); let result = (lift.lift)(store, self, storage)?; - self.task_complete(store, guest_task, result, Status::Returned, ValRaw::i32(0)) + self.task_complete( + store, + guest_thread.task, + result, + Status::Returned, + ValRaw::i32(0), + ) } /// Implements the `task.cancel` intrinsic. @@ -2616,8 +2670,8 @@ impl Instance { _caller_instance: RuntimeComponentInstanceIndex, ) -> Result<()> { let state = self.concurrent_state_mut(store); - let guest_task = state.guest_task.unwrap(); - let task = state.get_mut(guest_task)?; + let guest_thread = state.guest_thread.unwrap(); + let task = state.get_mut(guest_thread.task)?; if !task.cancel_sent { bail!("`task.cancel` called by task which has not been cancelled") } @@ -2627,11 +2681,11 @@ impl Instance { assert!(task.result.is_none()); - log::trace!("task.cancel for {guest_task:?}"); + log::trace!("task.cancel for {guest_thread:?}"); self.task_complete( store, - guest_task, + guest_thread.task, Box::new(DummyResult), Status::ReturnCancelled, ValRaw::i32(0), @@ -2746,46 +2800,50 @@ impl Instance { ) } - /// Implements the `thread.yield` and `thread.yield-to` intrinsics. - pub(crate) fn thread_yield_to( + /// Implements the `thread.yield` intrinsic. + pub(crate) fn thread_yield(self, store: &mut dyn VMStore, cancellable: bool) -> Result { + self.waitable_check(store, cancellable, WaitableCheck::Yield) + .map(|_| { + let state = self.concurrent_state_mut(store); + let thread = state.guest_thread.unwrap(); + if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + true + } else { + false + } + }) + } + + /// Implements the `thread.yield-to` intrinsic. + pub(crate) fn thread_yield_to( self, - store: &mut dyn VMStore, + mut store: StoreContextMut, cancellable: bool, - task: Option>, + thread: GuestThreadIndex, ) -> Result { - let check = if let Some(task) = task { - // TODO: validation - WaitableCheck::YieldTo { task } - } else { - WaitableCheck::Yield - }; - self.waitable_check(store, cancellable, check).map(|_| { - let state = self.concurrent_state_mut(store); - let task = state.guest_task.unwrap(); - if let Some(event) = state.get_mut(task).unwrap().event.take() { - assert!(matches!(event, Event::Cancelled)); - true - } else { - false - } - }) + log::trace!("thread yielding to {thread:?}"); + self.resume_suspended_thread(store.as_context_mut(), thread, true)?; + self.thread_yield(store.0, cancellable) } - pub(crate) fn thread_switch_to( + pub(crate) fn thread_switch_to( self, - store: &mut dyn VMStore, + mut store: StoreContextMut, _cancellable: bool, - task: TableId, - ) -> Result<()> { - let state = self.concurrent_state_mut(store); - let current_task = state.guest_task.unwrap(); + thread: GuestThreadIndex, + ) -> Result { + let state = self.concurrent_state_mut(store.0); + let current_thread = state.guest_thread.unwrap(); + self.resume_suspended_thread(store.as_context_mut(), thread, true)?; self.suspend( - store, + store.0, SuspendReason::ExplicitlySuspending { - task: current_task, - to: Some(task), + thread: current_thread, + to: Some(thread), }, - ) + )?; + Ok(true) } unsafe fn read_funcref_from_table( @@ -2838,115 +2896,153 @@ impl Instance { } let state = self.concurrent_state_mut(store); - let current_task = state.guest_task.unwrap(); - let instance = state.get_mut(current_task)?.instance; + let current_thread = state.guest_thread.unwrap(); // TODO check can_leave? + let new_index = state + .get_mut(current_thread.task)? + .new_thread(table_idx, func_idx, context); - let memory = state - .get_mut(current_task)? - .lift_result - .as_ref() - .and_then(|lift| lift.memory.map(|v| v.as_ptr())) - .ok_or_else(|| anyhow!("missing memory for thread start function"))?; - let new_task = GuestTask::new( - state, - Box::new(move |_store, _instance, params| { - if params.len() != 1 { - return Err(anyhow!("expected 1 parameter for thread start function")); - } - params[0].write(ValRaw::i32(context)); - Ok(()) - }), - LiftResult { - lift: Box::new(|_store, _instance, _result| { - Ok(Box::new(DummyResult) as Box) - }), - ty: TypeTupleIndex::from_u32(0), - memory: NonNull::new(memory).map(SendSyncPtr::new), - string_encoding: StringEncoding::Utf8, - }, - Caller::Guest { - task: current_task, - instance: instance, - }, - None, - instance, - )?; + log::trace!("new thread with index {new_index:?} created"); - let guest_task = state.push(new_task)?; - state.unscheduled_guest_tasks.insert( - guest_task, - UnscheduledTaskState::NotStarted { - table_idx, - func_idx, - }, + Ok(new_index.0) + } + + fn start_thread( + &self, + store: &mut StoreContextMut, + thread: GuestThreadIndex, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + high_priority: bool, + ) -> Result<()> { + let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); + log::trace!( + "resuming thread {thread:?} of task {:?} that was not started", + guest_thread.task ); + let callee = unsafe { + self.read_funcref_from_table(store.0, start_func_table_idx, start_func_idx as u64)? + }; + log::trace!("start function pointer: {callee:p}"); + // TODO check can_enter? + let new_thread = InstanceGuestThreadIndex { + task: guest_thread.task, + thread, + }; + let token = StoreToken::new(store.as_context_mut()); + let callee = SendSyncPtr::new(callee); + let start_func = Box::new( + move |store: &mut dyn VMStore, instance: Instance| -> Result<()> { + let old_thread = instance + .concurrent_state_mut(store) + .guest_thread + .replace(new_thread); + log::trace!( + "thread start: replaced {old_thread:?} with {new_thread:?} as current thread" + ); + + let mut store = token.as_context_mut(store); + instance.maybe_push_call_context(store.0, guest_thread.task)?; + + let params = [ValRaw::i32(context)]; + unsafe { + crate::Func::call_unchecked_raw( + &mut store, + callee.as_non_null(), + params.as_slice().into(), + )? + } - state.get_mut(current_task)?.subtasks.insert(guest_task); + instance.maybe_pop_call_context(store.0, guest_thread.task)?; - log::trace!("new thread with index {} created", guest_task.rep()); + let state = instance.concurrent_state_mut(store.0); + state.guest_thread = old_thread; + log::trace!("thread start: restored {old_thread:?} as current thread"); + + Ok(()) + }, + ); + if high_priority { + self.concurrent_state_mut(store.0) + .push_high_priority(WorkItem::GuestCall(GuestCall { + thread: new_thread, + kind: GuestCallKind::Start(start_func), + })); + } else { + self.concurrent_state_mut(store.0) + .push_low_priority(WorkItem::GuestCall(GuestCall { + thread: new_thread, + kind: GuestCallKind::Start(start_func), + })); + } - Ok(guest_task.rep()) + Ok(()) } - pub(crate) fn thread_resume_later( + pub(crate) fn resume_suspended_thread( self, - store: &mut dyn VMStore, - task: TableId, + mut store: StoreContextMut, + thread: GuestThreadIndex, + high_priority: bool, ) -> Result<()> { - let suspended_task = self - .concurrent_state_mut(store) - .unscheduled_guest_tasks - .remove(&task); + let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); + let state = self.concurrent_state_mut(store.0); + let suspended_task = state + .get_mut(guest_thread.task)? + .suspended_threads + .remove(&thread); if !suspended_task.is_some() { - bail!("can only resume a thread which is currently suspended"); + bail!("attempted to resume a thread that is not suspended"); } match suspended_task.unwrap() { - UnscheduledTaskState::NotStarted { + SuspendedThreadState::NotStarted { table_idx, func_idx, + context, } => { - log::trace!("resuming thread {task:?} which was not started"); - let mut callee = - unsafe { self.read_funcref_from_table(store, table_idx, func_idx as u64)? }; - // TODO check can_enter? - unsafe { - store.component_async_store().async_start( - self, - ptr::null_mut(), - ptr::null_mut(), - callee.as_mut(), - 1, - 0, - 0, //TODO maybe async - ); - } + self.start_thread( + &mut store, + thread, + table_idx, + func_idx, + context, + high_priority, + )?; } - UnscheduledTaskState::Started(fiber) => { - log::trace!("resuming thread {task:?} which was suspended"); - self.resume_fiber(store, fiber); + SuspendedThreadState::Started(fiber) => { + log::trace!( + "resuming thread {thread:?} of task {:?} that was suspended", + guest_thread.task + ); + if high_priority { + self.concurrent_state_mut(store.0) + .push_high_priority(WorkItem::ResumeFiber(fiber)); + } else { + self.concurrent_state_mut(store.0) + .push_low_priority(WorkItem::ResumeFiber(fiber)); + } } } - Ok(()) } - pub(crate) fn thread_suspend(self, store: &mut dyn VMStore, cancellable: bool) -> Result<()> { + pub(crate) fn thread_suspend(self, store: &mut dyn VMStore, cancellable: bool) -> Result { if cancellable { bail!("todo: cancellable `thread.suspend` not implemented yet"); } - let guest_task = self.concurrent_state_mut(store).guest_task.unwrap(); + let guest_thread = self.concurrent_state_mut(store).guest_thread.unwrap(); self.suspend( store, SuspendReason::ExplicitlySuspending { - task: guest_task, + thread: guest_thread, to: None, }, )?; - Ok(()) + Ok(true) } /// Helper function for the `waitable-set.wait`, `waitable-set.poll`, and @@ -2963,28 +3059,27 @@ impl Instance { ); } - let guest_task = self.concurrent_state_mut(store).guest_task.unwrap(); + let guest_thread = self.concurrent_state_mut(store).guest_thread.unwrap(); let (wait, set) = match &check { WaitableCheck::Wait(params) => (true, Some(params.set)), WaitableCheck::Poll(params) => (false, Some(params.set)), WaitableCheck::Yield => (false, None), - WaitableCheck::YieldTo { task } => (false, None), }; - // First, suspend this fiber, allowing any other tasks to run. + // First, suspend this fiber, allowing any other threads to run. self.suspend( store, SuspendReason::Yielding { - task: guest_task, + thread: guest_thread, to: None, }, )?; - log::trace!("waitable check for {guest_task:?}; set {set:?}"); + log::trace!("waitable check for {guest_thread:?}; set {set:?}"); let state = self.concurrent_state_mut(store); - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if wait && task.callback.is_some() { bail!("cannot call `task.wait` from async-lifted export with callback"); @@ -2996,20 +3091,23 @@ impl Instance { let set = set.unwrap(); if task.event.is_none() && state.get_mut(set)?.ready.is_empty() { - let old = state.get_mut(guest_task)?.wake_on_cancel.replace(set); + let old = state + .get_thread_mut(&guest_thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); self.suspend( store, SuspendReason::Waiting { set, - task: guest_task, + thread: guest_thread, }, )?; } } - log::trace!("waitable check for {guest_task:?}; set {set:?}, part two"); + log::trace!("waitable check for {guest_thread:?}; set {set:?}, part two"); let result = match check { // Deliver any pending events to the guest and return. @@ -3017,7 +3115,7 @@ impl Instance { let event = self .id() .get_mut(store) - .get_event(guest_task, Some(params.set))?; + .get_event(guest_thread.task, Some(params.set))?; let (ordinal, handle, result) = if wait { let (event, waitable) = event.unwrap(); @@ -3031,7 +3129,8 @@ impl Instance { (ordinal, handle, result) } else { log::trace!( - "no events ready to deliver via waitable-set.poll to {guest_task:?}; set {:?}", + "no events ready to deliver via waitable-set.poll to {:?}; set {:?}", + guest_thread.task, params.set ); let (ordinal, result) = Event::None.parts(); @@ -3049,7 +3148,6 @@ impl Instance { Ok(ordinal) } WaitableCheck::Yield => Ok(0), - WaitableCheck::YieldTo { .. } => Ok(0), }; result @@ -3097,7 +3195,7 @@ impl Instance { return Ok(Status::ReturnCancelled as u32); } } else { - let caller = concurrent_state.guest_task.unwrap(); + let caller = concurrent_state.guest_thread.unwrap(); let guest_task = TableId::::new(rep); let task = concurrent_state.get_mut(guest_task)?; if task.lower_params.is_some() { @@ -3107,14 +3205,18 @@ impl Instance { // Not yet started; cancel and remove from pending let callee_instance = task.instance; - let kind = concurrent_state + if !concurrent_state .instance_state(callee_instance) .pending - .remove(&guest_task); - - if kind.is_none() { + .iter() + .any(|(thread, _)| thread.task == guest_task) + { bail!("`subtask.cancel` called after terminal status delivered"); } + concurrent_state + .instance_state(callee_instance) + .pending + .retain(|thread, _| thread.task != guest_task); return Ok(Status::StartCancelled as u32); } else if task.lift_result.is_some() { @@ -3126,28 +3228,37 @@ impl Instance { // `Event::Cancelled` if it was already cancelled), but that's // okay -- this should supersede the previous state. task.event = Some(Event::Cancelled); - if let Some(set) = task.wake_on_cancel.take() { - let item = match concurrent_state - .get_mut(set)? - .waiting - .remove(&guest_task) - .unwrap() + let threads = task.threads.keys().cloned().collect::>(); + for thread in threads { + if let Some(set) = task.threads.get_mut(&thread).unwrap().wake_on_cancel.take() { - WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), - WaitMode::Callback => WorkItem::GuestCall(GuestCall { + let instance_thread_index = InstanceGuestThreadIndex { task: guest_task, - kind: GuestCallKind::DeliverEvent { set: None }, - }), - }; - concurrent_state.push_high_priority(item); + thread, + }; + let item = match concurrent_state + .get_mut(set)? + .waiting + .remove(&instance_thread_index) + .unwrap() + { + WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), + WaitMode::Callback => WorkItem::GuestCall(GuestCall { + thread: instance_thread_index, + kind: GuestCallKind::DeliverEvent { set: None }, + }), + }; + concurrent_state.push_high_priority(item); - self.suspend( - store, - SuspendReason::Yielding { - task: caller, - to: None, - }, - )?; + self.suspend( + store, + SuspendReason::Yielding { + thread: caller, + to: None, + }, + )?; + break; + } } let concurrent_state = self.concurrent_state_mut(store); @@ -3160,10 +3271,16 @@ impl Instance { } else { let waitable = Waitable::Guest(guest_task); let old_set = waitable.common(concurrent_state)?.set; - let set = concurrent_state.get_mut(caller)?.sync_call_set; + let set = concurrent_state.get_mut(caller.task)?.sync_call_set; waitable.join(concurrent_state, Some(set))?; - self.suspend(store, SuspendReason::Waiting { set, task: caller })?; + self.suspend( + store, + SuspendReason::Waiting { + set, + thread: caller, + }, + )?; waitable.join(self.concurrent_state_mut(store), old_set)?; } @@ -3362,6 +3479,25 @@ pub trait VMComponentAsyncStore { err_ctx_handle: u32, debug_msg_address: u32, ) -> Result<()>; + + /// The `thread.resume-later` intrinsic. + fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()>; + + /// The `thread.yield-to` intrinsic. + fn thread_yield_to( + &mut self, + instance: Instance, + cancellable: bool, + thread: GuestThreadIndex, + ) -> Result; + + /// The `thread.switch-to` intrinsic. + fn thread_switch_to( + &mut self, + instance: Instance, + cancellable: bool, + thread: GuestThreadIndex, + ) -> Result; } /// SAFETY: See trait docs. @@ -3639,6 +3775,28 @@ impl VMComponentAsyncStore for StoreInner { debug_msg_address, ) } + + fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()> { + instance.resume_suspended_thread(StoreContextMut(self), thread, false) + } + + fn thread_yield_to( + &mut self, + instance: Instance, + cancellable: bool, + thread: GuestThreadIndex, + ) -> Result { + instance.thread_yield_to(StoreContextMut(self), cancellable, thread) + } + + fn thread_switch_to( + &mut self, + instance: Instance, + cancellable: bool, + thread: GuestThreadIndex, + ) -> Result { + instance.thread_switch_to(StoreContextMut(self), cancellable, thread) + } } /// Represents the output of a host task or background task. @@ -3708,10 +3866,10 @@ enum Caller { /// If true, call `post-return` function (if any) automatically. call_post_return_automatically: bool, }, - /// Another guest task called the guest task + /// Another guest thread called the guest task Guest { /// The id of the caller - task: TableId, + thread: InstanceGuestThreadIndex, /// The instance to use to enforce reentrance rules. /// /// Note that this might not be the same as the instance the caller task @@ -3730,8 +3888,57 @@ struct LiftResult { string_encoding: StringEncoding, } +/// The index for a thread within a guest task. +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +#[repr(transparent)] +pub struct GuestThreadIndex(u32); +impl GuestThreadIndex { + pub fn rep(&self) -> u32 { + self.0 + } + pub fn from_u32(i: u32) -> Self { + Self(i) + } +} + +const MAIN_GUEST_THREAD_INDEX: GuestThreadIndex = GuestThreadIndex(0); +pub(crate) struct GuestThread { + /// The thread index within the owning guest task. + index: GuestThreadIndex, + /// Context-local state used to implement the `context.{get,set}` + /// intrinsics. + context: [u32; 2], + /// The owning guest task. Logically, a `GuestThread` always belongs to + /// exactly one `GuestTask`, but the `TableId` for that task is not available + /// until the `GuestTask` is stored in the state. As such, this is an `Option` + /// which is set when the `GuestTask` is stored. It is an invariant that this + /// be `Some` whenever operations on `GuestThread`s are performed. + parent_task: Option>, + /// If present, indicates that the thread is currently waiting on the + /// specified set but may be cancelled and woken immediately. + wake_on_cancel: Option>, +} + +/// The index for a thread within a component instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct InstanceGuestThreadIndex { + task: TableId, + thread: GuestThreadIndex, +} + +impl GuestThread { + fn new(index: GuestThreadIndex) -> Self { + Self { + index, + context: [0; 2], + parent_task: None, + wake_on_cancel: None, + } + } +} + /// Represents a pending guest task. -struct GuestTask { +pub(crate) struct GuestTask { /// See `WaitableCommon` common: WaitableCommon, /// Closure to lower the parameters passed to this task. @@ -3758,9 +3965,6 @@ struct GuestTask { /// Whether or not we've sent a `Status::Starting` event to any current or /// future waiters for this waitable. starting_sent: bool, - /// Context-local state used to implement the `context.{get,set}` - /// intrinsics. - context: [u32; 2], /// Pending guest subtasks created by this task (directly or indirectly). /// /// This is used to re-parent subtasks which are still running when their @@ -3777,13 +3981,21 @@ struct GuestTask { /// If present, a pending `Event::None` or `Event::Cancelled` to be /// delivered to this task. event: Option, - /// If present, indicates that the task is currently waiting on the - /// specified set but may be cancelled and woken immediately. - wake_on_cancel: Option>, /// The `ExportIndex` of the guest function being called, if known. function_index: Option, /// Whether or not the task has exited. exited: bool, + /// Threads belonging to this task, either created implicitly on a call to + /// an export, or explicitly via `thread.new_indirect`. + /// + /// The implicit thread is always present, and has index 0 (MAIN_GUEST_THREAD_INDEX). + threads: HashMap, + /// Threads that are suspended, either because they were created with + /// `thread.new_indirect` but not yet started, or because they explicitly + /// suspended themselves with `thread.suspend` or `thread.switch-to`. + suspended_threads: BTreeMap, + /// The next thread index to use when creating a new thread. + next_thread_index: GuestThreadIndex, } impl GuestTask { @@ -3808,17 +4020,43 @@ impl GuestTask { sync_result: None, cancel_sent: false, starting_sent: false, - context: [0u32; 2], subtasks: HashSet::new(), sync_call_set, instance: component_instance, event: None, - wake_on_cancel: None, function_index: None, exited: false, + threads: HashMap::from([( + MAIN_GUEST_THREAD_INDEX, + GuestThread::new(MAIN_GUEST_THREAD_INDEX), + )]), + suspended_threads: BTreeMap::new(), + next_thread_index: GuestThreadIndex(MAIN_GUEST_THREAD_INDEX.0 + 1), }) } + /// Create a new thread within this guest task that starts suspended + fn new_thread( + &mut self, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> GuestThreadIndex { + let thread_index = self.next_thread_index; + self.next_thread_index = GuestThreadIndex(self.next_thread_index.0 + 1); + self.threads + .insert(thread_index, GuestThread::new(thread_index)); + self.suspended_threads.insert( + thread_index, + SuspendedThreadState::NotStarted { + table_idx: start_func_table_idx, + func_idx: start_func_idx, + context, + }, + ); + thread_index + } + /// Dispose of this guest task, reparenting any pending subtasks to the /// caller. fn dispose(self, state: &mut ConcurrentState, me: TableId) -> Result<()> { @@ -3837,11 +4075,11 @@ impl GuestTask { // Reparent any pending subtasks to the caller. if let Caller::Guest { - task, + thread, instance: runtime_instance, } = &self.caller { - let task_mut = state.get_mut(*task)?; + let task_mut = state.get_mut(thread.task)?; let present = task_mut.subtasks.remove(&me); assert!(present); @@ -3851,7 +4089,7 @@ impl GuestTask { for subtask in &self.subtasks { state.get_mut(*subtask)?.caller = Caller::Guest { - task: *task, + thread: *thread, instance: *runtime_instance, }; } @@ -4003,14 +4241,14 @@ impl Waitable { fn mark_ready(&self, state: &mut ConcurrentState) -> Result<()> { if let Some(set) = self.common(state)?.set { state.get_mut(set)?.ready.insert(*self); - if let Some((task, mode)) = state.get_mut(set)?.waiting.pop_first() { - let wake_on_cancel = state.get_mut(task)?.wake_on_cancel.take(); + if let Some((thread, mode)) = state.get_mut(set)?.waiting.pop_first() { + let wake_on_cancel = state.get_thread_mut(&thread)?.wake_on_cancel.take(); assert!(wake_on_cancel.is_none() || wake_on_cancel == Some(set)); let item = match mode { WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), WaitMode::Callback => WorkItem::GuestCall(GuestCall { - task, + thread, kind: GuestCallKind::DeliverEvent { set: Some(set) }, }), }; @@ -4104,8 +4342,8 @@ impl fmt::Debug for Waitable { struct WaitableSet { /// Which waitables in this set have pending events, if any. ready: BTreeSet, - /// Which guest tasks are currently waiting on this set, if any. - waiting: BTreeMap, WaitMode>, + /// Which guest threads are currently waiting on this set, if any. + waiting: BTreeMap, } impl TableDebug for WaitableSet { @@ -4161,24 +4399,23 @@ struct InstanceState { do_not_enter: bool, /// Pending calls for this instance which require `Self::backpressure` to be /// `true` and/or `Self::do_not_enter` to be false before they can proceed. - pending: BTreeMap, GuestCallKind>, + pending: BTreeMap, } -enum UnscheduledTaskState { +enum SuspendedThreadState { Started(StoreFiber<'static>), NotStarted { table_idx: RuntimeTableIndex, func_idx: u32, + context: i32, }, } /// Represents the Component Model Async state of a top-level component instance /// (i.e. a `super::ComponentInstance`). pub struct ConcurrentState { - /// The currently running guest task, if any. - guest_task: Option>, - - unscheduled_guest_tasks: BTreeMap, UnscheduledTaskState>, + /// The currently running guest thread, if any. + guest_thread: Option, /// The set of pending host and background tasks, if any. /// @@ -4231,8 +4468,7 @@ pub struct ConcurrentState { impl ConcurrentState { pub(crate) fn new(component: &Component) -> Self { Self { - guest_task: None, - unscheduled_guest_tasks: BTreeMap::new(), + guest_thread: None, table: AlwaysMut::new(ResourceTable::new()), futures: AlwaysMut::new(Some(FuturesUnordered::new())), instance_states: HashMap::new(), @@ -4322,6 +4558,26 @@ impl ConcurrentState { self.table.get_mut().get_mut(&Resource::from(id)) } + fn get_thread( + &mut self, + idx: &InstanceGuestThreadIndex, + ) -> Result<&GuestThread, ResourceTableError> { + self.get_mut(idx.task)? + .threads + .get(&idx.thread) + .ok_or(ResourceTableError::NotPresent) + } + + fn get_thread_mut( + &mut self, + idx: &InstanceGuestThreadIndex, + ) -> Result<&mut GuestThread, ResourceTableError> { + self.get_mut(idx.task)? + .threads + .get_mut(&idx.thread) + .ok_or(ResourceTableError::NotPresent) + } + pub fn add_child( &mut self, child: TableId, @@ -4388,11 +4644,11 @@ impl ConcurrentState { loop { match &self.get_mut(guest_task).unwrap().caller { Caller::Host { .. } => break true, - Caller::Guest { task, instance } => { + Caller::Guest { thread, instance } => { if *instance == guest_instance { break false; } else { - guest_task = *task; + guest_task = thread.task; } } } @@ -4419,14 +4675,14 @@ impl ConcurrentState { /// /// See `GuestCall::is_ready` for details. fn partition_pending(&mut self, instance: RuntimeComponentInstanceIndex) -> Result<()> { - for (task, kind) in mem::take(&mut self.instance_state(instance).pending).into_iter() { - let call = GuestCall { task, kind }; + for (thread, kind) in mem::take(&mut self.instance_state(instance).pending).into_iter() { + let call = GuestCall { thread, kind }; if call.is_ready(self)? { self.push_high_priority(WorkItem::GuestCall(call)); } else { self.instance_state(instance) .pending - .insert(call.task, call.kind); + .insert(call.thread, call.kind); } } @@ -4455,23 +4711,27 @@ impl ConcurrentState { /// Implements the `context.get` intrinsic. pub(crate) fn context_get(&mut self, slot: u32) -> Result { - let task = self.guest_task.unwrap(); - let val = self.get_mut(task)?.context[usize::try_from(slot).unwrap()]; - log::trace!("context_get {task:?} slot {slot} val {val:#x}"); + let thread = self.guest_thread.unwrap(); + let val = self.get_thread(&thread)?.context[usize::try_from(slot).unwrap()]; + log::trace!("context_get {thread:?} slot {slot} val {val:#x}"); Ok(val) } /// Implements the `context.set` intrinsic. pub(crate) fn context_set(&mut self, slot: u32, val: u32) -> Result<()> { - let task = self.guest_task.unwrap(); - log::trace!("context_set {task:?} slot {slot} val {val:#x}"); - self.get_mut(task)?.context[usize::try_from(slot).unwrap()] = val; + let thread = self.guest_thread.unwrap(); + log::trace!("context_set {thread:?} slot {slot} val {val:#x}"); + self.get_thread_mut(&thread)?.context[usize::try_from(slot).unwrap()] = val; Ok(()) } /// Implements the `thread.index` intrinsic. pub(crate) fn thread_index(&self) -> Result { - Ok(self.guest_task.unwrap().rep()) + Ok(self + .guest_thread + .ok_or_else(|| anyhow!("no current thread"))? + .thread + .0) } fn options(&self, options: OptionsIndex) -> &CanonicalOptions { @@ -4564,15 +4824,14 @@ enum WaitableCheck { Wait(WaitableCheckParams), Poll(WaitableCheckParams), Yield, - YieldTo { task: TableId }, } /// Represents a guest task called from the host, prepared using `prepare_call`. pub(crate) struct PreparedCall { /// The guest export to be called handle: Func, - /// The guest task created by `prepare_call` - task: TableId, + /// The guest thread created by `prepare_call` + thread: InstanceGuestThreadIndex, /// The number of lowered core Wasm parameters to pass to the call. param_count: usize, /// The `oneshot::Receiver` to which the result of the call will be @@ -4586,7 +4845,7 @@ impl PreparedCall { pub(crate) fn task_id(&self) -> TaskId { TaskId { handle: self.handle, - task: self.task, + task: self.thread.task, } } } @@ -4643,7 +4902,7 @@ pub(crate) fn prepare_call( let token = StoreToken::new(store.as_context_mut()); let state = handle.instance().concurrent_state_mut(store.0); - assert!(state.guest_task.is_none()); + assert!(state.guest_thread.is_none()); let (tx, rx) = oneshot::channel(); @@ -4696,10 +4955,19 @@ pub(crate) fn prepare_call( task.function_index = Some(handle.index()); let task = state.push(task)?; + state + .get_mut(task)? + .threads + .get_mut(&MAIN_GUEST_THREAD_INDEX) + .unwrap() + .parent_task = Some(task); Ok(PreparedCall { handle, - task, + thread: InstanceGuestThreadIndex { + task, + thread: MAIN_GUEST_THREAD_INDEX, + }, param_count, rx, _phantom: PhantomData, @@ -4718,13 +4986,13 @@ pub(crate) fn queue_call( ) -> Result> + Send + 'static + use> { let PreparedCall { handle, - task, + thread, param_count, rx, .. } = prepared; - queue_call0(store.as_context_mut(), handle, task, param_count)?; + queue_call0(store.as_context_mut(), handle, thread, param_count)?; Ok(checked( handle.instance(), @@ -4741,7 +5009,7 @@ pub(crate) fn queue_call( fn queue_call0( store: StoreContextMut, handle: Func, - guest_task: TableId, + guest_thread: InstanceGuestThreadIndex, param_count: usize, ) -> Result<()> { let (options, flags, _ty, raw_options) = handle.abi_info(store.0); @@ -4751,7 +5019,7 @@ fn queue_call0( let callback = options.callback(); let post_return = handle.post_return_core_func(store.0); - log::trace!("queueing call {guest_task:?}"); + log::trace!("queueing call {guest_thread:?}"); let instance_flags = if callback.is_none() { None @@ -4765,7 +5033,7 @@ fn queue_call0( unsafe { instance.queue_call( store, - guest_task, + guest_thread, SendSyncPtr::new(callee), param_count, 1, diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 3e669ab27827..3bca6382d0d7 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -191,7 +191,7 @@ where let ptr = SendSyncPtr::from(NonNull::from(¶ms).cast::()); let prepared = - self.prepare_call(store.as_context_mut(), false, false, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), true, false, move |cx, ty, dst| { // SAFETY: The goal here is to get `Params`, a non-`'static` // value, to live long enough to the lowering of the // parameters. We're guaranteed that `Params` lives in the @@ -220,7 +220,7 @@ where impl<'a, T> Drop for RemoveOnDrop<'a, T> { fn drop(&mut self) { - self.task.remove(self.store.as_context_mut()).unwrap(); + // self.task.remove(self.store.as_context_mut()).unwrap(); } } diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 6f78c47aef02..7b3185492fc3 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -809,6 +809,7 @@ impl<'a> Instantiator<'a> { } fn extract_table(&mut self, store: &mut StoreOpaque, table: &ExtractTable) { + println!("extract_table: {:?}", table); let export = match lookup_vmexport(store, self.id, &table.export) { crate::runtime::vm::Export::Table(t) => t, _ => unreachable!(), diff --git a/crates/wasmtime/src/runtime/gc/.DS_Store b/crates/wasmtime/src/runtime/gc/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9429fe79751df48df500f7285c8dbec63f395e77 GIT binary patch literal 6148 zcmeHKK~BR!4D>dYNX4Z`ZtNGr3qqChfqVc$5FjO_QpG(_;2GRG@dDn!g%c0p&V}*X zYLljsI3ZNol6N*zat%;;~GK=0p@j87DIgdxY0n2QpC$FS@WtK^5IlS=}c2 zz}pP}kpcO*CEe4SuBh|>{i~(c77&sILP_tQLzDZ4t z0b{@z=rX|fgNHJvimhO{IxvJ5062v?26#Qq2b8f-8I{*-|qUrGBE+5EO)(;*vNO6iAVPj*zqEKy1!(Y#@o3}JG7x#(^U_~hZd}iY-6l1d^F1GGu5{Ejf0;)h+0j}L!l%jGU(n!|t>pN*S9N7TV{#5NX%sQRD zLAD2L^ZmW(-N~J=8+D%DB|S&`GV?7a(4ao0gT9z={JMWSFF8zqeE9tHw&h@BhS@CZ zye-HR@p58Vm$nE1~*0MFsx6Api_-B#Q z0X@d`OFW+mp3A;{PW_O_!`9gmlHc7&1-~=$NZ!jwh4*8BND=PghzufidtwFvh$D(u@YK^g%M^KD+be9I$N8>pLv~_)vujF*{rQlS%wp42w|A05B zqP6G+b{fBvaFPhdulPJs{Q7A%tM-yyuYD`HR;(vkF7^!htua4ss=#?rAaXa+=K6oT z{`~(u+^XJ#DxeC~C}37PJDp7!eQjNG<61jL-$m!fez`*%g29&KKwFLji+>p6I)*F9 aBp!2zm_hsJ2Lbl)3H|( + offset + offset_of!(VMTableImport, from) as u32, + ) = INVALID_PTR; + *self.as_mut().vmctx_plus_offset_mut::( + offset + offset_of!(VMTableImport, vmctx) as u32, + ) = INVALID_PTR; } } } diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 33f6454bfe41..723c55a9b25e 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -1,6 +1,7 @@ //! Implementation of string transcoding required by the component model. use crate::component::Instance; +use crate::component::concurrent::GuestThreadIndex; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; @@ -774,7 +775,7 @@ fn waitable_join( #[cfg(feature = "component-model-async")] fn thread_yield(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { - instance.thread_yield_to(store, cancellable != 0, None) + instance.thread_yield(store, cancellable != 0) } #[cfg(feature = "component-model-async")] @@ -1277,18 +1278,24 @@ fn thread_switch_to( instance: Instance, cancellable: u8, thread_idx: u32, -) -> Result { - todo!() +) -> Result { + store.component_async_store().thread_switch_to( + instance, + cancellable != 0, + GuestThreadIndex::from_u32(thread_idx), + ) } #[cfg(feature = "component-model-async")] -fn thread_suspend(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { - todo!() +fn thread_suspend(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { + instance.thread_suspend(store, cancellable != 0) } #[cfg(feature = "component-model-async")] fn thread_resume_later(store: &mut dyn VMStore, instance: Instance, thread_idx: u32) -> Result<()> { - todo!() + store + .component_async_store() + .thread_resume_later(instance, GuestThreadIndex::from_u32(thread_idx)) } #[cfg(feature = "component-model-async")] @@ -1298,5 +1305,9 @@ fn thread_yield_to( cancellable: u8, thread_idx: u32, ) -> Result { - instance.thread_yield_to(store, cancellable != 0, Some(TableId::new(thread_idx))) + store.component_async_store().thread_yield_to( + instance, + cancellable != 0, + GuestThreadIndex::from_u32(thread_idx), + ) } diff --git a/crates/wasmtime/src/runtime/vm/sys/.DS_Store b/crates/wasmtime/src/runtime/vm/sys/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0e4f80fb96aab064350932d589474e789dac0cd9 GIT binary patch literal 6148 zcmeHKu};G<5PgOSDp)GSz=ZM(Wyq&ydrjy06Z^vL%0+o8=YvCV$A=7v2I>V!jJ_&C9dn^}agq=TnWh z^jEx_`~k0=ocmZ3>l7o>eTjL%j?04B*COhBIDRen(1@ybvOOx;4<1XaY`_19N&2J;r~-dV z0TZRYv`eq#Yir}>xYowh`qbDs&I-B|>U=qN11`lU)SQto<^f^i(F)QGnf?ek8MIRc HepG=k8PJYE literal 0 HcmV?d00001 diff --git a/tests/all/component_model.rs b/tests/all/component_model.rs index 3bc1891df876..42389811b5bf 100644 --- a/tests/all/component_model.rs +++ b/tests/all/component_model.rs @@ -19,6 +19,7 @@ mod nested; mod post_return; mod resources; mod strings; +mod threading; #[test] #[cfg_attr(miri, ignore)] diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs new file mode 100644 index 000000000000..eb2d9f1e0e4f --- /dev/null +++ b/tests/all/component_model/threading.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use wasmtime::component::types::ComponentItem; +use wasmtime::component::{Component, Linker, Type}; +use wasmtime::{Config, Engine, Module, Precompiled, Store}; + +#[tokio::test] +async fn threads() -> Result<()> { + use std::io::IsTerminal; + use tracing_subscriber::{EnvFilter, FmtSubscriber}; + let builder = FmtSubscriber::builder() + .with_writer(std::io::stderr) + .with_env_filter(EnvFilter::from_env("WASMTIME_LOG")) + .with_ansi(std::io::stderr().is_terminal()) + .init(); + let mut config = Config::new(); + config.async_support(true); + config.wasm_component_model_async(true); + config.wasm_component_model_threading(true); + let engine = Engine::new(&config)?; + let component = Component::new( + &engine, + r#" + (component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A global that we will set from the spawned thread + (global $g (mut i32) (i32.const 0)) + + ;; The thread entry point, which sets the global to the value passed in + (func $thread-start (param i32) + local.get 0 + global.set $g + i32.const 0 + call $thread-switch-to + drop) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 + ;; as the context value, and then yields to it + (func (export "run") (result i32) + i32.const 0 + i32.const 42 + call $thread-new-indirect + call $thread-switch-to + drop + global.get $g)) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + "#, + )? + .serialize()?; + + let component = unsafe { Component::deserialize(&engine, &component)? }; + let mut store = Store::new(&engine, ()); + let instance = Linker::new(&engine) + .instantiate_async(&mut store, &component) + .await?; + let func = instance.get_typed_func::<(), (u32,)>(&mut store, "run")?; + assert_eq!(func.call_async(&mut store, ()).await?, (1,)); + + Ok(()) +} diff --git a/tests/misc_testsuite/component-model-threading/test.wast b/tests/misc_testsuite/component-model-threading/test.wast new file mode 100644 index 000000000000..7f5cfda3d803 --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/test.wast @@ -0,0 +1,71 @@ +;;! component_model_async = true +;;! component_model_threading = true + +(component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A global that we will set from the spawned thread + (global $g (mut i32) (i32.const 0)) + + ;; The thread entry point, which sets the global to the value passed in + (func $thread-start (param i32) + local.get 0 + global.set $g + i32.const 0 + call $thread-switch-to + drop) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 + ;; as the context value, and then yields to it + (func (export "run") (result i32) + i32.const 0 + i32.const 42 + call $thread-new-indirect + call $thread-switch-to + drop + global.get $g)) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From 6e6b83ff472bd14fea49b52cf33a1443eafa43d7 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 29 Sep 2025 09:52:27 +0100 Subject: [PATCH 04/13] Cleanup --- crates/environ/src/component/translate.rs | 1 - .../environ/src/component/translate/inline.rs | 4 - .../src/runtime/component/concurrent.rs | 86 +++++++++++++------ .../src/runtime/vm/component/libcalls.rs | 2 +- tests/all/component_model/threading.rs | 9 +- 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 9f05788b5ad0..b978be793abc 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -1,6 +1,5 @@ use crate::ScopeVec; use crate::component::dfg::AbstractInstantiations; -use crate::component::dfg::TableId; use crate::component::*; use crate::prelude::*; use crate::{ diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 44949c3dd143..509fd5f4eb7a 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1048,10 +1048,6 @@ impl<'a> Inliner<'a> { }); let table_id = self.result.tables.push(table_export); - println!( - "Table index={:?}, id={:?}", - start_func_table_index, table_id - ); let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadNewIndirect { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 0044bf1f3fc6..27b08b6527ac 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -642,17 +642,21 @@ impl GuestCall { /// - the call is for a not-yet started task and the (sub-)component /// instance to be called has backpressure enabled fn is_ready(&self, state: &mut ConcurrentState) -> Result { - return Ok(true); let task_instance = state.get_mut(self.thread.task)?.instance; let state = state.instance_state(task_instance); - let ready = match &self.kind { - GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, - GuestCallKind::Start(_) => !(state.do_not_enter || state.backpressure), - }; + + // Explicit threads are always ready, as they can only be entered from within the same + // task. + let ready = self.thread.is_explicit_thread() + || match &self.kind { + GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, + GuestCallKind::Start(_) => !(state.do_not_enter || state.backpressure), + }; log::trace!( - "call {self:?} ready? {ready} (do_not_enter: {}; backpressure: {})", + "call {self:?} ready? {ready} (do_not_enter: {}; backpressure: {}, is_explicit_thread: {})", state.do_not_enter, - state.backpressure + state.backpressure, + self.thread.is_explicit_thread() ); Ok(ready) } @@ -1407,31 +1411,22 @@ impl Instance { if let Some(mut fiber) = fiber { // See the `SuspendReason` documentation for what each case means. - let next = match state.suspend_reason.take().unwrap() { + match state.suspend_reason.take().unwrap() { SuspendReason::NeedWork => { if state.worker.is_none() { state.worker = Some(fiber); } else { fiber.dispose(store); } - None } - SuspendReason::Yielding { to, thread } => { + SuspendReason::Yielding { .. } => { state.push_low_priority(WorkItem::ResumeFiber(fiber)); - to.map(|next_thread| InstanceGuestThreadIndex { - task: thread.task, - thread: next_thread, - }) } - SuspendReason::ExplicitlySuspending { to, thread } => { + SuspendReason::ExplicitlySuspending { thread, .. } => { state .get_mut(thread.task)? .suspended_threads .insert(thread.thread, SuspendedThreadState::Started(fiber)); - to.map(|next_thread| InstanceGuestThreadIndex { - task: thread.task, - thread: next_thread, - }) } SuspendReason::Waiting { set, thread } => { let old = state @@ -1439,9 +1434,10 @@ impl Instance { .waiting .insert(thread, WaitMode::Fiber(fiber)); assert!(old.is_none()); - None } }; + } else { + log::trace!("resume_fiber: fiber has exited"); } Ok(()) @@ -1886,9 +1882,9 @@ impl Instance { instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; - let task = instance - .concurrent_state_mut(store) - .get_mut(guest_thread.task)?; + let state = instance.concurrent_state_mut(store); + let task = state.get_mut(guest_thread.task)?; + task.thread_completed(guest_thread.thread); match &task.caller { Caller::Host { @@ -1896,8 +1892,7 @@ impl Instance { .. } => { if *remove_task_automatically { - Waitable::Guest(guest_thread.task) - .delete_from(instance.concurrent_state_mut(store))?; + self.delete_task_if_all_threads_exited(store, guest_thread.task)?; } } Caller::Guest { .. } => { @@ -2317,6 +2312,7 @@ impl Instance { storage[0] = MaybeUninit::new(result); } + log::trace!("lowered sync result for {guest_task:?}"); Waitable::Guest(guest_task).delete_from(state)?; } else { // This means the callee failed to call either `task.return` or @@ -3329,6 +3325,34 @@ impl Instance { ) -> &'a mut ConcurrentState { self.id().get_mut(store).concurrent_state_mut() } + + fn delete_task_if_all_threads_exited( + &self, + store: &mut StoreOpaque, + task: TableId, + ) -> Result { + let state = self.concurrent_state_mut(store); + let guest_task = state.get_mut(task)?; + // We consider all threads to have exited if every thread that remains + // (which may be zero) is suspended, because these threads will never be + // resumed again. + let all_threads_exited = guest_task + .threads + .keys() + .all(|thread| guest_task.suspended_threads.contains_key(thread)); + if all_threads_exited { + log::trace!("deleting guest task {task:?} as all threads exited"); + // TODO: delete any fibers + Waitable::Guest(task).delete_from(state)?; + Ok(true) + } else { + log::trace!( + "not deleting guest task {task:?} as {} threads remain", + guest_task.threads.len() + ); + Ok(false) + } + } } /// Trait representing component model ABI async intrinsics and fused adapter @@ -3926,6 +3950,12 @@ struct InstanceGuestThreadIndex { thread: GuestThreadIndex, } +impl InstanceGuestThreadIndex { + fn is_explicit_thread(&self) -> bool { + self.thread != MAIN_GUEST_THREAD_INDEX + } +} + impl GuestThread { fn new(index: GuestThreadIndex) -> Self { Self { @@ -4057,6 +4087,12 @@ impl GuestTask { thread_index } + fn thread_completed(&mut self, thread_index: GuestThreadIndex) { + let present = self.threads.remove(&thread_index).is_some(); + assert!(present); + self.suspended_threads.remove(&thread_index); + } + /// Dispose of this guest task, reparenting any pending subtasks to the /// caller. fn dispose(self, state: &mut ConcurrentState, me: TableId) -> Result<()> { diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 723c55a9b25e..e5d318ca5ce2 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -1,11 +1,11 @@ //! Implementation of string transcoding required by the component model. use crate::component::Instance; +#[cfg(feature = "component-model-async")] use crate::component::concurrent::GuestThreadIndex; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; -use crate::runtime::component::concurrent::table::TableId; use crate::runtime::vm::component::{ComponentInstance, VMComponentContext}; use crate::runtime::vm::{HostResultHasUnwindSentinel, VMStore, VmSafe}; use core::cell::Cell; diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs index eb2d9f1e0e4f..48a03fbfe8d2 100644 --- a/tests/all/component_model/threading.rs +++ b/tests/all/component_model/threading.rs @@ -40,10 +40,7 @@ async fn threads() -> Result<()> { ;; The thread entry point, which sets the global to the value passed in (func $thread-start (param i32) local.get 0 - global.set $g - i32.const 0 - call $thread-switch-to - drop) + global.set $g) (export "thread-start" (func $thread-start)) ;; Initialize the function table with our thread-start function; this will be @@ -56,7 +53,7 @@ async fn threads() -> Result<()> { i32.const 0 i32.const 42 call $thread-new-indirect - call $thread-switch-to + call $thread-yield-to drop global.get $g)) @@ -96,7 +93,7 @@ async fn threads() -> Result<()> { .instantiate_async(&mut store, &component) .await?; let func = instance.get_typed_func::<(), (u32,)>(&mut store, "run")?; - assert_eq!(func.call_async(&mut store, ()).await?, (1,)); + assert_eq!(func.call_async(&mut store, ()).await?, (42,)); Ok(()) } From 4fb67b3ee9f135f719cf1ad482c1ced4bc7e59a3 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 30 Sep 2025 10:39:10 +0100 Subject: [PATCH 05/13] Fix merge --- Cargo.lock | 1 + cranelift/codegen/src/machinst/abi.rs | 3 ++ crates/cli-flags/src/lib.rs | 8 ++++ crates/cranelift/src/compiler/component.rs | 28 ++++--------- crates/environ/src/component.rs | 6 +-- crates/environ/src/component/dfg.rs | 39 +++++++++---------- crates/environ/src/component/info.rs | 6 +-- crates/environ/src/component/translate.rs | 8 +--- .../environ/src/component/translate/inline.rs | 25 ++---------- crates/environ/src/trap_encoding.rs | 6 --- crates/fuzzing/src/generators/config.rs | 18 ++++++--- crates/fuzzing/src/generators/module.rs | 4 ++ .../tests/scenario/util.rs | 2 + .../misc/component-async-tests/wit/test.wit | 4 ++ crates/test-util/src/wasmtime_wast.rs | 6 +++ crates/test-util/src/wast.rs | 2 + crates/wasmtime/src/config.rs | 25 ++++++++++++ .../src/runtime/component/concurrent.rs | 23 +++++++---- .../src/runtime/vm/component/libcalls.rs | 4 +- tests/all/component_model/func.rs | 2 + tests/all/component_model/threading.rs | 3 +- tests/all/pulley.rs | 2 + .../component-model-async/fused.wast | 1 + .../component-model-async/futures.wast | 1 + .../component-model-async/lift.wast | 1 + .../component-model-async/streams.wast | 1 + .../component-model-async/task-builtins.wast | 1 + tests/wasi_testsuite/wasi-common | 2 +- 28 files changed, 131 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78b495f531d2..b3ba92fb883b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4457,6 +4457,7 @@ dependencies = [ "target-lexicon", "tempfile", "tokio", + "tracing-subscriber", "wasm-encoder", "wasm-wave", "wasmparser 0.239.0", diff --git a/cranelift/codegen/src/machinst/abi.rs b/cranelift/codegen/src/machinst/abi.rs index cc13a6d49db9..639da756ab3f 100644 --- a/cranelift/codegen/src/machinst/abi.rs +++ b/cranelift/codegen/src/machinst/abi.rs @@ -1791,6 +1791,9 @@ impl Callee { let mut uses: CallArgList = smallvec![]; let mut insts = smallvec![]; + if args.len() != sigs.num_args(sig) { + println!("MISMATCH: {}", self.signature()); + } assert_eq!(args.len(), sigs.num_args(sig)); let call_conv = sigs[sig].call_conv; diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index a12698c8af1f..243d07c9df09 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -375,6 +375,12 @@ wasmtime_option_group! { pub component_model: Option, /// Component model support for async lifting/lowering. pub component_model_async: Option, + /// Component model support for async lifting/lowering: this corresponds + /// to the ๐Ÿš emoji in the component model specification. + pub component_model_async_builtins: Option, + /// Component model support for async lifting/lowering: this corresponds + /// to the ๐ŸšŸ emoji in the component model specification. + pub component_model_async_stackful: Option, /// Component model support for threading: this corresponds /// to the ๐Ÿงต emoji in the component model specification. pub component_model_threading: Option, @@ -1047,6 +1053,8 @@ impl CommonOptions { handle_conditionally_compiled! { ("component-model", component_model, wasm_component_model) ("component-model-async", component_model_async, wasm_component_model_async) + ("component-model-async", component_model_async_builtins, wasm_component_model_async_builtins) + ("component-model-async", component_model_async_stackful, wasm_component_model_async_stackful) ("component-model-async", component_model_threading, wasm_component_model_threading) ("component-model", component_model_error_context, wasm_component_model_error_context) ("threads", threads, wasm_threads) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index f251944158a3..d93ecf85eb94 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -296,11 +296,7 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::WaitableSetWait { - instance, - options, - cancellable, - } => { + Trampoline::WaitableSetWait { instance, options } => { self.translate_libcall( host::waitable_set_wait, TrapSentinel::NegativeOne, @@ -308,19 +304,10 @@ impl<'a> TrampolineCompiler<'a> { |me, params| { params.push(me.index_value(*instance)); params.push(me.index_value(*options)); - params.push( - me.builder - .ins() - .iconst(ir::types::I8, i64::from(*cancellable)), - ); }, ); } - Trampoline::WaitableSetPoll { - instance, - options, - cancellable, - } => { + Trampoline::WaitableSetPoll { instance, options } => { self.translate_libcall( host::waitable_set_poll, TrapSentinel::NegativeOne, @@ -328,11 +315,6 @@ impl<'a> TrampolineCompiler<'a> { |me, params| { params.push(me.index_value(*instance)); params.push(me.index_value(*options)); - params.push( - me.builder - .ins() - .iconst(ir::types::I8, i64::from(*cancellable)), - ); }, ); } @@ -865,12 +847,16 @@ impl<'a> TrampolineCompiler<'a> { |_, _| {}, ); } - Trampoline::ThreadYieldTo { cancellable } => { + Trampoline::ThreadYieldTo { + instance, + cancellable, + } => { self.translate_libcall( host::thread_yield_to, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { + params.push(me.index_value(*instance)); params.push( me.builder .ins() diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index ecb380eb956d..81f028f6f512 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -108,9 +108,9 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] waitable_set_new(vmctx: vmctx, caller_instance: u32) -> u64; #[cfg(feature = "component-model-async")] - waitable_set_wait(vmctx: vmctx, caller_instance: u32, options: u32, cancellable: u8, set: u32, payload: u32) -> u64; + waitable_set_wait(vmctx: vmctx, caller_instance: u32, options: u32, set: u32, payload: u32) -> u64; #[cfg(feature = "component-model-async")] - waitable_set_poll(vmctx: vmctx, caller_instance: u32, options: u32, cancellable: u8, set: u32, payload: u32) -> u64; + waitable_set_poll(vmctx: vmctx, caller_instance: u32, options: u32, set: u32, payload: u32) -> u64; #[cfg(feature = "component-model-async")] waitable_set_drop(vmctx: vmctx, caller_instance: u32, set: u32) -> bool; #[cfg(feature = "component-model-async")] @@ -198,7 +198,7 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; #[cfg(feature = "component-model-async")] - thread_yield_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u32; + thread_yield_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; trap(vmctx: vmctx, code: u8) -> bool; diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 42af2900e392..138e1d3a10a7 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -354,12 +354,10 @@ pub enum Trampoline { WaitableSetWait { instance: RuntimeComponentInstanceIndex, options: OptionsId, - cancellable: bool, }, WaitableSetPoll { instance: RuntimeComponentInstanceIndex, options: OptionsId, - cancellable: bool, }, WaitableSetDrop { instance: RuntimeComponentInstanceIndex, @@ -494,6 +492,7 @@ pub enum Trampoline { }, ThreadResumeLater, ThreadYieldTo { + instance: RuntimeComponentInstanceIndex, cancellable: bool, }, } @@ -961,24 +960,18 @@ impl LinearizeDfg<'_> { Trampoline::WaitableSetNew { instance } => info::Trampoline::WaitableSetNew { instance: *instance, }, - Trampoline::WaitableSetWait { - instance, - options, - cancellable, - } => info::Trampoline::WaitableSetWait { - instance: *instance, - options: self.options(*options), - cancellable: *cancellable, - }, - Trampoline::WaitableSetPoll { - instance, - options, - cancellable, - } => info::Trampoline::WaitableSetPoll { - instance: *instance, - options: self.options(*options), - cancellable: *cancellable, - }, + Trampoline::WaitableSetWait { instance, options } => { + info::Trampoline::WaitableSetWait { + instance: *instance, + options: self.options(*options), + } + } + Trampoline::WaitableSetPoll { instance, options } => { + info::Trampoline::WaitableSetPoll { + instance: *instance, + options: self.options(*options), + } + } Trampoline::WaitableSetDrop { instance } => info::Trampoline::WaitableSetDrop { instance: *instance, }, @@ -1168,7 +1161,11 @@ impl LinearizeDfg<'_> { cancellable: *cancellable, }, Trampoline::ThreadResumeLater => info::Trampoline::ThreadResumeLater, - Trampoline::ThreadYieldTo { cancellable } => info::Trampoline::ThreadYieldTo { + Trampoline::ThreadYieldTo { + instance, + cancellable, + } => info::Trampoline::ThreadYieldTo { + instance: *instance, cancellable: *cancellable, }, }; diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 94401dc2a1a7..8a580142b68b 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -795,8 +795,6 @@ pub enum Trampoline { instance: RuntimeComponentInstanceIndex, /// Configuration options for this intrinsic call. options: OptionsIndex, - /// If `true`, indicates the caller instance maybe reentered. - cancellable: bool, }, /// A `waitable-set.poll` intrinsic, which checks whether any outstanding @@ -807,8 +805,6 @@ pub enum Trampoline { instance: RuntimeComponentInstanceIndex, /// Configuration options for this intrinsic call. options: OptionsIndex, - /// If `true`, indicates the caller instance maybe reentered. - cancellable: bool, }, /// A `waitable-set.drop` intrinsic. @@ -1149,6 +1145,8 @@ pub enum Trampoline { /// Intrinsic used to implement the `thread.yield-to` component model builtin. ThreadYieldTo { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, /// If `true`, indicates the caller instance maybe reentered. cancellable: bool, }, diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 295102cd40c7..f729fba676da 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -211,11 +211,9 @@ enum LocalInitializer<'data> { }, WaitableSetWait { options: LocalCanonicalOptions, - cancellable: bool, }, WaitableSetPoll { options: LocalCanonicalOptions, - cancellable: bool, }, WaitableSetDrop { func: ModuleInternedTypeIndex, @@ -901,7 +899,6 @@ impl<'a, 'data> Translator<'a, 'data> { callback: None, string_encoding: StringEncoding::Utf8, }, - cancellable, } } wasmparser::CanonicalFunction::WaitableSetPoll { @@ -923,7 +920,6 @@ impl<'a, 'data> Translator<'a, 'data> { callback: None, string_encoding: StringEncoding::Utf8, }, - cancellable, } } wasmparser::CanonicalFunction::WaitableSetDrop => { @@ -1123,14 +1119,14 @@ impl<'a, 'data> Translator<'a, 'data> { } wasmparser::CanonicalFunction::ThreadNewIndirect { func_ty_index, - table_id, + table_index, } => { let func = self.core_func_signature(core_func_index)?; core_func_index += 1; LocalInitializer::ThreadNewIndirect { func, start_func_ty: ComponentTypeIndex::from_u32(func_ty_index), - start_func_table_index: TableIndex::from_u32(table_id), + start_func_table_index: TableIndex::from_u32(table_index), } } wasmparser::CanonicalFunction::ThreadSwitchTo { cancellable } => { diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 12d33db612a4..6d2f8af3c2d9 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -745,10 +745,7 @@ impl<'a> Inliner<'a> { )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } - WaitableSetWait { - options, - cancellable, - } => { + WaitableSetWait { options } => { let func = options.core_type; let options = self.adapter_options(frame, types, options); let options = self.canonical_options(options); @@ -761,10 +758,7 @@ impl<'a> Inliner<'a> { )); frame.funcs.push((func, dfg::CoreDef::Trampoline(index))); } - WaitableSetPoll { - options, - cancellable, - } => { + WaitableSetPoll { options } => { let func = options.core_type; let options = self.adapter_options(frame, types, options); let options = self.canonical_options(options); @@ -1122,20 +1116,6 @@ impl<'a> Inliner<'a> { .push((*func, dfg::Trampoline::ThreadIndex)); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } - ContextSet { func, i } => { - let index = self - .result - .trampolines - .push((*func, dfg::Trampoline::ContextSet(*i))); - frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); - } - ThreadIndex { func } => { - let index = self - .result - .trampolines - .push((*func, dfg::Trampoline::ThreadIndex)); - frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); - } ThreadNewIndirect { func, start_func_table_index, @@ -1187,6 +1167,7 @@ impl<'a> Inliner<'a> { let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadYieldTo { + instance: frame.instance, cancellable: *cancellable, }, )); diff --git a/crates/environ/src/trap_encoding.rs b/crates/environ/src/trap_encoding.rs index 7e085f489c1b..939a6f5c515b 100644 --- a/crates/environ/src/trap_encoding.rs +++ b/crates/environ/src/trap_encoding.rs @@ -112,11 +112,6 @@ pub enum Trap { /// that all host tasks have completed and any/all host-owned stream/future /// handles have been dropped. AsyncDeadlock, - - /// When the `component-model` feature is enabled this trap represents a - /// scenario where a component instance tried to call an import or intrinsic - /// when it wasn't allowed to, e.g. from a post-return function. - CannotLeaveComponent, // if adding a variant here be sure to update the `check!` macro below } @@ -195,7 +190,6 @@ impl fmt::Display for Trap { ContinuationAlreadyConsumed => "continuation already consumed", DisabledOpcode => "pulley opcode disabled at compile time was executed", AsyncDeadlock => "deadlock detected: event loop cannot make further progress", - CannotLeaveComponent => "cannot leave component instance", }; write!(f, "wasm trap: {desc}") } diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index f58f6f0dbf0b..63cced7174ba 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -135,7 +135,9 @@ impl Config { extended_const, wide_arithmetic, component_model_async, - component_model_threading + component_model_async_builtins, + component_model_async_stackful, + component_model_threading, component_model_error_context, component_model_gc, simd, @@ -154,8 +156,11 @@ impl Config { self.module_config.function_references_enabled = function_references.or(gc).unwrap_or(false); self.module_config.component_model_async = component_model_async.unwrap_or(false); - self.module_config.component_model_threading = - component_model_threading.unwrap_or(false); + self.module_config.component_model_async_builtins = + component_model_async_builtins.unwrap_or(false); + self.module_config.component_model_async_stackful = + component_model_async_stackful.unwrap_or(false); + self.module_config.component_model_threading = component_model_threading.unwrap_or(false); self.module_config.component_model_error_context = component_model_error_context.unwrap_or(false); self.module_config.component_model_gc = component_model_gc.unwrap_or(false); @@ -276,8 +281,11 @@ impl Config { cfg.wasm.async_stack_zeroing = Some(self.wasmtime.async_stack_zeroing); cfg.wasm.bulk_memory = Some(true); cfg.wasm.component_model_async = Some(self.module_config.component_model_async); - cfg.wasm.component_model_threading = - Some(self.module_config.component_model_threading); + cfg.wasm.component_model_async_builtins = + Some(self.module_config.component_model_async_builtins); + cfg.wasm.component_model_async_stackful = + Some(self.module_config.component_model_async_stackful); + cfg.wasm.component_model_threading = Some(self.module_config.component_model_threading); cfg.wasm.component_model_error_context = Some(self.module_config.component_model_error_context); cfg.wasm.component_model_gc = Some(self.module_config.component_model_gc); diff --git a/crates/fuzzing/src/generators/module.rs b/crates/fuzzing/src/generators/module.rs index 0606c8e5a47a..942ff9fad882 100644 --- a/crates/fuzzing/src/generators/module.rs +++ b/crates/fuzzing/src/generators/module.rs @@ -17,6 +17,8 @@ pub struct ModuleConfig { // config-to-`wasmtime::Config` translation. pub function_references_enabled: bool, pub component_model_async: bool, + pub component_model_async_builtins: bool, + pub component_model_async_stackful: bool, pub component_model_threading: bool, pub component_model_error_context: bool, pub component_model_gc: bool, @@ -67,6 +69,8 @@ impl<'a> Arbitrary<'a> for ModuleConfig { Ok(ModuleConfig { component_model_async: false, + component_model_async_builtins: false, + component_model_async_stackful: false, component_model_threading: false, component_model_error_context: false, component_model_gc: false, diff --git a/crates/misc/component-async-tests/tests/scenario/util.rs b/crates/misc/component-async-tests/tests/scenario/util.rs index 3c7de70de78c..780f34531193 100644 --- a/crates/misc/component-async-tests/tests/scenario/util.rs +++ b/crates/misc/component-async-tests/tests/scenario/util.rs @@ -35,6 +35,8 @@ pub fn config() -> Config { } config.wasm_component_model(true); config.wasm_component_model_async(true); + config.wasm_component_model_async_builtins(true); + config.wasm_component_model_async_stackful(true); config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config.async_support(true); diff --git a/crates/misc/component-async-tests/wit/test.wit b/crates/misc/component-async-tests/wit/test.wit index 13e76bbd8fcd..a949a64d9ec3 100644 --- a/crates/misc/component-async-tests/wit/test.wit +++ b/crates/misc/component-async-tests/wit/test.wit @@ -326,6 +326,10 @@ world intertask-communication { export run; } +world readiness-guest { + export readiness; +} + world synchronous-transmit-guest { export synchronous-transmit; } diff --git a/crates/test-util/src/wasmtime_wast.rs b/crates/test-util/src/wasmtime_wast.rs index d6e82c057305..e4f9a0497103 100644 --- a/crates/test-util/src/wasmtime_wast.rs +++ b/crates/test-util/src/wasmtime_wast.rs @@ -38,6 +38,8 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { extended_const, wide_arithmetic, component_model_async, + component_model_async_builtins, + component_model_async_stackful, component_model_threading, component_model_error_context, component_model_gc, @@ -64,6 +66,8 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { let extended_const = extended_const.unwrap_or(false); let wide_arithmetic = wide_arithmetic.unwrap_or(false); let component_model_async = component_model_async.unwrap_or(false); + let component_model_async_builtins = component_model_async_builtins.unwrap_or(false); + let component_model_async_stackful = component_model_async_stackful.unwrap_or(false); let component_model_threading = component_model_threading.unwrap_or(false); let component_model_error_context = component_model_error_context.unwrap_or(false); let component_model_gc = component_model_gc.unwrap_or(false); @@ -98,6 +102,8 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { .wasm_extended_const(extended_const) .wasm_wide_arithmetic(wide_arithmetic) .wasm_component_model_async(component_model_async) + .wasm_component_model_async_builtins(component_model_async_builtins) + .wasm_component_model_async_stackful(component_model_async_stackful) .wasm_component_model_threading(component_model_threading) .wasm_component_model_error_context(component_model_error_context) .wasm_component_model_gc(component_model_gc) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index 8d40cd918910..cefa50ac27ba 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -226,6 +226,8 @@ macro_rules! foreach_config_option { hogs_memory nan_canonicalization component_model_async + component_model_async_builtins + component_model_async_stackful component_model_threading component_model_error_context component_model_gc diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 3e6f34adda58..bcb20f3a36a4 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1137,6 +1137,31 @@ impl Config { self } + /// This corresponds to the ๐Ÿš emoji in the component model specification. + /// + /// Please note that Wasmtime's support for this feature is _very_ + /// incomplete. + /// + /// [proposal]: + /// https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md + #[cfg(feature = "component-model-async")] + pub fn wasm_component_model_async_builtins(&mut self, enable: bool) -> &mut Self { + self.wasm_feature(WasmFeatures::CM_ASYNC_BUILTINS, enable); + self + } + + /// This corresponds to the ๐ŸšŸ emoji in the component model specification. + /// + /// Please note that Wasmtime's support for this feature is _very_ + /// incomplete. + /// + /// [proposal]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md + #[cfg(feature = "component-model-async")] + pub fn wasm_component_model_async_stackful(&mut self, enable: bool) -> &mut Self { + self.wasm_feature(WasmFeatures::CM_ASYNC_STACKFUL, enable); + self + } + /// This corresponds to the ๐Ÿงต emoji in the component model specification. /// /// Please note that Wasmtime's support for this feature is _very_ diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index b0b4f3f9713c..8ffcf4982b73 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -2827,7 +2827,6 @@ impl Instance { store: &mut dyn VMStore, caller: RuntimeComponentInstanceIndex, options: OptionsIndex, - cancellable: bool, set: u32, payload: u32, ) -> Result { @@ -2855,7 +2854,6 @@ impl Instance { store: &mut dyn VMStore, caller: RuntimeComponentInstanceIndex, options: OptionsIndex, - cancellable: bool, set: u32, payload: u32, ) -> Result { @@ -2906,12 +2904,13 @@ impl Instance { pub(crate) fn thread_yield_to( self, mut store: StoreContextMut, + caller: RuntimeComponentInstanceIndex, cancellable: bool, thread: GuestThreadIndex, ) -> Result { log::trace!("thread yielding to {thread:?}"); self.resume_suspended_thread(store.as_context_mut(), thread, true)?; - self.thread_yield(store.0, cancellable) + self.thread_yield(store.0, caller, cancellable) } pub(crate) fn thread_switch_to( @@ -3357,7 +3356,7 @@ impl Instance { if async_ { return Ok(BLOCKED); } else { - self.wait_for_event(store, Waitable::Guest(guest_thread.task))?; + self.wait_for_event(store, Waitable::Guest(guest_task))?; } } } @@ -3376,11 +3375,17 @@ impl Instance { fn wait_for_event(self, store: &mut dyn VMStore, waitable: Waitable) -> Result<()> { let state = self.concurrent_state_mut(store); - let caller = state.guest_task.unwrap(); + let caller = state.guest_thread.unwrap(); let old_set = waitable.common(state)?.set; - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; waitable.join(state, Some(set))?; - self.suspend(store, SuspendReason::Waiting { set, task: caller })?; + self.suspend( + store, + SuspendReason::Waiting { + set, + thread: caller, + }, + )?; let state = self.concurrent_state_mut(store); waitable.join(state, old_set) } @@ -3631,6 +3636,7 @@ pub trait VMComponentAsyncStore { fn thread_yield_to( &mut self, instance: Instance, + caller: RuntimeComponentInstanceIndex, cancellable: bool, thread: GuestThreadIndex, ) -> Result; @@ -3945,10 +3951,11 @@ impl VMComponentAsyncStore for StoreInner { fn thread_yield_to( &mut self, instance: Instance, + caller: RuntimeComponentInstanceIndex, cancellable: bool, thread: GuestThreadIndex, ) -> Result { - instance.thread_yield_to(StoreContextMut(self), cancellable, thread) + instance.thread_yield_to(StoreContextMut(self), caller, cancellable, thread) } fn thread_switch_to( diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 14b1298186ac..c444fb18b5a9 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -745,7 +745,6 @@ fn waitable_set_wait( instance: Instance, caller: u32, options: u32, - cancellable: u8, set: u32, payload: u32, ) -> Result { @@ -764,7 +763,6 @@ fn waitable_set_poll( instance: Instance, caller: u32, options: u32, - cancellable: u8, set: u32, payload: u32, ) -> Result { @@ -1419,11 +1417,13 @@ fn thread_resume_later(store: &mut dyn VMStore, instance: Instance, thread_idx: fn thread_yield_to( store: &mut dyn VMStore, instance: Instance, + caller_instance: u32, cancellable: u8, thread_idx: u32, ) -> Result { store.component_async_store().thread_yield_to( instance, + RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, GuestThreadIndex::from_u32(thread_idx), ) diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index a9879af0436f..55d0b49cf7fe 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -910,6 +910,7 @@ async fn async_reentrance() -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); + config.wasm_component_model_async_stackful(true); config.async_support(true); let engine = &Engine::new(&config)?; let component = Component::new(&engine, component)?; @@ -1039,6 +1040,7 @@ async fn task_return_string_encoding_mismatch() -> Result<()> { async fn task_return_trap(component: &str, substring: &str) -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); + config.wasm_component_model_async_stackful(true); config.wasm_component_model_threading(true); config.async_support(true); let engine = &Engine::new(&config)?; diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs index 48a03fbfe8d2..30d46f87d6cd 100644 --- a/tests/all/component_model/threading.rs +++ b/tests/all/component_model/threading.rs @@ -10,8 +10,7 @@ async fn threads() -> Result<()> { let builder = FmtSubscriber::builder() .with_writer(std::io::stderr) .with_env_filter(EnvFilter::from_env("WASMTIME_LOG")) - .with_ansi(std::io::stderr().is_terminal()) - .init(); + .with_ansi(std::io::stderr().is_terminal()); let mut config = Config::new(); config.async_support(true); config.wasm_component_model_async(true); diff --git a/tests/all/pulley.rs b/tests/all/pulley.rs index 76ea951c6d2a..aa656f41ac2c 100644 --- a/tests/all/pulley.rs +++ b/tests/all/pulley.rs @@ -85,6 +85,8 @@ fn provenance_test_config() -> Config { config.memory_guard_size(0); config.signals_based_traps(false); config.wasm_component_model_async(true); + config.wasm_component_model_async_builtins(true); + config.wasm_component_model_async_stackful(true); config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config diff --git a/tests/misc_testsuite/component-model-async/fused.wast b/tests/misc_testsuite/component-model-async/fused.wast index 4979b39d1b9d..3ced2e1ed228 100644 --- a/tests/misc_testsuite/component-model-async/fused.wast +++ b/tests/misc_testsuite/component-model-async/fused.wast @@ -1,4 +1,5 @@ ;;! component_model_async = true +;;! component_model_async_stackful = true ;;! component_model_threading = true ;;! reference_types = true ;;! gc_types = true diff --git a/tests/misc_testsuite/component-model-async/futures.wast b/tests/misc_testsuite/component-model-async/futures.wast index 8e7ce277acac..062b53a55dcc 100644 --- a/tests/misc_testsuite/component-model-async/futures.wast +++ b/tests/misc_testsuite/component-model-async/futures.wast @@ -1,4 +1,5 @@ ;;! component_model_async = true +;;! component_model_async_builtins = true ;;! component_model_threading = true ;; future.new diff --git a/tests/misc_testsuite/component-model-async/lift.wast b/tests/misc_testsuite/component-model-async/lift.wast index 7088ed435b10..d7682a33a7ca 100644 --- a/tests/misc_testsuite/component-model-async/lift.wast +++ b/tests/misc_testsuite/component-model-async/lift.wast @@ -1,4 +1,5 @@ ;;! component_model_async = true +;;! component_model_async_stackful = true ;;! component_model_threading = true ;; async lift; no callback diff --git a/tests/misc_testsuite/component-model-async/streams.wast b/tests/misc_testsuite/component-model-async/streams.wast index 6efadded4993..3e3f500642f1 100644 --- a/tests/misc_testsuite/component-model-async/streams.wast +++ b/tests/misc_testsuite/component-model-async/streams.wast @@ -1,4 +1,5 @@ ;;! component_model_async = true +;;! component_model_async_builtins = true ;;! component_model_threading = true ;; stream.new diff --git a/tests/misc_testsuite/component-model-async/task-builtins.wast b/tests/misc_testsuite/component-model-async/task-builtins.wast index 6ed9a8c87c1b..fa327b08a387 100644 --- a/tests/misc_testsuite/component-model-async/task-builtins.wast +++ b/tests/misc_testsuite/component-model-async/task-builtins.wast @@ -1,4 +1,5 @@ ;;! component_model_async = true +;;! component_model_async_stackful = true ;;! component_model_threading = true ;; backpressure.set diff --git a/tests/wasi_testsuite/wasi-common b/tests/wasi_testsuite/wasi-common index c11cd6bbee3e..2fec29ea6de1 160000 --- a/tests/wasi_testsuite/wasi-common +++ b/tests/wasi_testsuite/wasi-common @@ -1 +1 @@ -Subproject commit c11cd6bbee3e209431415262f9701e42e9fe050a +Subproject commit 2fec29ea6de1244c124f7fe3bfe9f2946113f66e From aaa1810a56efa43030ef39c1faf75d5a93cbb599 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 30 Sep 2025 12:41:54 +0100 Subject: [PATCH 06/13] Cancellation and suspension refactoring --- crates/cranelift/src/compiler/component.rs | 12 +- crates/environ/src/component.rs | 4 +- crates/environ/src/component/dfg.rs | 14 +- crates/environ/src/component/info.rs | 4 + .../environ/src/component/translate/inline.rs | 2 + .../src/runtime/component/concurrent.rs | 156 +++++++----------- .../src/runtime/vm/component/libcalls.rs | 33 +++- 7 files changed, 112 insertions(+), 113 deletions(-) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index d93ecf85eb94..a3f65989823d 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -811,12 +811,16 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::ThreadSwitchTo { cancellable } => { + Trampoline::ThreadSwitchTo { + instance, + cancellable, + } => { self.translate_libcall( host::thread_switch_to, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { + params.push(me.index_value(*instance)); params.push( me.builder .ins() @@ -825,12 +829,16 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::ThreadSuspend { cancellable } => { + Trampoline::ThreadSuspend { + instance, + cancellable, + } => { self.translate_libcall( host::thread_suspend, TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { + params.push(me.index_value(*instance)); params.push( me.builder .ins() diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 81f028f6f512..70008d85f9e4 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -192,9 +192,9 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] thread_new_indirect(vmctx: vmctx, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; #[cfg(feature = "component-model-async")] - thread_switch_to(vmctx: vmctx, cancellable: u8, thread_idx: u32) -> u32; + thread_switch_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; #[cfg(feature = "component-model-async")] - thread_suspend(vmctx: vmctx, cancellable: u8) -> u32; + thread_suspend(vmctx: vmctx, caller_instance: u32, cancellable: u8) -> u32; #[cfg(feature = "component-model-async")] thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; #[cfg(feature = "component-model-async")] diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 138e1d3a10a7..7095ede562c0 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -485,9 +485,11 @@ pub enum Trampoline { start_func_table_id: TableId, }, ThreadSwitchTo { + instance: RuntimeComponentInstanceIndex, cancellable: bool, }, ThreadSuspend { + instance: RuntimeComponentInstanceIndex, cancellable: bool, }, ThreadResumeLater, @@ -1154,10 +1156,18 @@ impl LinearizeDfg<'_> { start_func_ty_idx: *start_func_ty_idx, start_func_table_idx: self.runtime_table(*start_func_table_id), }, - Trampoline::ThreadSwitchTo { cancellable } => info::Trampoline::ThreadSwitchTo { + Trampoline::ThreadSwitchTo { + instance, + cancellable, + } => info::Trampoline::ThreadSwitchTo { + instance: *instance, cancellable: *cancellable, }, - Trampoline::ThreadSuspend { cancellable } => info::Trampoline::ThreadSuspend { + Trampoline::ThreadSuspend { + instance, + cancellable, + } => info::Trampoline::ThreadSuspend { + instance: *instance, cancellable: *cancellable, }, Trampoline::ThreadResumeLater => info::Trampoline::ThreadResumeLater, diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 8a580142b68b..80639e9a044f 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -1130,12 +1130,16 @@ pub enum Trampoline { /// Intrinsic used to implement the `thread.switch-to` component model builtin. ThreadSwitchTo { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, /// If `true`, indicates the caller instance maybe reentered. cancellable: bool, }, /// Intrinsic used to implement the `thread.suspend` component model builtin. ThreadSuspend { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, /// If `true`, indicates the caller instance maybe reentered. cancellable: bool, }, diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 6d2f8af3c2d9..34c1c9a48cea 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1142,6 +1142,7 @@ impl<'a> Inliner<'a> { let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadSwitchTo { + instance: frame.instance, cancellable: *cancellable, }, )); @@ -1151,6 +1152,7 @@ impl<'a> Inliner<'a> { let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadSuspend { + instance: frame.instance, cancellable: *cancellable, }, )); diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 8ffcf4982b73..280081f3d320 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -2875,63 +2875,6 @@ impl Instance { ) } - /// Implements the `thread.yield` intrinsic. - pub(crate) fn thread_yield( - self, - store: &mut dyn VMStore, - caller: RuntimeComponentInstanceIndex, - cancellable: bool, - ) -> Result { - self.id().get(store).check_may_leave(caller)?; - self.waitable_check(store, cancellable, WaitableCheck::Yield) - .map(|_| { - if cancellable { - let state = self.concurrent_state_mut(store); - let thread = state.guest_thread.unwrap(); - if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { - assert!(matches!(event, Event::Cancelled)); - true - } else { - false - } - } else { - false - } - }) - } - - /// Implements the `thread.yield-to` intrinsic. - pub(crate) fn thread_yield_to( - self, - mut store: StoreContextMut, - caller: RuntimeComponentInstanceIndex, - cancellable: bool, - thread: GuestThreadIndex, - ) -> Result { - log::trace!("thread yielding to {thread:?}"); - self.resume_suspended_thread(store.as_context_mut(), thread, true)?; - self.thread_yield(store.0, caller, cancellable) - } - - pub(crate) fn thread_switch_to( - self, - mut store: StoreContextMut, - _cancellable: bool, - thread: GuestThreadIndex, - ) -> Result { - let state = self.concurrent_state_mut(store.0); - let current_thread = state.guest_thread.unwrap(); - self.resume_suspended_thread(store.as_context_mut(), thread, true)?; - self.suspend( - store.0, - SuspendReason::ExplicitlySuspending { - thread: current_thread, - to: Some(thread), - }, - )?; - Ok(true) - } - unsafe fn read_funcref_from_table( self, store: &mut dyn VMStore, @@ -3114,25 +3057,52 @@ impl Instance { Ok(()) } - pub(crate) fn thread_suspend(self, store: &mut dyn VMStore, cancellable: bool) -> Result { - if cancellable { - bail!("todo: cancellable `thread.suspend` not implemented yet"); + /// Helper function for the `thread.yield`, `thread.yield-to`, `thread.suspend`, + /// and `thread.switch-to` intrinsics. + pub(crate) fn suspension_intrinsic( + self, + mut store: StoreContextMut, + caller: RuntimeComponentInstanceIndex, + cancellable: bool, + yielding: bool, + to_thread: Option, + ) -> Result { + self.id().get(store.0).check_may_leave(caller)?; + + if let Some(thread) = to_thread { + self.resume_suspended_thread(store.as_context_mut(), thread, true)?; } - let guest_thread = self.concurrent_state_mut(store).guest_thread.unwrap(); - self.suspend( - store, + let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); + let reason = if yielding { + SuspendReason::Yielding { + thread: guest_thread, + to: to_thread, + } + } else { SuspendReason::ExplicitlySuspending { thread: guest_thread, - to: None, - }, - )?; + to: to_thread, + } + }; + + self.suspend(store.0, reason)?; - Ok(true) + if cancellable { + let state = self.concurrent_state_mut(store.0); + let thread = state.guest_thread.unwrap(); + if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } } - /// Helper function for the `waitable-set.wait`, `waitable-set.poll`, and - /// `thread.yield` intrinsics. + /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. fn waitable_check( self, store: &mut dyn VMStore, @@ -3144,7 +3114,6 @@ impl Instance { let (wait, set) = match &check { WaitableCheck::Wait(params) => (true, Some(params.set)), WaitableCheck::Poll(params) => (false, Some(params.set)), - WaitableCheck::Yield => (false, None), }; // First, suspend this fiber, allowing any other threads to run. @@ -3233,7 +3202,6 @@ impl Instance { options.memory_mut(store)[ptr + 4..][..4].copy_from_slice(&result.to_le_bytes()); Ok(ordinal) } - WaitableCheck::Yield => Ok(0), }; result @@ -3629,25 +3597,18 @@ pub trait VMComponentAsyncStore { debug_msg_address: u32, ) -> Result<()>; - /// The `thread.resume-later` intrinsic. - fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()>; - - /// The `thread.yield-to` intrinsic. - fn thread_yield_to( + /// The `thread.yield`, `thread.yield-to`, `thread.suspend`, and `thread.switch-to` intrinsics. + fn suspension_intrinsic( &mut self, instance: Instance, caller: RuntimeComponentInstanceIndex, cancellable: bool, - thread: GuestThreadIndex, + yielding: bool, + to_thread: Option, ) -> Result; - /// The `thread.switch-to` intrinsic. - fn thread_switch_to( - &mut self, - instance: Instance, - cancellable: bool, - thread: GuestThreadIndex, - ) -> Result; + /// The `thread.resume-later` intrinsic. + fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()>; } /// SAFETY: See trait docs. @@ -3944,27 +3905,25 @@ impl VMComponentAsyncStore for StoreInner { ) } - fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()> { - instance.resume_suspended_thread(StoreContextMut(self), thread, false) - } - - fn thread_yield_to( + fn suspension_intrinsic( &mut self, instance: Instance, caller: RuntimeComponentInstanceIndex, cancellable: bool, - thread: GuestThreadIndex, + yielding: bool, + to_thread: Option, ) -> Result { - instance.thread_yield_to(StoreContextMut(self), caller, cancellable, thread) + instance.suspension_intrinsic( + StoreContextMut(self), + caller, + cancellable, + yielding, + to_thread, + ) } - fn thread_switch_to( - &mut self, - instance: Instance, - cancellable: bool, - thread: GuestThreadIndex, - ) -> Result { - instance.thread_switch_to(StoreContextMut(self), cancellable, thread) + fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()> { + instance.resume_suspended_thread(StoreContextMut(self), thread, false) } } @@ -4997,7 +4956,6 @@ struct WaitableCheckParams { enum WaitableCheck { Wait(WaitableCheckParams), Poll(WaitableCheckParams), - Yield, } /// Represents a guest task called from the host, prepared using `prepare_call`. diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index c444fb18b5a9..2ccb3200f8bb 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -810,10 +810,12 @@ fn thread_yield( caller_instance: u32, cancellable: u8, ) -> Result { - instance.thread_yield( - store, + store.component_async_store().suspension_intrinsic( + instance, RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, + true, + None, ) } @@ -1391,19 +1393,33 @@ fn thread_new_indirect( fn thread_switch_to( store: &mut dyn VMStore, instance: Instance, + caller: u32, cancellable: u8, thread_idx: u32, ) -> Result { - store.component_async_store().thread_switch_to( + store.component_async_store().suspension_intrinsic( instance, + RuntimeComponentInstanceIndex::from_u32(caller), cancellable != 0, - GuestThreadIndex::from_u32(thread_idx), + false, + Some(GuestThreadIndex::from_u32(thread_idx)), ) } #[cfg(feature = "component-model-async")] -fn thread_suspend(store: &mut dyn VMStore, instance: Instance, cancellable: u8) -> Result { - instance.thread_suspend(store, cancellable != 0) +fn thread_suspend( + store: &mut dyn VMStore, + instance: Instance, + caller: u32, + cancellable: u8, +) -> Result { + store.component_async_store().suspension_intrinsic( + instance, + RuntimeComponentInstanceIndex::from_u32(caller), + cancellable != 0, + false, + None, + ) } #[cfg(feature = "component-model-async")] @@ -1421,10 +1437,11 @@ fn thread_yield_to( cancellable: u8, thread_idx: u32, ) -> Result { - store.component_async_store().thread_yield_to( + store.component_async_store().suspension_intrinsic( instance, RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, - GuestThreadIndex::from_u32(thread_idx), + true, + Some(GuestThreadIndex::from_u32(thread_idx)), ) } From 5a11d9187b96df80977475458e7d35355e4bb2b6 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 30 Sep 2025 12:44:10 +0100 Subject: [PATCH 07/13] Remove printlns --- cranelift/codegen/src/machinst/abi.rs | 3 --- crates/wasmtime/src/runtime/component/instance.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/cranelift/codegen/src/machinst/abi.rs b/cranelift/codegen/src/machinst/abi.rs index 639da756ab3f..cc13a6d49db9 100644 --- a/cranelift/codegen/src/machinst/abi.rs +++ b/cranelift/codegen/src/machinst/abi.rs @@ -1791,9 +1791,6 @@ impl Callee { let mut uses: CallArgList = smallvec![]; let mut insts = smallvec![]; - if args.len() != sigs.num_args(sig) { - println!("MISMATCH: {}", self.signature()); - } assert_eq!(args.len(), sigs.num_args(sig)); let call_conv = sigs[sig].call_conv; diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 067c330734c7..a7aa6f162406 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -815,7 +815,6 @@ impl<'a> Instantiator<'a> { } fn extract_table(&mut self, store: &mut StoreOpaque, table: &ExtractTable) { - println!("extract_table: {:?}", table); let export = match lookup_vmexport(store, self.id, &table.export) { crate::runtime::vm::Export::Table(t) => t, _ => unreachable!(), From 08de34ca45ce7ecf8227e2675a517ec387559177 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 1 Oct 2025 12:41:02 +0100 Subject: [PATCH 08/13] Test with several threads --- .../many-threads-indexed.wast | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/misc_testsuite/component-model-threading/many-threads-indexed.wast diff --git a/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast b/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast new file mode 100644 index 000000000000..852b7f00c982 --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast @@ -0,0 +1,126 @@ +;;! component_model_async = true +;;! component_model_threading = true + +;; Spawns 5 threads, makes them write their indices into a buffer out-of-order, then yields to them in-order, +;; ensuring that the yield order is as-expected. + +;; More concretely: +;; Thread Index | Assigned Number +;; 1 | 16 +;; 2 | 12 +;; 3 | 04 +;; 4 | 00 +;; 5 | 08 + +;; After all threads have spawned and written their indices to the byte position given by their assigned number, +;; the buffer state will be: +;; 4 3 5 2 1 + +;; The main thread will then yield to these threads in the order that they are stored in the buffer, +;; and they will write their assigned number into the buffer, after the indices. +;; After all threads have been yielded to, the buffer contents will be: +;; 4 3 5 2 1 0 4 8 12 16 + +;; The main thread then ensures that the assigned numbers have been written into the correct locations. + +(component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A memory block that threads will write their thread indexes and assigned values into + (memory 1) + + ;; A global that points to the next memory index to write into + ;; We initialize this to 20 (threads * 4 bytes of storage per thread) + (global $g (mut i32) (i32.const 20)) + + ;; The thread entry point, which writes the thread's index into memory at the assigned location, + ;; suspends back to the main thread, then writes the assigned value into memory + (func $thread-start (param i32) + ;; Store the thread index into the assigned location + (i32.store (local.get 0) (call $thread-index)) + (drop (call $thread-suspend)) + (i32.store (global.get $g) (local.get 0)) + (global.set $g + (i32.add (global.get $g) (i32.const 4)))) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + (func $new-thread (param i32) + (drop + (call $thread-yield-to + (call $thread-new-indirect (i32.const 0) (local.get 0))))) + + ;; The main entry point + (func (export "run") (result i32) + ;; Spawn 5 new threads with assigned numbers + (call $new-thread (i32.const 16)) + (call $new-thread (i32.const 12)) + (call $new-thread (i32.const 4)) + (call $new-thread (i32.const 0)) + (call $new-thread (i32.const 8)) + + ;; Yield to all threads in ascending order of assigned number + (drop (call $thread-yield-to (i32.load (i32.const 0)))) + (drop (call $thread-yield-to (i32.load (i32.const 4)))) + (drop (call $thread-yield-to (i32.load (i32.const 8)))) + (drop (call $thread-yield-to (i32.load (i32.const 12)))) + (drop (call $thread-yield-to (i32.load (i32.const 16)))) + + ;; Ensure all assigned numbers have been written to the buffer in order + (if (i32.ne (i32.load (i32.const 20)) (i32.const 0)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 4)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 28)) (i32.const 8)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 32)) (i32.const 12)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 36)) (i32.const 16)) (then unreachable)) + + ;; Sentinel value + (i32.const 42))) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From 3b4790e48516e100d59e76445dfe51fc7a826f76 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 1 Oct 2025 13:10:33 +0100 Subject: [PATCH 09/13] More testing --- .../component-model-threading/test.wast | 71 ------------ .../threading-builtins.wast | 102 ++++++++++++++++++ 2 files changed, 102 insertions(+), 71 deletions(-) delete mode 100644 tests/misc_testsuite/component-model-threading/test.wast create mode 100644 tests/misc_testsuite/component-model-threading/threading-builtins.wast diff --git a/tests/misc_testsuite/component-model-threading/test.wast b/tests/misc_testsuite/component-model-threading/test.wast deleted file mode 100644 index 7f5cfda3d803..000000000000 --- a/tests/misc_testsuite/component-model-threading/test.wast +++ /dev/null @@ -1,71 +0,0 @@ -;;! component_model_async = true -;;! component_model_threading = true - -(component - ;; Defines the table for the thread start function - (core module $libc - (table (export "__indirect_function_table") 1 funcref)) - ;; Defines the thread start function and a function that calls thread.new_indirect - (core module $m - ;; Import the threading builtins and the table from libc - (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) - (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) - (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) - (import "" "thread.yield" (func $thread-yield (result i32))) - (import "" "thread.resume-later" (func $thread-resume-later (param i32))) - (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) - - ;; A global that we will set from the spawned thread - (global $g (mut i32) (i32.const 0)) - - ;; The thread entry point, which sets the global to the value passed in - (func $thread-start (param i32) - local.get 0 - global.set $g - i32.const 0 - call $thread-switch-to - drop) - (export "thread-start" (func $thread-start)) - - ;; Initialize the function table with our thread-start function; this will be - ;; used by thread.new_indirect - (elem (table $indirect-function-table) (i32.const 0) func $thread-start) - - ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 - ;; as the context value, and then yields to it - (func (export "run") (result i32) - i32.const 0 - i32.const 42 - call $thread-new-indirect - call $thread-switch-to - drop - global.get $g)) - - ;; Instantiate the libc module to get the table - (core instance $libc (instantiate $libc)) - ;; Get access to `thread.new_indirect` that uses the table from libc - (core type $start-func-ty (func (param i32))) - (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) - - (core func $thread-new-indirect - (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) - (core func $thread-yield (canon thread.yield)) - (core func $thread-yield-to (canon thread.yield-to)) - (core func $thread-resume-later (canon thread.resume-later)) - (core func $thread-switch-to (canon thread.switch-to)) - - ;; Instantiate the main module - (core instance $i ( - instantiate $m - (with "" (instance - (export "thread.new_indirect" (func $thread-new-indirect)) - (export "thread.yield-to" (func $thread-yield-to)) - (export "thread.yield" (func $thread-yield)) - (export "thread.switch-to" (func $thread-switch-to)) - (export "thread.resume-later" (func $thread-resume-later)))) - (with "libc" (instance $libc)))) - - ;; Export the main entry point - (func (export "run") (result u32) (canon lift (core func $i "run")))) - -(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file diff --git a/tests/misc_testsuite/component-model-threading/threading-builtins.wast b/tests/misc_testsuite/component-model-threading/threading-builtins.wast new file mode 100644 index 000000000000..2402f4bb505c --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/threading-builtins.wast @@ -0,0 +1,102 @@ +;;! component_model_async = true +;;! component_model_threading = true + +;; Tests for basic functioning of all threading builtins with the implicit thread + one explicit thread +;; Switches between threads using all of the different threading intrinsics. + +(component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A global that we will set from the spawned thread + (global $g (mut i32) (i32.const 0)) + (global $main-thread-index (mut i32) (i32.const 0)) + + ;; The thread entry point, which sets the global to incrementing values starting from the context value + (func $thread-start (param i32) + ;; Set the global to the context value + (global.set $g (local.get 0)) + ;; The main thread switched to us, so is no longer scheduled, so we explicitly schedule it + (call $thread-resume-later (global.get $main-thread-index)) + ;; Yield back to the main thread (since that is the only other one) + (drop (call $thread-yield) + ;; Increment the global + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; The main thread will have explicitly requested suspension, so yield to it directly + (drop (call $thread-yield-to (global.get $main-thread-index))) + ;; Increment the global again + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; Reschedule the main thread so that it runs after we exit + (call $thread-resume-later (global.get $main-thread-index)))) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 + ;; as the context value, and then yields to it + (func (export "run") (result i32) + ;; Store the main thread's index for the spawned thread to yield to + (global.set $main-thread-index (call $thread-index)) + ;; Create a new thread, which starts suspended, and switch to it + (drop + (call $thread-switch-to + (call $thread-new-indirect (i32.const 0) (i32.const 42)))) + ;; After the thread yields back to us, check that the global was set to 42 + (if (i32.ne (global.get $g) (i32.const 42)) (then unreachable)) + ;; Suspend ourselves, which will cause the spawned thread to run + (drop (call $thread-suspend)) + ;; The spawned thread will resume us after incrementing the global, so check that it is now 43 + (if (i32.ne (global.get $g) (i32.const 43)) (then unreachable)) + ;; Suspend again, which will cause the spawned thread to run again + (drop (call $thread-suspend)) + ;; The spawned thread will reschedule us before it exits, so when we resume here the global should be 44 + (if (i32.ne (global.get $g) (i32.const 44)) (then unreachable)) + ;; Return success + (i32.const 42))) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From 7d5f98d5e740fcb26694415210662ca13ec9fed2 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 3 Oct 2025 09:45:27 +0100 Subject: [PATCH 10/13] Cancellation --- .../src/runtime/component/concurrent.rs | 7 + .../stackful-cancellation.wast | 303 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 tests/misc_testsuite/component-model-threading/stackful-cancellation.wast diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 280081f3d320..c4fe150ccff9 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -605,11 +605,13 @@ enum SuspendReason { Yielding { thread: InstanceGuestThreadIndex, to: Option, + cancellable: bool, }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. ExplicitlySuspending { thread: InstanceGuestThreadIndex, to: Option, + cancellable: bool, }, } @@ -3078,11 +3080,13 @@ impl Instance { SuspendReason::Yielding { thread: guest_thread, to: to_thread, + cancellable, } } else { SuspendReason::ExplicitlySuspending { thread: guest_thread, to: to_thread, + cancellable, } }; @@ -3116,12 +3120,14 @@ impl Instance { WaitableCheck::Poll(params) => (false, Some(params.set)), }; + log::trace!("waitable check for {guest_thread:?}; set {set:?}"); // First, suspend this fiber, allowing any other threads to run. self.suspend( store, SuspendReason::Yielding { thread: guest_thread, to: None, + cancellable, }, )?; @@ -3310,6 +3316,7 @@ impl Instance { SuspendReason::Yielding { thread: caller, to: None, + cancellable: false, }, )?; break; diff --git a/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast new file mode 100644 index 000000000000..0846e3d0ba98 --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast @@ -0,0 +1,303 @@ +;;! component_model_async = true +;;! component_model_threading = true + +(component + (component $C + (type $FT (future)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + (core module $CM + ;; Import the threading builtins and the table from libc + (import "" "mem" (memory 1)) + (import "" "task.cancel" (func $task-cancel)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.suspend-cancellable" (func $thread-suspend-cancellable (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.yield-cancellable" (func $thread-yield-cancellable (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + (func (export "run-yield") + ;; Yield back to the caller, who will attempt to cancel us, but we won't see it + ;; because we're using an uncancellable yield + (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) + ;; Yield back to the caller again. This time, we should receive the cancellation immediately. + (if (i32.ne (call $thread-yield-cancellable) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + + (func $wait-for-future-write (param i32) + ;; Waitable set to wait on the future read + (local $ws i32) (local $ret i32) (local $event_code i32) + (local.set $ws (call $waitable-set.new)) + ;; Perform a future.read, which will block, waiting for the supertask to write + (local.set $ret (call $future.read (local.get 0) (i32.const 0xba5eba11))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get 0) (local.get $ws)) + + ;; Wait on $ws synchronously, don't expect cancellation + (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + ) + + (func $wake-from-suspend (param i32) + ;; Extract the thread index and future to wait on from the argument structure + (local $thread-index i32) (local $future i32) + (local.set $thread-index (i32.load offset=0 (local.get 0))) + (local.set $future (i32.load offset=4 (local.get 0))) + + ;; Wait for the supertask to signal us to wake up suspended thread. + (call $wait-for-future-write (local.get $future)) + ;; Resume the main thread, which is suspended in an uncancellable suspend + (call $thread-resume-later (local.get $thread-index)) + ) + + ;; Initialize the function table with our wake-from-suspend function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $wake-from-suspend) + + (func (export "run-suspend") (param i32) + ;; Set up the arguments for the wake-for-suspend thread start function. + ;; It expects a pointer to a structure containing the thread index to resume + ;; and the future to wait on before resuming it. + (local $wake-from-suspend-argp i32) + (local.set $wake-from-suspend-argp (i32.const 4)) + (i32.store offset=0 (local.get $wake-from-suspend-argp) (call $thread-index)) + (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get 0)) + ;; Spawn a new thread that will wake us up from our uncancellable suspend and schedule + ;; it to resume after we suspend. + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (local.get $wake-from-suspend-argp))) + + ;; Request suspension. We will not be woken up by cancellation, because this is an uncancellable + ;; suspend. We will be woken up by the other thread we spawned above, which will be resumed after + ;; the supertask cancels our subtask. + (if (i32.ne (call $thread-suspend) (i32.const 0)) (then unreachable)) + ;; Request suspension again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-suspend-cancellable) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + ) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $task-cancel (canon task.cancel)) + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-yield-cancellable (canon thread.yield cancellable)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) + (core func $thread-suspend-cancellable (canon thread.suspend cancellable)) + (core func $future.read (canon future.read $FT async (memory $memory "mem"))) + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable.join (canon waitable.join)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + + ;; Instantiate the main module + (core instance $cm ( + instantiate $CM + (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.cancel" (func $task-cancel)) + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.yield-cancellable" (func $thread-yield-cancellable)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.suspend-cancellable" (func $thread-suspend-cancellable)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.new" (func $waitable-set.new)))) + (with "libc" (instance $libc)))) + + (func (export "run-yield") (result u32) (canon lift (core func $cm "run-yield") async)) + (func (export "run-suspend") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-suspend") async)) + ) + (component $D + (type $FT (future)) + (import "run-yield" (func $run-yield (result u32))) + (import "run-suspend" (func $run-suspend (param "fut" $FT) (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "run-yield" (func $run-yield (param i32) (result i32))) + (import "" "run-suspend" (func $run-suspend (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (func $test-yield (result i32) + (local $ret i32) (local $subtask i32) + (local $ws i32) (local $event_code i32) + (local $run-yield-retp i32) (local $wait-retp i32) + + ;; Set up return value storage for run-yield and waitable-set.wait + (local.set $run-yield-retp (i32.const 4)) + (local.set $wait-retp (i32.const 8)) + (i32.store (local.get $run-yield-retp) (i32.const 0xbad0bad0)) + (i32.store (local.get $wait-retp) (i32.const 0xbad0bad0)) + + ;; Calling run-yield will start the thread, which will yield + (local.set $ret (call $run-yield (local.get $run-yield-retp))) + ;; Ensure that the thread started + (if (i32.ne (i32.and (local.get $ret) (i32.const 0xF)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + ;; Extract the subtask index + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + ;; Cancel the subtask, which should block, because the initial yield is uncancellable + (local.set $ret (call $subtask.cancel (local.get $subtask))) + ;; Ensure the cancellation blocked + (if (i32.ne (local.get $ret) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; Wait on the subtask, which will cause it to resume, see the cancellation, and exit + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $wait-retp))) + ;; Ensure we got the subtask event + (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + ;; Ensure the subtask index matches + (if (i32.ne (local.get $subtask) (i32.load (local.get $wait-retp))) + (then unreachable)) + ;; Ensure the subtask was cancelled before it returned + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED=4 | (0<<4) ;)) + (i32.load offset=4 (local.get $wait-retp))) + (then unreachable)) + + ;; Return success + (i32.const 42) + ) + + (func $test-suspend (result i32) + (local $ret i32) (local $subtask i32) + (local $ws i32) (local $event_code i32) + (local $run-suspend-retp i32) (local $wait-retp i32) + (local $ret64 i64) (local $futr i32) (local $futw i32) + + ;; Set up return value storage for run-suspend and waitable-set.wait + (local.set $run-suspend-retp (i32.const 4)) + (local.set $wait-retp (i32.const 8)) + (i32.store (local.get $run-suspend-retp) (i32.const 0xbad0bad0)) + (i32.store (local.get $wait-retp) (i32.const 0xbad0bad0)) + + ;; Create a future that the run-suspend thread will wait on + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; Calling run-suspend will start the thread, which will suspend + (local.set $ret (call $run-suspend (local.get $futr) (local.get $run-suspend-retp))) + ;; Ensure that the thread started + (if (i32.ne (i32.and (local.get $ret) (i32.const 0xF)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + ;; Extract the subtask index + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + ;; Cancel the subtask, which should block, because the initial suspend is uncancellable + (local.set $ret (call $subtask.cancel (local.get $subtask))) + ;; Ensure the cancellation blocked + (if (i32.ne (local.get $ret) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; Yield, ensuring the subtask's spawned thread gets to run + (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) + + ;; Write to the future, which the subtask's spawned thread is waiting on + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + ;; The write should succeed + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + ;; Wait on the subtask, which will cause its spawned thread to wake up the main thread, + ;; which will then enact a cancellable suspend, see the cancellation, and exit + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $wait-retp))) + ;; Ensure we got the subtask event + (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + ;; Ensure the subtask index matches + (if (i32.ne (local.get $subtask) (i32.load (local.get $wait-retp))) + (then unreachable)) + ;; Ensure the subtask was cancelled before it returned + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED=4 | (0<<4) ;)) + (i32.load offset=4 (local.get $wait-retp))) + (then unreachable)) + + ;; Return success + (i32.const 42) + ) + + (func $run (export "run") (result i32) + (if (i32.ne (call $test-yield) (i32.const 42)) + (then unreachable)) + (if (i32.ne (call $test-suspend) (i32.const 42)) + (then unreachable)) + + ;; Return success + (i32.const 42) + ) + ) + + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + (core func $waitable.join (canon waitable.join)) + (core func $subtask.cancel (canon subtask.cancel async)) + (core func $future.new (canon future.new $FT)) + (core func $future.write (canon future.write $FT async (memory $memory "mem"))) + (core func $thread.yield (canon thread.yield)) + (canon lower (func $run-yield) async (memory $memory "mem") (core func $run-yield')) + (canon lower (func $run-suspend) async (memory $memory "mem") (core func $run-suspend')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "run-yield" (func $run-yield')) + (export "run-suspend" (func $run-suspend')) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "subtask.cancel" (func $subtask.cancel)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "thread.yield" (func $thread.yield)) + )))) + (func (export "run") (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "run-yield" (func $c "run-yield")) + (with "run-suspend" (func $c "run-suspend")) + )) + (func (export "run") (alias export $d "run")) +) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From 607e680c33b485f6657266afcc99e52f2258a3ec Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 3 Oct 2025 15:03:05 +0100 Subject: [PATCH 11/13] Fix cancellation for explicit suspends --- .../src/runtime/component/concurrent.rs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index c4fe150ccff9..70627836f086 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3059,6 +3059,17 @@ impl Instance { Ok(()) } + fn pending_cancellation(&self, store: &mut StoreOpaque) -> bool { + let state = self.concurrent_state_mut(store); + let thread = state.guest_thread.unwrap(); + if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + true + } else { + false + } + } + /// Helper function for the `thread.yield`, `thread.yield-to`, `thread.suspend`, /// and `thread.switch-to` intrinsics. pub(crate) fn suspension_intrinsic( @@ -3069,6 +3080,11 @@ impl Instance { yielding: bool, to_thread: Option, ) -> Result { + // There could be a pending cancellation from a previous uncancellable wait + if cancellable && self.pending_cancellation(store.0) { + return Ok(true); + } + self.id().get(store.0).check_may_leave(caller)?; if let Some(thread) = to_thread { @@ -3092,18 +3108,7 @@ impl Instance { self.suspend(store.0, reason)?; - if cancellable { - let state = self.concurrent_state_mut(store.0); - let thread = state.guest_thread.unwrap(); - if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { - assert!(matches!(event, Event::Cancelled)); - Ok(true) - } else { - Ok(false) - } - } else { - Ok(false) - } + Ok(cancellable && self.pending_cancellation(store.0)) } /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. From 73668c7208bff2fd33bc5bd1b834ab7a12bf1ac8 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 3 Oct 2025 17:06:33 +0100 Subject: [PATCH 12/13] Finish cancellation test --- .../stackful-cancellation.wast | 254 ++++++++++++------ 1 file changed, 168 insertions(+), 86 deletions(-) diff --git a/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast index 0846e3d0ba98..4eaa0abad99b 100644 --- a/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast +++ b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast @@ -1,23 +1,58 @@ ;;! component_model_async = true +;;! component_model_async_stackful = true +;;! component_model_async_builtins = true ;;! component_model_threading = true +;;! reference_types = true + +;; Tests that cancellation works with the async threading intrinsics. +;; Consists of two components, C and D. C implements functions that mix cancellable and uncancellable yields and suspensions. +;; D calls these functions and cancels the resulting subtasks, ensuring that cancellation is only seen when expected. + +;; -- Component C -- + +;; `run-yield`: Yields twice, first with an uncancellable yield, then with a cancellable yield. +;; The caller cancels the subtask during the first yield, and ensures that the cancellation only takes effect +;; on the second yield. + +;; `run-yield-to`: Yields twice to a spawned thread, first with an uncancellable yield, then with a cancellable yield. +;; A complication is that we can't guarantee that if the spawned thread yields, the supertask will be scheduled to +;; cancel the subtask before the subtask's implicit thread is rescheduled. To handle this, the subtask's implicit +;; thread first waits on a future to be written by the supertask, then yields to the spawned thread. + +;; `run-suspend`: More complex, because executing an uncancellable suspension requires another +;; thread in the same subtask to explicitly wake it up. This is done by the subtask spawning a new thread that +;; waits on a future to be written by the supertask, and then resumes the main thread once that happens. +;; After setting up this thread, `run-suspend` performs an uncancellable suspend, then a cancellable suspend. +;; The caller cancels the subtask during the first suspend, writes to the future to make the spawned thread +;; resume the implicit thread, and ensures that the cancellation only takes effect on the second suspend. + +;; `run-switch-to`: Similar to `run-suspend`, but uses `thread.switch-to` instead of `thread.suspend`. + +;; -- Component D -- + +;; `run-test`: Calls one of the functions in C based on a test id, cancels the resulting subtask, and ensures that +;; cancellation is only seen when expected. + +;; `run`: Calls `run-test` for each of the functions in C. (component (component $C (type $FT (future)) (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) - ;; Defines the table for the thread start function + ;; Defines the table for the thread start functions, of which there are two (core module $libc - (table (export "__indirect_function_table") 1 funcref)) + (table (export "__indirect_function_table") 2 funcref)) (core module $CM - ;; Import the threading builtins and the table from libc (import "" "mem" (memory 1)) (import "" "task.cancel" (func $task-cancel)) (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) (import "" "thread.suspend" (func $thread-suspend (result i32))) (import "" "thread.suspend-cancellable" (func $thread-suspend-cancellable (result i32))) (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.yield-to-cancellable" (func $thread-yield-to-cancellable (param i32) (result i32))) (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.switch-to-cancellable" (func $thread-switch-to-cancellable (param i32) (result i32))) (import "" "thread.yield" (func $thread-yield (result i32))) (import "" "thread.yield-cancellable" (func $thread-yield-cancellable (result i32))) (import "" "thread.index" (func $thread-index (result i32))) @@ -26,7 +61,11 @@ (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + (import "libc" "__indirect_function_table" (table $indirect-function-table 2 funcref)) + + ;; Indices into the function table for the thread start functions + (global $wake-from-suspend-ftbl-idx i32 (i32.const 0)) + (global $just-yield-ftbl-idx i32 (i32.const 1)) (func (export "run-yield") ;; Yield back to the caller, who will attempt to cancel us, but we won't see it @@ -38,18 +77,10 @@ ) (func $wait-for-future-write (param i32) - ;; Waitable set to wait on the future read - (local $ws i32) (local $ret i32) (local $event_code i32) - (local.set $ws (call $waitable-set.new)) + (local $ret i32) ;; Perform a future.read, which will block, waiting for the supertask to write (local.set $ret (call $future.read (local.get 0) (i32.const 0xba5eba11))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - (call $waitable.join (local.get 0) (local.get $ws)) - - ;; Wait on $ws synchronously, don't expect cancellation - (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) (then unreachable)) ) @@ -65,22 +96,50 @@ (call $thread-resume-later (local.get $thread-index)) ) - ;; Initialize the function table with our wake-from-suspend function; this will be - ;; used by thread.new_indirect - (elem (table $indirect-function-table) (i32.const 0) func $wake-from-suspend) + (func $just-yield (param $explicit-thread-idx i32) + ;; Yield nondeterministically, either back to the supertask, who will then wait on cancellation to be acknowledged, + ;; or to the implicit thread, who will acknowledge the cancellation. + (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) + ) + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; wake-from-suspend-ftbl-idx ;)) func $wake-from-suspend) + (elem (table $indirect-function-table) (i32.const 1 (; just-yield-ftbl-idx ;)) func $just-yield) + + (func (export "run-yield-to") (param $futr i32) + (local $thread-index i32) + ;; Spawn a new thread that will wake us up from our uncancellable suspend; we'll switch to it next + (local.set $thread-index + (call $thread-new-indirect (global.get $just-yield-ftbl-idx) (call $thread-index))) + + ;; We can't guarantee that the supertask will be scheduled to cancel us before we're rescheduled, so we first + ;; wait on the future to be written, then yield to the spawned thread. This means that cancellation will be + ;; sent while we're waiting on the future rather than at the yield point, but the cancel will still be pending + ;; when we reach the yield point, so it should still be ignored by the uncancellable yield and only take effect + ;; when we reach the second, cancellable yield. + (call $wait-for-future-write (local.get $futr)) - (func (export "run-suspend") (param i32) + ;; Yield to the spawned thread uncancellably. We should eventually be rescheduled without being notified + ;; of the pending cancellation. + (if (i32.ne (call $thread-yield-to (local.get $thread-index)) (i32.const 0)) (then unreachable)) + ;; Yield to the spawned thread again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-yield-to-cancellable (local.get $thread-index)) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + + (func (export "run-suspend") (param $futr i32) ;; Set up the arguments for the wake-for-suspend thread start function. ;; It expects a pointer to a structure containing the thread index to resume ;; and the future to wait on before resuming it. (local $wake-from-suspend-argp i32) (local.set $wake-from-suspend-argp (i32.const 4)) (i32.store offset=0 (local.get $wake-from-suspend-argp) (call $thread-index)) - (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get 0)) + (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get $futr)) + ;; Spawn a new thread that will wake us up from our uncancellable suspend and schedule ;; it to resume after we suspend. (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (local.get $wake-from-suspend-argp))) + (call $thread-new-indirect (global.get $wake-from-suspend-ftbl-idx) (local.get $wake-from-suspend-argp))) ;; Request suspension. We will not be woken up by cancellation, because this is an uncancellable ;; suspend. We will be woken up by the other thread we spawned above, which will be resumed after @@ -90,6 +149,30 @@ (if (i32.ne (call $thread-suspend-cancellable) (i32.const 1)) (then unreachable)) (call $task-cancel) ) + + (func (export "run-switch-to") (param $futr i32) + (local $thread-index i32) + ;; Set up the arguments for the wake-for-suspend thread start function. + ;; It expects a pointer to a structure containing the thread index to resume + ;; and the future to wait on before resuming it. + (local $wake-from-suspend-argp i32) + (local.set $wake-from-suspend-argp (i32.const 4)) + (i32.store offset=0 (local.get $wake-from-suspend-argp) (call $thread-index)) + (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get $futr)) + + ;; Spawn a new thread that will wake us up from our uncancellable suspend; we'll switch to it next + (local.set $thread-index + (call $thread-new-indirect (global.get $wake-from-suspend-ftbl-idx) (local.get $wake-from-suspend-argp))) + + ;; Request suspension by switching to the spawned thread. + ;; We will not be woken up by cancellation, because this is an uncancellable suspend. + ;; We will be woken up by the other thread we spawned above, which will be resumed after + ;; the supertask cancels our subtask. + (if (i32.ne (call $thread-switch-to (local.get $thread-index)) (i32.const 0)) (then unreachable)) + ;; Request suspension again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-switch-to-cancellable (local.get $thread-index)) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) ) ;; Instantiate the libc module to get the table @@ -105,11 +188,13 @@ (core func $thread-yield-cancellable (canon thread.yield cancellable)) (core func $thread-index (canon thread.index)) (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-yield-to-cancellable (canon thread.yield-to cancellable)) (core func $thread-resume-later (canon thread.resume-later)) (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-switch-to-cancellable (canon thread.switch-to cancellable)) (core func $thread-suspend (canon thread.suspend)) (core func $thread-suspend-cancellable (canon thread.suspend cancellable)) - (core func $future.read (canon future.read $FT async (memory $memory "mem"))) + (core func $future.read (canon future.read $FT (memory $memory "mem"))) (core func $waitable-set.new (canon waitable-set.new)) (core func $waitable.join (canon waitable.join)) (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) @@ -123,9 +208,11 @@ (export "thread.new_indirect" (func $thread-new-indirect)) (export "thread.index" (func $thread-index)) (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield-to-cancellable" (func $thread-yield-to-cancellable)) (export "thread.yield" (func $thread-yield)) (export "thread.yield-cancellable" (func $thread-yield-cancellable)) (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.switch-to-cancellable" (func $thread-switch-to-cancellable)) (export "thread.suspend" (func $thread-suspend)) (export "thread.suspend-cancellable" (func $thread-suspend-cancellable)) (export "thread.resume-later" (func $thread-resume-later)) @@ -136,12 +223,17 @@ (with "libc" (instance $libc)))) (func (export "run-yield") (result u32) (canon lift (core func $cm "run-yield") async)) + (func (export "run-yield-to") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-yield-to") async)) (func (export "run-suspend") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-suspend") async)) + (func (export "run-switch-to") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-switch-to") async)) ) + (component $D (type $FT (future)) (import "run-yield" (func $run-yield (result u32))) + (import "run-yield-to" (func $run-yield-to (param "fut" $FT) (result u32))) (import "run-suspend" (func $run-suspend (param "fut" $FT) (result u32))) + (import "run-switch-to" (func $run-switch-to (param "fut" $FT) (result u32))) (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) @@ -149,96 +241,69 @@ (import "" "mem" (memory 1)) (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) (import "" "run-yield" (func $run-yield (param i32) (result i32))) + (import "" "run-yield-to" (func $run-yield-to (param i32 i32) (result i32))) (import "" "run-suspend" (func $run-suspend (param i32 i32) (result i32))) + (import "" "run-switch-to" (func $run-switch-to (param i32 i32) (result i32))) (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) (import "" "future.new" (func $future.new (result i64))) (import "" "future.write" (func $future.write (param i32 i32) (result i32))) (import "" "thread.yield" (func $thread-yield (result i32))) - (func $test-yield (result i32) + + (func $run-test (param $test-id i32) (result i32) (local $ret i32) (local $subtask i32) (local $ws i32) (local $event_code i32) - (local $run-yield-retp i32) (local $wait-retp i32) - - ;; Set up return value storage for run-yield and waitable-set.wait - (local.set $run-yield-retp (i32.const 4)) - (local.set $wait-retp (i32.const 8)) - (i32.store (local.get $run-yield-retp) (i32.const 0xbad0bad0)) - (i32.store (local.get $wait-retp) (i32.const 0xbad0bad0)) - - ;; Calling run-yield will start the thread, which will yield - (local.set $ret (call $run-yield (local.get $run-yield-retp))) - ;; Ensure that the thread started - (if (i32.ne (i32.and (local.get $ret) (i32.const 0xF)) (i32.const 1 (; STARTED ;))) - (then unreachable)) - ;; Extract the subtask index - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - ;; Cancel the subtask, which should block, because the initial yield is uncancellable - (local.set $ret (call $subtask.cancel (local.get $subtask))) - ;; Ensure the cancellation blocked - (if (i32.ne (local.get $ret) (i32.const -1 (; BLOCKED ;))) - (then unreachable)) - - ;; Wait on the subtask, which will cause it to resume, see the cancellation, and exit - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $subtask) (local.get $ws)) - (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $wait-retp))) - ;; Ensure we got the subtask event - (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) - (then unreachable)) - ;; Ensure the subtask index matches - (if (i32.ne (local.get $subtask) (i32.load (local.get $wait-retp))) - (then unreachable)) - ;; Ensure the subtask was cancelled before it returned - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED=4 | (0<<4) ;)) - (i32.load offset=4 (local.get $wait-retp))) - (then unreachable)) - - ;; Return success - (i32.const 42) - ) - - (func $test-suspend (result i32) - (local $ret i32) (local $subtask i32) - (local $ws i32) (local $event_code i32) - (local $run-suspend-retp i32) (local $wait-retp i32) + (local $run-retp i32) (local $wait-retp i32) (local $ret64 i64) (local $futr i32) (local $futw i32) - ;; Set up return value storage for run-suspend and waitable-set.wait - (local.set $run-suspend-retp (i32.const 4)) + ;; Set up return value storage for run-suspend/switch-to and waitable-set.wait + (local.set $run-retp (i32.const 4)) (local.set $wait-retp (i32.const 8)) - (i32.store (local.get $run-suspend-retp) (i32.const 0xbad0bad0)) + (i32.store (local.get $run-retp) (i32.const 0xbad0bad0)) (i32.store (local.get $wait-retp) (i32.const 0xbad0bad0)) - ;; Create a future that the run-suspend thread will wait on + ;; Create a future that the subtask may wait on (local.set $ret64 (call $future.new)) (local.set $futr (i32.wrap_i64 (local.get $ret64))) (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - ;; Calling run-suspend will start the thread, which will suspend - (local.set $ret (call $run-suspend (local.get $futr) (local.get $run-suspend-retp))) + ;; Calling run-suspend/switch-to will start the thread, which will suspend. + ;; This is basically a switch statement: + ;; 0: run-yield + ;; 1: run-yield-to + ;; 2: run-suspend + ;; 3: run-switch-to + (if (i32.eq (local.get $test-id) (i32.const 0)) + (then (local.set $ret (call $run-yield (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 1)) + (then (local.set $ret (call $run-yield-to (local.get $futr) (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 2)) + (then (local.set $ret (call $run-suspend (local.get $futr) (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 3)) + (then (local.set $ret (call $run-switch-to (local.get $futr) (local.get $run-retp)))) + (else unreachable)))))))) + ;; Ensure that the thread started (if (i32.ne (i32.and (local.get $ret) (i32.const 0xF)) (i32.const 1 (; STARTED ;))) (then unreachable)) ;; Extract the subtask index (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - ;; Cancel the subtask, which should block, because the initial suspend is uncancellable + ;; Cancel the subtask, which should block, because the initial suspend/yield is uncancellable (local.set $ret (call $subtask.cancel (local.get $subtask))) ;; Ensure the cancellation blocked (if (i32.ne (local.get $ret) (i32.const -1 (; BLOCKED ;))) (then unreachable)) - ;; Yield, ensuring the subtask's spawned thread gets to run - (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) - - ;; Write to the future, which the subtask's spawned thread is waiting on - (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) - ;; The write should succeed - (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) - (then unreachable)) - ;; Wait on the subtask, which will cause its spawned thread to wake up the main thread, - ;; which will then enact a cancellable suspend, see the cancellation, and exit + ;; If we're not testing run-yield, the subtask is expecting a write to our future, so write to it + (if (i32.ne (local.get $test-id) (i32.const 0)) + (then + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + ;; The write should succeed + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)))) + + ;; Wait on the subtask, which will eventually progress to a cancellable yield/suspend and acknowledge the cancellation (local.set $ws (call $waitable-set.new)) (call $waitable.join (local.get $subtask) (local.get $ws)) (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $wait-retp))) @@ -258,9 +323,20 @@ ) (func $run (export "run") (result i32) - (if (i32.ne (call $test-yield) (i32.const 42)) + ;; test-id 0: run-yield + (if (i32.ne (call $run-test (i32.const 0)) (i32.const 42)) (then unreachable)) - (if (i32.ne (call $test-suspend) (i32.const 42)) + + ;; test-id 1: run-yield-to + (if (i32.ne (call $run-test (i32.const 1)) (i32.const 42)) + (then unreachable)) + + ;; test-id 2: run-suspend + (if (i32.ne (call $run-test (i32.const 2)) (i32.const 42)) + (then unreachable)) + + ;; test-id 3: run-switch-to + (if (i32.ne (call $run-test (i32.const 3)) (i32.const 42)) (then unreachable)) ;; Return success @@ -273,14 +349,18 @@ (core func $waitable.join (canon waitable.join)) (core func $subtask.cancel (canon subtask.cancel async)) (core func $future.new (canon future.new $FT)) - (core func $future.write (canon future.write $FT async (memory $memory "mem"))) + (core func $future.write (canon future.write $FT (memory $memory "mem"))) (core func $thread.yield (canon thread.yield)) (canon lower (func $run-yield) async (memory $memory "mem") (core func $run-yield')) (canon lower (func $run-suspend) async (memory $memory "mem") (core func $run-suspend')) + (canon lower (func $run-switch-to) async (memory $memory "mem") (core func $run-switch-to')) + (canon lower (func $run-yield-to) async (memory $memory "mem") (core func $run-yield-to')) (core instance $dm (instantiate $DM (with "" (instance (export "mem" (memory $memory "mem")) (export "run-yield" (func $run-yield')) (export "run-suspend" (func $run-suspend')) + (export "run-switch-to" (func $run-switch-to')) + (export "run-yield-to" (func $run-yield-to')) (export "waitable.join" (func $waitable.join)) (export "waitable-set.new" (func $waitable-set.new)) (export "waitable-set.wait" (func $waitable-set.wait)) @@ -295,7 +375,9 @@ (instance $c (instantiate $C)) (instance $d (instantiate $D (with "run-yield" (func $c "run-yield")) + (with "run-yield-to" (func $c "run-yield-to")) (with "run-suspend" (func $c "run-suspend")) + (with "run-switch-to" (func $c "run-switch-to")) )) (func (export "run") (alias export $d "run")) ) From aa565b28c1923df4098f139c5e6e726e2fa29c63 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Sat, 4 Oct 2025 14:04:32 +0100 Subject: [PATCH 13/13] Store threads in the instance table --- crates/cranelift/src/compiler/component.rs | 2 + crates/environ/src/component.rs | 2 +- crates/environ/src/component/dfg.rs | 3 + crates/environ/src/component/info.rs | 2 + .../environ/src/component/translate/inline.rs | 1 + .../src/runtime/component/concurrent.rs | 654 ++++++++---------- .../src/runtime/vm/component/libcalls.rs | 10 +- tests/all/component_model/threading.rs | 65 +- 8 files changed, 360 insertions(+), 379 deletions(-) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index a3f65989823d..401401c5b387 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -798,6 +798,7 @@ impl<'a> TrampolineCompiler<'a> { ); } Trampoline::ThreadNewIndirect { + instance, start_func_table_idx, start_func_ty_idx, } => { @@ -806,6 +807,7 @@ impl<'a> TrampolineCompiler<'a> { TrapSentinel::NegativeOne, WasmArgs::InRegisters, |me, params| { + params.push(me.index_value(*instance)); params.push(me.index_value(*start_func_table_idx)); params.push(me.index_value(*start_func_ty_idx)); }, diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 70008d85f9e4..dec960fee091 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -190,7 +190,7 @@ macro_rules! foreach_builtin_component_function { #[cfg(feature = "component-model-async")] thread_index(vmctx: vmctx) -> u64; #[cfg(feature = "component-model-async")] - thread_new_indirect(vmctx: vmctx, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; + thread_new_indirect(vmctx: vmctx, caller_instance: u32, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; #[cfg(feature = "component-model-async")] thread_switch_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; #[cfg(feature = "component-model-async")] diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 7095ede562c0..bb3f9cdcef39 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -481,6 +481,7 @@ pub enum Trampoline { }, ThreadIndex, ThreadNewIndirect { + instance: RuntimeComponentInstanceIndex, start_func_ty_idx: ComponentTypeIndex, start_func_table_id: TableId, }, @@ -1150,9 +1151,11 @@ impl LinearizeDfg<'_> { }, Trampoline::ThreadIndex => info::Trampoline::ThreadIndex, Trampoline::ThreadNewIndirect { + instance, start_func_ty_idx, start_func_table_id, } => info::Trampoline::ThreadNewIndirect { + instance: *instance, start_func_ty_idx: *start_func_ty_idx, start_func_table_idx: self.runtime_table(*start_func_table_id), }, diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 80639e9a044f..28e363c69913 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -1122,6 +1122,8 @@ pub enum Trampoline { /// Intrinsic used to implement the `thread.new_indirect` component model builtin. ThreadNewIndirect { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, /// The type index for the start function of the thread. start_func_ty_idx: ComponentTypeIndex, /// The index of the table that stores the start function. diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 34c1c9a48cea..227224589dea 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1132,6 +1132,7 @@ impl<'a> Inliner<'a> { let index = self.result.trampolines.push(( *func, dfg::Trampoline::ThreadNewIndirect { + instance: frame.instance, start_func_ty_idx: *start_func_ty, start_func_table_id: table_id, }, diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 70627836f086..f37cedf76d6b 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -595,7 +595,7 @@ enum SuspendReason { /// waitable set or task. Waiting { set: TableId, - thread: InstanceGuestThreadIndex, + thread: TableId, }, /// The fiber has finished handling its most recent work item and is waiting /// for another (or to be dropped if it is no longer needed). @@ -603,14 +603,14 @@ enum SuspendReason { /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. Yielding { - thread: InstanceGuestThreadIndex, - to: Option, + thread: TableId, + to: Option>, cancellable: bool, }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. ExplicitlySuspending { - thread: InstanceGuestThreadIndex, - to: Option, + thread: TableId, + to: Option>, cancellable: bool, }, } @@ -628,14 +628,16 @@ enum GuestCallKind { }, /// Indicates that a new guest task call is pending and may be executed /// using the specified closure. - Start(Box Result<()> + Send + Sync>), + StartImplicit(Box Result<()> + Send + Sync>), + StartExplicit(Box Result<()> + Send + Sync>), } impl fmt::Debug for GuestCallKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::DeliverEvent { set } => f.debug_struct("DeliverEvent").field("set", set).finish(), - Self::Start(_) => f.debug_tuple("Start").finish(), + Self::StartImplicit(_) => f.debug_tuple("StartImplicit").finish(), + Self::StartExplicit(_) => f.debug_tuple("StartExplicit").finish(), } } } @@ -643,7 +645,7 @@ impl fmt::Debug for GuestCallKind { /// Represents a pending call into guest code for a given guest task. #[derive(Debug)] struct GuestCall { - thread: InstanceGuestThreadIndex, + thread: TableId, kind: GuestCallKind, } @@ -658,21 +660,18 @@ impl GuestCall { /// - the call is for a not-yet started task and the (sub-)component /// instance to be called has backpressure enabled fn is_ready(&self, state: &mut ConcurrentState) -> Result { - let task_instance = state.get_mut(self.thread.task)?.instance; + let task_instance = state.get_task_mut(self.thread)?.instance; let state = state.instance_state(task_instance); - // Explicit threads are always ready, as they can only be entered from within the same - // task. - let ready = self.thread.is_explicit_thread() - || match &self.kind { - GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, - GuestCallKind::Start(_) => !(state.do_not_enter || state.backpressure > 0), - }; + let ready = match &self.kind { + GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, + GuestCallKind::StartImplicit(_) => !(state.do_not_enter || state.backpressure > 0), + GuestCallKind::StartExplicit(_) => true, + }; log::trace!( - "call {self:?} ready? {ready} (do_not_enter: {}; backpressure: {}, is_explicit_thread: {})", + "call {self:?} ready? {ready} (do_not_enter: {}; backpressure: {})", state.do_not_enter, - state.backpressure, - self.thread.is_explicit_thread() + state.backpressure ); Ok(ready) } @@ -689,7 +688,7 @@ enum WorkerItem { #[derive(Debug)] struct PollParams { /// Identifies the polling thread. - thread: InstanceGuestThreadIndex, + thread: TableId, /// The waitable set being polled. set: TableId, } @@ -729,7 +728,7 @@ impl ComponentInstance { /// async-lifted export; otherwise, it was received from its callback. fn handle_callback_code( mut self: Pin<&mut Self>, - guest_thread: InstanceGuestThreadIndex, + guest_thread: TableId, runtime_instance: RuntimeComponentInstanceIndex, code: u32, initial_call: bool, @@ -739,7 +738,8 @@ impl ComponentInstance { log::trace!("received callback code from {guest_thread:?}: {code} (set: {set})"); let state = self.as_mut().concurrent_state_mut(); - let task = state.get_mut(guest_thread.task)?; + let task_id = state.get_task_id(guest_thread)?; + let task = state.get_mut(task_id)?; if task.lift_result.is_some() { if code == callback_code::EXIT { @@ -748,7 +748,7 @@ impl ComponentInstance { if initial_call { // Notify any current or future waiters that this subtask has // started. - Waitable::Guest(guest_thread.task).set_event( + Waitable::Guest(task_id).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -769,18 +769,15 @@ impl ComponentInstance { match code { callback_code::EXIT => { - let task = state.get_mut(guest_thread.task)?; + let task = state.get_task_mut(guest_thread)?; match &task.caller { Caller::Host { remove_task_automatically, .. } => { if *remove_task_automatically { - log::trace!( - "handle_callback_code will delete task {:?}", - guest_thread.task - ); - Waitable::Guest(guest_thread.task).delete_from(state)?; + log::trace!("handle_callback_code will delete task {:?}", task_id); + Waitable::Guest(task_id).delete_from(state)?; } } Caller::Guest { .. } => { @@ -792,7 +789,7 @@ impl ComponentInstance { callback_code::YIELD => { // Push this thread onto the "low priority" queue so it runs after // any other threads have had a chance to run. - let task = state.get_mut(guest_thread.task)?; + let task = state.get_task_mut(guest_thread)?; assert!(task.event.is_none()); task.event = Some(Event::None); state.push_low_priority(WorkItem::GuestCall(GuestCall { @@ -804,7 +801,7 @@ impl ComponentInstance { let set = get_set(self.as_mut(), set)?; let state = self.concurrent_state_mut(); - if state.get_mut(guest_thread.task)?.event.is_some() + if state.get_task_mut(guest_thread)?.event.is_some() || !state.get_mut(set)?.ready.is_empty() { // An event is immediately available; deliver it ASAP. @@ -830,10 +827,7 @@ impl ComponentInstance { // Here we also set `GuestTask::wake_on_cancel` // which allows `subtask.cancel` to interrupt the // wait. - let old = state - .get_thread_mut(&guest_thread)? - .wake_on_cancel - .replace(set); + let old = state.get_mut(guest_thread)?.wake_on_cancel.replace(set); assert!(old.is_none()); let old = state .get_mut(set)? @@ -1397,11 +1391,12 @@ impl Instance { self.run_on_worker(store, WorkerItem::GuestCall(call)) .await?; } else { - let task = state.get_mut(call.thread.task)?; + let task_id = state.get_task_id(call.thread)?; + let task = state.get_mut(task_id)?; if !task.starting_sent { task.starting_sent = true; - if let GuestCallKind::Start(_) = &call.kind { - Waitable::Guest(call.thread.task).set_event( + if let GuestCallKind::StartImplicit(_) = &call.kind { + Waitable::Guest(task_id).set_event( state, Some(Event::Subtask { status: Status::Starting, @@ -1410,7 +1405,7 @@ impl Instance { } } - let runtime_instance = state.get_mut(call.thread.task)?.instance; + let runtime_instance = state.get_mut(task_id)?.instance; state .instance_state(runtime_instance) .pending @@ -1419,7 +1414,7 @@ impl Instance { } WorkItem::Poll(params) => { let state = self.concurrent_state_mut(store.0); - if state.get_mut(params.thread.task)?.event.is_some() + if state.get_task_mut(params.thread)?.event.is_some() || !state.get_mut(params.set)?.ready.is_empty() { // There's at least one event immediately available; deliver @@ -1433,7 +1428,7 @@ impl Instance { } else { // There are no events immediately available; deliver // `Event::None` to the guest. - state.get_mut(params.thread.task)?.event = Some(Event::None); + state.get_task_mut(params.thread)?.event = Some(Event::None); state.push_high_priority(WorkItem::GuestCall(GuestCall { thread: params.thread, kind: GuestCallKind::DeliverEvent { @@ -1464,6 +1459,7 @@ impl Instance { log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { + log::trace!("resume_fiber: suspend reason {:?}", &state.suspend_reason); // See the `SuspendReason` documentation for what each case means. match state.suspend_reason.take().unwrap() { SuspendReason::NeedWork => { @@ -1473,14 +1469,13 @@ impl Instance { fiber.dispose(store); } } - SuspendReason::Yielding { .. } => { + SuspendReason::Yielding { thread, .. } => { + state.get_mut(thread)?.state = GuestThreadState::Pending; state.push_low_priority(WorkItem::ResumeFiber(fiber)); } SuspendReason::ExplicitlySuspending { thread, .. } => { - state - .get_mut(thread.task)? - .suspended_threads - .insert(thread.thread, SuspendedThreadState::Started(fiber)); + // TODO do this earlier? + state.get_mut(thread)?.state = GuestThreadState::Suspended(fiber); } SuspendReason::Waiting { set, thread } => { let old = state @@ -1529,14 +1524,14 @@ impl Instance { fn handle_guest_call(self, store: &mut dyn VMStore, call: GuestCall) -> Result<()> { match call.kind { GuestCallKind::DeliverEvent { set } => { + let task_id = self.concurrent_state_mut(store).get_task_id(call.thread)?; let (event, waitable) = self .id() .get_mut(store) - .get_event(call.thread.task, set, true)? + .get_event(task_id, set, true)? .unwrap(); let state = self.concurrent_state_mut(store); - let task = state.get_mut(call.thread.task)?; - let runtime_instance = task.instance; + let runtime_instance = state.get_mut(task_id)?.instance; let handle = waitable.map(|(_, v)| v).unwrap_or(0); log::trace!( @@ -1549,22 +1544,23 @@ impl Instance { "GuestCallKind::DeliverEvent: replaced {old_thread:?} with {:?} as current thread", call.thread ); + let old_task = old_thread.map(|thread| state.get_task_id(thread).unwrap()); - self.maybe_push_call_context(store.store_opaque_mut(), call.thread.task)?; + self.maybe_push_call_context(store.store_opaque_mut(), task_id, old_task)?; let state = self.concurrent_state_mut(store); state.enter_instance(runtime_instance); - let callback = state.get_mut(call.thread.task)?.callback.take().unwrap(); + let callback = state.get_mut(task_id)?.callback.take().unwrap(); let code = callback(store, self, runtime_instance, event, handle)?; let state = self.concurrent_state_mut(store); - state.get_mut(call.thread.task)?.callback = Some(callback); + state.get_mut(task_id)?.callback = Some(callback); state.exit_instance(runtime_instance)?; - self.maybe_pop_call_context(store.store_opaque_mut(), call.thread.task)?; + self.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; self.id().get_mut(store).handle_callback_code( call.thread, @@ -1578,7 +1574,10 @@ impl Instance { "GuestCallKind::DeliverEvent: restored {old_thread:?} as current thread" ); } - GuestCallKind::Start(fun) => { + GuestCallKind::StartImplicit(fun) => { + fun(store, self)?; + } + GuestCallKind::StartExplicit(fun) => { fun(store, self)?; } } @@ -1594,19 +1593,29 @@ impl Instance { fn suspend(self, store: &mut dyn VMStore, reason: SuspendReason) -> Result<()> { log::trace!("suspend fiber: {reason:?}"); + let state = self.concurrent_state_mut(store); + // If we're yielding or waiting on behalf of a guest thread, we'll need to // pop the call context which manages resource borrows before suspending // and then push it again once we've resumed. let task = match &reason { SuspendReason::Yielding { thread, .. } | SuspendReason::Waiting { thread, .. } - | SuspendReason::ExplicitlySuspending { thread, .. } => Some(thread.task), + | SuspendReason::ExplicitlySuspending { thread, .. } => { + Some(state.get_task_id(*thread)?) + } SuspendReason::NeedWork => None, }; let old_guest_thread = if let Some(task) = task { - self.maybe_pop_call_context(store, task)?; - self.concurrent_state_mut(store).guest_thread + let old_thread = self.concurrent_state_mut(store).guest_thread; + let old_task = old_thread.map(|thread| { + self.concurrent_state_mut(store) + .get_task_id(thread) + .unwrap() + }); + self.maybe_pop_call_context(store, task, old_task)?; + old_thread } else { None }; @@ -1619,7 +1628,12 @@ impl Instance { if let Some(task) = task { self.concurrent_state_mut(store).guest_thread = old_guest_thread; - self.maybe_push_call_context(store, task)?; + let old_task = old_guest_thread.map(|thread| { + self.concurrent_state_mut(store) + .get_task_id(thread) + .unwrap() + }); + self.maybe_push_call_context(store, task, old_task)?; } Ok(()) @@ -1632,9 +1646,10 @@ impl Instance { self, store: &mut StoreOpaque, guest_task: TableId, + old_task: Option>, ) -> Result<()> { let task = self.concurrent_state_mut(store).get_mut(guest_task)?; - if task.lift_result.is_some() { + if Some(guest_task) != old_task && task.lift_result.is_some() { log::trace!("push call context for {guest_task:?}"); let call_context = task.call_context.take().unwrap(); store.component_resource_state().0.push(call_context); @@ -1649,12 +1664,14 @@ impl Instance { self, store: &mut StoreOpaque, guest_task: TableId, + old_task: Option>, ) -> Result<()> { - if self - .concurrent_state_mut(store) - .get_mut(guest_task)? - .lift_result - .is_some() + if Some(guest_task) != old_task + && self + .concurrent_state_mut(store) + .get_mut(guest_task)? + .lift_result + .is_some() { log::trace!("pop call context for {guest_task:?}"); let call_context = Some(store.component_resource_state().0.pop().unwrap()); @@ -1674,7 +1691,7 @@ impl Instance { unsafe fn queue_call( self, mut store: StoreContextMut, - guest_thread: InstanceGuestThreadIndex, + guest_thread: TableId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1699,7 +1716,7 @@ impl Instance { /// the returned closure is called. unsafe fn make_call( store: StoreContextMut, - guest_thread: InstanceGuestThreadIndex, + guest_thread: TableId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1717,7 +1734,7 @@ impl Instance { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; let task = instance .concurrent_state_mut(store) - .get_mut(guest_thread.task)?; + .get_task_mut(guest_thread)?; let may_enter_after_call = task.call_post_return_automatically(); let lower = task.lower_params.take().unwrap(); @@ -1766,7 +1783,7 @@ impl Instance { let callee_instance = self .concurrent_state_mut(store.0) - .get_mut(guest_thread.task)? + .get_task_mut(guest_thread)? .instance; let fun = if callback.is_some() { assert!(async_); @@ -1779,8 +1796,17 @@ impl Instance { log::trace!( "stackless call: replaced {old_thread:?} with {guest_thread:?} as current thread" ); + let old_task = old_thread.map(|thread| { + instance + .concurrent_state_mut(store) + .get_task_id(thread) + .unwrap() + }); + let task_id = instance + .concurrent_state_mut(store) + .get_task_id(guest_thread)?; - instance.maybe_push_call_context(store.store_opaque_mut(), guest_thread.task)?; + instance.maybe_push_call_context(store.store_opaque_mut(), task_id, old_task)?; instance .concurrent_state_mut(store) @@ -1798,7 +1824,7 @@ impl Instance { .concurrent_state_mut(store) .exit_instance(callee_instance)?; - instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; let state = instance.concurrent_state_mut(store); state.guest_thread = old_thread; @@ -1828,10 +1854,19 @@ impl Instance { log::trace!( "stackful call: replaced {old_thread:?} with {guest_thread:?} as current thread", ); + let old_task = old_thread.map(|thread| { + self.concurrent_state_mut(store) + .get_task_id(thread) + .unwrap() + }); + + let task_id = instance + .concurrent_state_mut(store) + .get_task_id(guest_thread)?; let mut flags = instance.id().get(store).instance_flags(callee_instance); - instance.maybe_push_call_context(store.store_opaque_mut(), guest_thread.task)?; + instance.maybe_push_call_context(store.store_opaque_mut(), task_id, old_task)?; // Unless this is a callback-less (i.e. stackful) // async-lifted export, we need to record that the instance @@ -1857,7 +1892,7 @@ impl Instance { // been called. if instance .concurrent_state_mut(store) - .get_mut(guest_thread.task)? + .get_mut(task_id)? .lift_result .is_some() { @@ -1873,13 +1908,9 @@ impl Instance { let state = instance.concurrent_state_mut(store); state.exit_instance(callee_instance)?; - assert!(state.get_mut(guest_thread.task)?.result.is_none()); + assert!(state.get_mut(task_id)?.result.is_none()); - state - .get_mut(guest_thread.task)? - .lift_result - .take() - .unwrap() + state.get_mut(task_id)?.lift_result.take().unwrap() }; // SAFETY: `result_count` represents the number of core Wasm @@ -1900,7 +1931,7 @@ impl Instance { if instance .concurrent_state_mut(store) - .get_mut(guest_thread.task)? + .get_mut(task_id)? .call_post_return_automatically() { unsafe { @@ -1933,18 +1964,18 @@ impl Instance { instance.task_complete( store, - guest_thread.task, + task_id, result, Status::Returned, post_return_arg, )?; } - instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; let state = instance.concurrent_state_mut(store); - let task = state.get_mut(guest_thread.task)?; - task.thread_completed(guest_thread.thread); + state.get_mut(guest_thread)?.state = GuestThreadState::Completed; + let task = state.get_mut(task_id)?; match &task.caller { Caller::Host { @@ -1952,7 +1983,7 @@ impl Instance { .. } => { if *remove_task_automatically { - self.delete_task_if_all_threads_exited(store, guest_thread.task)?; + //todo } } Caller::Guest { .. } => { @@ -1967,7 +1998,7 @@ impl Instance { self.concurrent_state_mut(store.0) .push_high_priority(WorkItem::GuestCall(GuestCall { thread: guest_thread, - kind: GuestCallKind::Start(fun), + kind: GuestCallKind::StartImplicit(fun), })); Ok(()) @@ -2079,7 +2110,7 @@ impl Instance { } dst.copy_from_slice(&src[..dst.len()]); let state = instance.concurrent_state_mut(store.0); - let task = state.guest_thread.unwrap().task; + let task = state.get_task_id(state.guest_thread.unwrap())?; Waitable::Guest(task).set_event( state, Some(Event::Subtask { @@ -2113,7 +2144,7 @@ impl Instance { let state = instance.concurrent_state_mut(store.0); let thread = state.guest_thread.unwrap(); if sync_caller { - state.get_mut(thread.task)?.sync_result = + state.get_task_mut(thread)?.sync_result = Some(if let ResultInfo::Stack { result_count } = &result_info { match result_count { 0 => None, @@ -2139,28 +2170,24 @@ impl Instance { )?; let guest_task = state.push(new_task)?; - state - .get_mut(guest_task)? - .threads - .get_mut(&MAIN_GUEST_THREAD_INDEX) - .unwrap() - .parent_task = Some(guest_task); + let new_thread = GuestThread::new_implicit(guest_task); + let guest_thread = state.push(new_thread)?; + state.get_mut(guest_task)?.threads.push(guest_thread); if let Some(old_thread) = old_thread { if !state.may_enter(guest_task) { bail!(crate::Trap::CannotEnterComponent); } - state.get_mut(old_thread.task)?.subtasks.insert(guest_task); + state.get_task_mut(old_thread)?.subtasks.insert(guest_task); }; - // Make the new task the current one so that `Self::start_call` knows + // Make the new thread the current one so that `Self::start_call` knows // which one to start. - state.guest_thread = Some(InstanceGuestThreadIndex { - task: guest_task, - thread: MAIN_GUEST_THREAD_INDEX, - }); - log::trace!("pushed {guest_task:?} as current task; old thread was {old_thread:?}"); + state.guest_thread = Some(guest_thread); + log::trace!( + "pushed {guest_task:?}:{guest_thread:?} as current thread; old thread was {old_thread:?}" + ); Ok(()) } @@ -2229,10 +2256,7 @@ impl Instance { let async_caller = storage.is_none(); let state = self.concurrent_state_mut(store.0); let guest_thread = state.guest_thread.unwrap(); - // start_call should only be called for the main thread of a guest task. - assert_eq!(guest_thread.thread, MAIN_GUEST_THREAD_INDEX); - - let guest_task = guest_thread.task; + let guest_task = state.get_task_id(guest_thread)?; let may_enter_after_call = state.get_mut(guest_task)?.call_post_return_automatically(); let callee = SendSyncPtr::new(NonNull::new(callee).unwrap()); let param_count = usize::try_from(param_count).unwrap(); @@ -2304,7 +2328,7 @@ impl Instance { // the subtask... let guest_waitable = Waitable::Guest(guest_task); let old_set = guest_waitable.common(state)?.set; - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_task_mut(caller)?.sync_call_set; guest_waitable.join(state, Some(set))?; // ... and suspend this fiber temporarily while we wait for it to start. @@ -2590,7 +2614,7 @@ impl Instance { // Save any existing result stashed in `GuestTask::result` so we can // replace it with the new result. let old_result = state - .get_mut(caller.task) + .get_task_mut(caller) .with_context(|| format!("bad handle: {caller:?}"))? .result .take(); @@ -2609,7 +2633,7 @@ impl Instance { let result = future.await?; tls::get(move |store| { let state = self.concurrent_state_mut(store); - state.get_mut(caller.task)?.result = Some(Box::new(result) as _); + state.get_task_mut(caller)?.result = Some(Box::new(result) as _); Waitable::Host(task).set_event( state, @@ -2648,7 +2672,7 @@ impl Instance { let state = self.concurrent_state_mut(store); state.push_future(future); - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_task_mut(caller)?.sync_call_set; Waitable::Host(task).join(state, Some(set))?; self.suspend( @@ -2665,7 +2689,7 @@ impl Instance { Ok(*mem::replace( &mut self .concurrent_state_mut(store) - .get_mut(caller.task)? + .get_task_mut(caller)? .result, old_result, ) @@ -2692,14 +2716,15 @@ impl Instance { .. } = *state.options(options); let guest_thread = state.guest_thread.unwrap(); + let guest_task = state.get_task_id(guest_thread)?; let lift = state - .get_mut(guest_thread.task)? + .get_task_mut(guest_thread)? .lift_result .take() .ok_or_else(|| { anyhow!("`task.return` or `task.cancel` called more than once for current task") })?; - assert!(state.get_mut(guest_thread.task)?.result.is_none()); + assert!(state.get_task_mut(guest_thread)?.result.is_none()); let invalid = ty != lift.ty || string_encoding != lift.string_encoding @@ -2725,14 +2750,7 @@ impl Instance { log::trace!("task.return for {guest_thread:?}"); let result = (lift.lift)(store, self, storage)?; - - self.task_complete( - store, - guest_thread.task, - result, - Status::Returned, - ValRaw::i32(0), - ) + self.task_complete(store, guest_task, result, Status::Returned, ValRaw::i32(0)) } /// Implements the `task.cancel` intrinsic. @@ -2744,7 +2762,7 @@ impl Instance { self.id().get(store).check_may_leave(caller)?; let state = self.concurrent_state_mut(store); let guest_thread = state.guest_thread.unwrap(); - let task = state.get_mut(guest_thread.task)?; + let task = state.get_task_mut(guest_thread)?; if !task.cancel_sent { bail!("`task.cancel` called by task which has not been cancelled") } @@ -2756,9 +2774,10 @@ impl Instance { log::trace!("task.cancel for {guest_thread:?}"); + let guest_task = state.get_task_id(guest_thread)?; self.task_complete( store, - guest_thread.task, + guest_task, Box::new(DummyResult), Status::ReturnCancelled, ValRaw::i32(0), @@ -2911,15 +2930,20 @@ impl Instance { pub(crate) fn thread_new_indirect( self, store: &mut dyn VMStore, + caller: RuntimeComponentInstanceIndex, _func_ty_idx: TypeFuncIndex, // currently unused - table_idx: RuntimeTableIndex, - func_idx: u32, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, context: i32, ) -> Result { + self.id().get(store).check_may_leave(caller)?; + log::trace!("creating new thread"); let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); - let funcref = unsafe { self.read_funcref_from_table(store, table_idx, func_idx as u64) }?; + let funcref = unsafe { + self.read_funcref_from_table(store, start_func_table_idx, start_func_idx as u64) + }?; if unsafe { funcref.as_ref().type_index } != start_func_ty.type_index() { bail!( "start function does not match expected type (currently only `(i32) -> ()` is supported)" @@ -2928,40 +2952,35 @@ impl Instance { let state = self.concurrent_state_mut(store); let current_thread = state.guest_thread.unwrap(); + let parent_task = state.get_task_id(current_thread)?; - // TODO check can_leave? - let new_index = state - .get_mut(current_thread.task)? - .new_thread(table_idx, func_idx, context); + let new_thread = + GuestThread::new_explicit(parent_task, start_func_table_idx, start_func_idx, context); + let thread_id = state.push(new_thread)?; - log::trace!("new thread with index {new_index:?} created"); + log::trace!("new thread with id {thread_id:?} created"); - Ok(new_index.0) + Ok(thread_id.rep()) } fn start_thread( &self, store: &mut StoreContextMut, - thread: GuestThreadIndex, + thread: TableId, start_func_table_idx: RuntimeTableIndex, start_func_idx: u32, context: i32, high_priority: bool, ) -> Result<()> { let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); - log::trace!( - "resuming thread {thread:?} of task {:?} that was not started", - guest_thread.task - ); + let task_id = self + .concurrent_state_mut(store.0) + .get_task_id(guest_thread)?; + log::trace!("starting thread {task_id:?}:{thread:?}"); let callee = unsafe { self.read_funcref_from_table(store.0, start_func_table_idx, start_func_idx as u64)? }; - log::trace!("start function pointer: {callee:p}"); - // TODO check can_enter? - let new_thread = InstanceGuestThreadIndex { - task: guest_thread.task, - thread, - }; + let token = StoreToken::new(store.as_context_mut()); let callee = SendSyncPtr::new(callee); let start_func = Box::new( @@ -2969,13 +2988,12 @@ impl Instance { let old_thread = instance .concurrent_state_mut(store) .guest_thread - .replace(new_thread); + .replace(thread); log::trace!( - "thread start: replaced {old_thread:?} with {new_thread:?} as current thread" + "thread start: replaced {old_thread:?} with {thread:?} as current thread" ); let mut store = token.as_context_mut(store); - instance.maybe_push_call_context(store.0, guest_thread.task)?; let params = [ValRaw::i32(context)]; unsafe { @@ -2986,8 +3004,6 @@ impl Instance { )? } - instance.maybe_pop_call_context(store.0, guest_thread.task)?; - let state = instance.concurrent_state_mut(store.0); state.guest_thread = old_thread; log::trace!("thread start: restored {old_thread:?} as current thread"); @@ -2995,18 +3011,16 @@ impl Instance { Ok(()) }, ); + let guest_call = WorkItem::GuestCall(GuestCall { + thread, + kind: GuestCallKind::StartExplicit(start_func), + }); if high_priority { self.concurrent_state_mut(store.0) - .push_high_priority(WorkItem::GuestCall(GuestCall { - thread: new_thread, - kind: GuestCallKind::Start(start_func), - })); + .push_high_priority(guest_call); } else { self.concurrent_state_mut(store.0) - .push_low_priority(WorkItem::GuestCall(GuestCall { - thread: new_thread, - kind: GuestCallKind::Start(start_func), - })); + .push_low_priority(guest_call); } Ok(()) @@ -3015,38 +3029,30 @@ impl Instance { pub(crate) fn resume_suspended_thread( self, mut store: StoreContextMut, - thread: GuestThreadIndex, + thread_id: TableId, high_priority: bool, ) -> Result<()> { - let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); let state = self.concurrent_state_mut(store.0); - let suspended_task = state - .get_mut(guest_thread.task)? - .suspended_threads - .remove(&thread); - if !suspended_task.is_some() { - bail!("attempted to resume a thread that is not suspended"); - } - match suspended_task.unwrap() { - SuspendedThreadState::NotStarted { - table_idx, - func_idx, + let thread = state.get_mut(thread_id)?; + + match mem::replace(&mut thread.state, GuestThreadState::Running) { + GuestThreadState::NotStartedExplicit { + start_func_table_idx, + start_func_idx, context, } => { self.start_thread( &mut store, - thread, - table_idx, - func_idx, + thread_id, + start_func_table_idx, + start_func_idx, context, high_priority, )?; } - SuspendedThreadState::Started(fiber) => { - log::trace!( - "resuming thread {thread:?} of task {:?} that was suspended", - guest_thread.task - ); + GuestThreadState::Suspended(fiber) => { + let task_id = state.get_task_id(thread_id)?; + log::trace!("resuming thread {task_id:?}:{thread_id:?} that was suspended"); if high_priority { self.concurrent_state_mut(store.0) .push_high_priority(WorkItem::ResumeFiber(fiber)); @@ -3055,6 +3061,9 @@ impl Instance { .push_low_priority(WorkItem::ResumeFiber(fiber)); } } + _ => { + bail!("cannot resume thread which is not suspended"); + } } Ok(()) } @@ -3062,7 +3071,7 @@ impl Instance { fn pending_cancellation(&self, store: &mut StoreOpaque) -> bool { let state = self.concurrent_state_mut(store); let thread = state.guest_thread.unwrap(); - if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { + if let Some(event) = state.get_task_mut(thread).unwrap().event.take() { assert!(matches!(event, Event::Cancelled)); true } else { @@ -3078,7 +3087,7 @@ impl Instance { caller: RuntimeComponentInstanceIndex, cancellable: bool, yielding: bool, - to_thread: Option, + to_thread: Option>, ) -> Result { // There could be a pending cancellation from a previous uncancellable wait if cancellable && self.pending_cancellation(store.0) { @@ -3139,7 +3148,7 @@ impl Instance { log::trace!("waitable check for {guest_thread:?}; set {set:?}"); let state = self.concurrent_state_mut(store); - let task = state.get_mut(guest_thread.task)?; + let task = state.get_task_mut(guest_thread)?; if wait && task.callback.is_some() { bail!("cannot call `task.wait` from async-lifted export with callback"); @@ -3155,10 +3164,7 @@ impl Instance { && state.get_mut(set)?.ready.is_empty() { if cancellable { - let old = state - .get_thread_mut(&guest_thread)? - .wake_on_cancel - .replace(set); + let old = state.get_mut(guest_thread)?.wake_on_cancel.replace(set); assert!(old.is_none()); } @@ -3177,11 +3183,11 @@ impl Instance { let result = match check { // Deliver any pending events to the guest and return. WaitableCheck::Wait(params) | WaitableCheck::Poll(params) => { - let event = self.id().get_mut(store).get_event( - guest_thread.task, - Some(params.set), - cancellable, - )?; + let task_id = self.concurrent_state_mut(store).get_task_id(guest_thread)?; + let event = + self.id() + .get_mut(store) + .get_event(task_id, Some(params.set), cancellable)?; let (ordinal, handle, result) = if wait { let (event, waitable) = event.unwrap(); @@ -3196,7 +3202,7 @@ impl Instance { } else { log::trace!( "no events ready to deliver via waitable-set.poll to {:?}; set {:?}", - guest_thread.task, + task_id, params.set ); let (ordinal, result) = Event::None.parts(); @@ -3271,6 +3277,7 @@ impl Instance { // Not yet started; cancel and remove from pending let callee_instance = task.instance; + /* todo if !concurrent_state .instance_state(callee_instance) .pending @@ -3282,7 +3289,7 @@ impl Instance { concurrent_state .instance_state(callee_instance) .pending - .retain(|thread, _| thread.task != guest_task); + .retain(|thread, _| thread.task != guest_task);*/ return Ok(Status::StartCancelled as u32); } else if task.lift_result.is_some() { @@ -3294,23 +3301,22 @@ impl Instance { // `Event::Cancelled` if it was already cancelled), but that's // okay -- this should supersede the previous state. task.event = Some(Event::Cancelled); - let threads = task.threads.keys().cloned().collect::>(); - for thread in threads { - if let Some(set) = task.threads.get_mut(&thread).unwrap().wake_on_cancel.take() + for thread in task.threads.clone() { + if let Some(set) = concurrent_state + .get_mut(thread) + .unwrap() + .wake_on_cancel + .take() { - let instance_thread_index = InstanceGuestThreadIndex { - task: guest_task, - thread, - }; let item = match concurrent_state .get_mut(set)? .waiting - .remove(&instance_thread_index) + .remove(&thread) .unwrap() { WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), WaitMode::Callback => WorkItem::GuestCall(GuestCall { - thread: instance_thread_index, + thread: thread, kind: GuestCallKind::DeliverEvent { set: None }, }), }; @@ -3357,7 +3363,7 @@ impl Instance { let state = self.concurrent_state_mut(store); let caller = state.guest_thread.unwrap(); let old_set = waitable.common(state)?.set; - let set = state.get_mut(caller.task)?.sync_call_set; + let set = state.get_task_mut(caller)?.sync_call_set; waitable.join(state, Some(set))?; self.suspend( store, @@ -3421,34 +3427,6 @@ impl Instance { self.id().get(store).check_may_leave(caller)?; self.concurrent_state_mut(store).context_set(slot, value) } - - fn delete_task_if_all_threads_exited( - &self, - store: &mut StoreOpaque, - task: TableId, - ) -> Result { - let state = self.concurrent_state_mut(store); - let guest_task = state.get_mut(task)?; - // We consider all threads to have exited if every thread that remains - // (which may be zero) is suspended, because these threads will never be - // resumed again. - let all_threads_exited = guest_task - .threads - .keys() - .all(|thread| guest_task.suspended_threads.contains_key(thread)); - if all_threads_exited { - log::trace!("deleting guest task {task:?} as all threads exited"); - // TODO: delete any fibers - Waitable::Guest(task).delete_from(state)?; - Ok(true) - } else { - log::trace!( - "not deleting guest task {task:?} as {} threads remain", - guest_task.threads.len() - ); - Ok(false) - } - } } /// Trait representing component model ABI async intrinsics and fused adapter @@ -3616,11 +3594,15 @@ pub trait VMComponentAsyncStore { caller: RuntimeComponentInstanceIndex, cancellable: bool, yielding: bool, - to_thread: Option, + to_thread: Option>, ) -> Result; /// The `thread.resume-later` intrinsic. - fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()>; + fn thread_resume_later( + &mut self, + instance: Instance, + thread: TableId, + ) -> Result<()>; } /// SAFETY: See trait docs. @@ -3923,7 +3905,7 @@ impl VMComponentAsyncStore for StoreInner { caller: RuntimeComponentInstanceIndex, cancellable: bool, yielding: bool, - to_thread: Option, + to_thread: Option>, ) -> Result { instance.suspension_intrinsic( StoreContextMut(self), @@ -3934,7 +3916,11 @@ impl VMComponentAsyncStore for StoreInner { ) } - fn thread_resume_later(&mut self, instance: Instance, thread: GuestThreadIndex) -> Result<()> { + fn thread_resume_later( + &mut self, + instance: Instance, + thread: TableId, + ) -> Result<()> { instance.resume_suspended_thread(StoreContextMut(self), thread, false) } } @@ -3996,7 +3982,7 @@ enum Caller { /// Another guest thread called the guest task Guest { /// The id of the caller - thread: InstanceGuestThreadIndex, + thread: TableId, /// The instance to use to enforce reentrance rules. /// /// Note that this might not be the same as the instance the caller task @@ -4015,61 +4001,65 @@ struct LiftResult { string_encoding: StringEncoding, } -/// The index for a thread within a guest task. -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -#[repr(transparent)] -pub struct GuestThreadIndex(u32); -impl GuestThreadIndex { - pub fn rep(&self) -> u32 { - self.0 - } - pub fn from_u32(i: u32) -> Self { - Self(i) - } +enum GuestThreadState { + NotStartedImplicit, + NotStartedExplicit { + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + }, + Running, + Suspended(StoreFiber<'static>), + Pending, + Completed, } - -const MAIN_GUEST_THREAD_INDEX: GuestThreadIndex = GuestThreadIndex(0); pub(crate) struct GuestThread { - /// The thread index within the owning guest task. - index: GuestThreadIndex, /// Context-local state used to implement the `context.{get,set}` /// intrinsics. context: [u32; 2], - /// The owning guest task. Logically, a `GuestThread` always belongs to - /// exactly one `GuestTask`, but the `TableId` for that task is not available - /// until the `GuestTask` is stored in the state. As such, this is an `Option` - /// which is set when the `GuestTask` is stored. It is an invariant that this - /// be `Some` whenever operations on `GuestThread`s are performed. - parent_task: Option>, + /// The owning guest task. + parent_task: TableId, /// If present, indicates that the thread is currently waiting on the /// specified set but may be cancelled and woken immediately. wake_on_cancel: Option>, + state: GuestThreadState, } -/// The index for a thread within a component instance. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct InstanceGuestThreadIndex { - task: TableId, - thread: GuestThreadIndex, -} - -impl InstanceGuestThreadIndex { - fn is_explicit_thread(&self) -> bool { - self.thread != MAIN_GUEST_THREAD_INDEX +impl GuestThread { + fn new_implicit(parent_task: TableId) -> Self { + Self { + context: [0; 2], + parent_task: parent_task, + wake_on_cancel: None, + state: GuestThreadState::NotStartedImplicit, + } } -} -impl GuestThread { - fn new(index: GuestThreadIndex) -> Self { + fn new_explicit( + parent_task: TableId, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Self { Self { - index, context: [0; 2], - parent_task: None, + parent_task: parent_task, wake_on_cancel: None, + state: GuestThreadState::NotStartedExplicit { + start_func_table_idx, + start_func_idx, + context, + }, } } } +impl TableDebug for GuestThread { + fn type_name() -> &'static str { + "GuestThread" + } +} + /// Represents a pending guest task. pub(crate) struct GuestTask { /// See `WaitableCommon` @@ -4118,17 +4108,8 @@ pub(crate) struct GuestTask { function_index: Option, /// Whether or not the task has exited. exited: bool, - /// Threads belonging to this task, either created implicitly on a call to - /// an export, or explicitly via `thread.new_indirect`. - /// - /// The implicit thread is always present, and has index 0 (MAIN_GUEST_THREAD_INDEX). - threads: HashMap, - /// Threads that are suspended, either because they were created with - /// `thread.new_indirect` but not yet started, or because they explicitly - /// suspended themselves with `thread.suspend` or `thread.switch-to`. - suspended_threads: BTreeMap, - /// The next thread index to use when creating a new thread. - next_thread_index: GuestThreadIndex, + /// Threads belonging to this task + threads: Vec>, } impl GuestTask { @@ -4159,43 +4140,10 @@ impl GuestTask { event: None, function_index: None, exited: false, - threads: HashMap::from([( - MAIN_GUEST_THREAD_INDEX, - GuestThread::new(MAIN_GUEST_THREAD_INDEX), - )]), - suspended_threads: BTreeMap::new(), - next_thread_index: GuestThreadIndex(MAIN_GUEST_THREAD_INDEX.0 + 1), + threads: Vec::new(), }) } - /// Create a new thread within this guest task that starts suspended - fn new_thread( - &mut self, - start_func_table_idx: RuntimeTableIndex, - start_func_idx: u32, - context: i32, - ) -> GuestThreadIndex { - let thread_index = self.next_thread_index; - self.next_thread_index = GuestThreadIndex(self.next_thread_index.0 + 1); - self.threads - .insert(thread_index, GuestThread::new(thread_index)); - self.suspended_threads.insert( - thread_index, - SuspendedThreadState::NotStarted { - table_idx: start_func_table_idx, - func_idx: start_func_idx, - context, - }, - ); - thread_index - } - - fn thread_completed(&mut self, thread_index: GuestThreadIndex) { - let present = self.threads.remove(&thread_index).is_some(); - assert!(present); - self.suspended_threads.remove(&thread_index); - } - /// Dispose of this guest task, reparenting any pending subtasks to the /// caller. fn dispose(self, state: &mut ConcurrentState, me: TableId) -> Result<()> { @@ -4218,7 +4166,7 @@ impl GuestTask { thread, instance: runtime_instance, } => { - let task_mut = state.get_mut(thread.task)?; + let task_mut = state.get_task_mut(*thread)?; let present = task_mut.subtasks.remove(&me); assert!(present); @@ -4387,7 +4335,7 @@ impl Waitable { if let Some(set) = self.common(state)?.set { state.get_mut(set)?.ready.insert(*self); if let Some((thread, mode)) = state.get_mut(set)?.waiting.pop_first() { - let wake_on_cancel = state.get_thread_mut(&thread)?.wake_on_cancel.take(); + let wake_on_cancel = state.get_mut(thread)?.wake_on_cancel.take(); assert!(wake_on_cancel.is_none() || wake_on_cancel == Some(set)); let item = match mode { @@ -4488,7 +4436,7 @@ struct WaitableSet { /// Which waitables in this set have pending events, if any. ready: BTreeSet, /// Which guest threads are currently waiting on this set, if any. - waiting: BTreeMap, + waiting: BTreeMap, WaitMode>, } impl TableDebug for WaitableSet { @@ -4544,23 +4492,14 @@ struct InstanceState { do_not_enter: bool, /// Pending calls for this instance which require `Self::backpressure` to be /// `true` and/or `Self::do_not_enter` to be false before they can proceed. - pending: BTreeMap, -} - -enum SuspendedThreadState { - Started(StoreFiber<'static>), - NotStarted { - table_idx: RuntimeTableIndex, - func_idx: u32, - context: i32, - }, + pending: BTreeMap, GuestCallKind>, } /// Represents the Component Model Async state of a top-level component instance /// (i.e. a `super::ComponentInstance`). pub struct ConcurrentState { /// The currently running guest thread, if any. - guest_thread: Option, + guest_thread: Option>, /// The set of pending host and background tasks, if any. /// @@ -4703,24 +4642,20 @@ impl ConcurrentState { self.table.get_mut().get_mut(&Resource::from(id)) } - fn get_thread( + fn get_task_id( &mut self, - idx: &InstanceGuestThreadIndex, - ) -> Result<&GuestThread, ResourceTableError> { - self.get_mut(idx.task)? - .threads - .get(&idx.thread) - .ok_or(ResourceTableError::NotPresent) + id: TableId, + ) -> Result, ResourceTableError> { + let thread = self.get_mut(id)?; + Ok(thread.parent_task) } - fn get_thread_mut( + fn get_task_mut( &mut self, - idx: &InstanceGuestThreadIndex, - ) -> Result<&mut GuestThread, ResourceTableError> { - self.get_mut(idx.task)? - .threads - .get_mut(&idx.thread) - .ok_or(ResourceTableError::NotPresent) + id: TableId, + ) -> Result<&mut GuestTask, ResourceTableError> { + self.get_task_id(id) + .and_then(|task_id| self.get_mut(task_id)) } pub fn add_child( @@ -4787,16 +4722,18 @@ impl ConcurrentState { // that task or an ancestor of that task, in which case this would be a // constant time check. loop { - match &self.get_mut(guest_task).unwrap().caller { + let next_thread = match &self.get_mut(guest_task).unwrap().caller { Caller::Host { .. } => break true, Caller::Guest { thread, instance } => { if *instance == guest_instance { break false; } else { - guest_task = thread.task; + *thread } } - } + }; + let task = self.get_task_id(next_thread).unwrap(); + guest_task = task; } } @@ -4857,7 +4794,7 @@ impl ConcurrentState { /// Implements the `context.get` intrinsic. pub(crate) fn context_get(&mut self, slot: u32) -> Result { let thread = self.guest_thread.unwrap(); - let val = self.get_thread(&thread)?.context[usize::try_from(slot).unwrap()]; + let val = self.get_mut(thread)?.context[usize::try_from(slot).unwrap()]; log::trace!("context_get {thread:?} slot {slot} val {val:#x}"); Ok(val) } @@ -4866,7 +4803,7 @@ impl ConcurrentState { pub(crate) fn context_set(&mut self, slot: u32, val: u32) -> Result<()> { let thread = self.guest_thread.unwrap(); log::trace!("context_set {thread:?} slot {slot} val {val:#x}"); - self.get_thread_mut(&thread)?.context[usize::try_from(slot).unwrap()] = val; + self.get_mut(thread)?.context[usize::try_from(slot).unwrap()] = val; Ok(()) } @@ -4875,8 +4812,7 @@ impl ConcurrentState { Ok(self .guest_thread .ok_or_else(|| anyhow!("no current thread"))? - .thread - .0) + .rep()) } fn options(&self, options: OptionsIndex) -> &CanonicalOptions { @@ -4974,8 +4910,10 @@ enum WaitableCheck { pub(crate) struct PreparedCall { /// The guest export to be called handle: Func, + /// The guest task created by `prepare_call` + task: TableId, /// The guest thread created by `prepare_call` - thread: InstanceGuestThreadIndex, + thread: TableId, /// The number of lowered core Wasm parameters to pass to the call. param_count: usize, /// The `oneshot::Receiver` to which the result of the call will be @@ -4992,7 +4930,7 @@ impl PreparedCall { pub(crate) fn task_id(&self) -> TaskId { TaskId { handle: self.handle, - task: self.thread.task, + task: self.task, } } } @@ -5104,19 +5042,13 @@ pub(crate) fn prepare_call( task.function_index = Some(handle.index()); let task = state.push(task)?; - state - .get_mut(task)? - .threads - .get_mut(&MAIN_GUEST_THREAD_INDEX) - .unwrap() - .parent_task = Some(task); + let thread = state.push(GuestThread::new_implicit(task))?; + state.get_mut(task)?.threads.push(thread); Ok(PreparedCall { handle, - thread: InstanceGuestThreadIndex { - task, - thread: MAIN_GUEST_THREAD_INDEX, - }, + task, + thread, param_count, rx, exit_rx, @@ -5160,7 +5092,7 @@ pub(crate) fn queue_call( fn queue_call0( store: StoreContextMut, handle: Func, - guest_thread: InstanceGuestThreadIndex, + guest_thread: TableId, param_count: usize, ) -> Result<()> { let (options, flags, _ty, raw_options) = handle.abi_info(store.0); diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 2ccb3200f8bb..2b68ee4c083e 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -2,7 +2,7 @@ use crate::component::Instance; #[cfg(feature = "component-model-async")] -use crate::component::concurrent::GuestThreadIndex; +use crate::component::concurrent::table::TableId; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; @@ -1375,6 +1375,7 @@ fn thread_index(store: &mut dyn VMStore, instance: Instance) -> Result { fn thread_new_indirect( store: &mut dyn VMStore, instance: Instance, + caller: u32, func_ty_id: u32, func_table_idx: u32, func_idx: u32, @@ -1382,6 +1383,7 @@ fn thread_new_indirect( ) -> Result { instance.thread_new_indirect( store, + RuntimeComponentInstanceIndex::from_u32(caller), TypeFuncIndex::from_u32(func_ty_id), RuntimeTableIndex::from_u32(func_table_idx), func_idx, @@ -1402,7 +1404,7 @@ fn thread_switch_to( RuntimeComponentInstanceIndex::from_u32(caller), cancellable != 0, false, - Some(GuestThreadIndex::from_u32(thread_idx)), + Some(TableId::new(thread_idx)), ) } @@ -1426,7 +1428,7 @@ fn thread_suspend( fn thread_resume_later(store: &mut dyn VMStore, instance: Instance, thread_idx: u32) -> Result<()> { store .component_async_store() - .thread_resume_later(instance, GuestThreadIndex::from_u32(thread_idx)) + .thread_resume_later(instance, TableId::new(thread_idx)) } #[cfg(feature = "component-model-async")] @@ -1442,6 +1444,6 @@ fn thread_yield_to( RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, true, - Some(GuestThreadIndex::from_u32(thread_idx)), + Some(TableId::new(thread_idx)), ) } diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs index 30d46f87d6cd..f64af0c6b7d1 100644 --- a/tests/all/component_model/threading.rs +++ b/tests/all/component_model/threading.rs @@ -10,16 +10,24 @@ async fn threads() -> Result<()> { let builder = FmtSubscriber::builder() .with_writer(std::io::stderr) .with_env_filter(EnvFilter::from_env("WASMTIME_LOG")) - .with_ansi(std::io::stderr().is_terminal()); + .with_ansi(std::io::stderr().is_terminal()) + .init(); let mut config = Config::new(); config.async_support(true); config.wasm_component_model_async(true); config.wasm_component_model_threading(true); + config.wasm_component_model_async_stackful(true); + config.wasm_component_model_async_builtins(true); let engine = Engine::new(&config)?; let component = Component::new( &engine, - r#" - (component + r#";;! component_model_async = true +;;! component_model_threading = true + +;; Tests for basic functioning of all threading builtins with the implicit thread + one explicit thread +;; Switches between threads using all of the different threading intrinsics. + +(component ;; Defines the table for the thread start function (core module $libc (table (export "__indirect_function_table") 1 funcref)) @@ -27,19 +35,34 @@ async fn threads() -> Result<()> { (core module $m ;; Import the threading builtins and the table from libc (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) (import "" "thread.resume-later" (func $thread-resume-later (param i32))) (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) ;; A global that we will set from the spawned thread (global $g (mut i32) (i32.const 0)) + (global $main-thread-index (mut i32) (i32.const 0)) - ;; The thread entry point, which sets the global to the value passed in + ;; The thread entry point, which sets the global to incrementing values starting from the context value (func $thread-start (param i32) - local.get 0 - global.set $g) + ;; Set the global to the context value + (global.set $g (local.get 0)) + ;; The main thread switched to us, so is no longer scheduled, so we explicitly schedule it + (call $thread-resume-later (global.get $main-thread-index)) + ;; Yield back to the main thread (since that is the only other one) + (drop (call $thread-yield) + ;; Increment the global + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; The main thread will have explicitly requested suspension, so yield to it directly + (drop (call $thread-yield-to (global.get $main-thread-index))) + ;; Increment the global again + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; Reschedule the main thread so that it runs after we exit + (call $thread-resume-later (global.get $main-thread-index)))) (export "thread-start" (func $thread-start)) ;; Initialize the function table with our thread-start function; this will be @@ -49,12 +72,24 @@ async fn threads() -> Result<()> { ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 ;; as the context value, and then yields to it (func (export "run") (result i32) - i32.const 0 - i32.const 42 - call $thread-new-indirect - call $thread-yield-to - drop - global.get $g)) + ;; Store the main thread's index for the spawned thread to yield to + (global.set $main-thread-index (call $thread-index)) + ;; Create a new thread, which starts suspended, and switch to it + (drop + (call $thread-switch-to + (call $thread-new-indirect (i32.const 0) (i32.const 42)))) + ;; After the thread yields back to us, check that the global was set to 42 + (if (i32.ne (global.get $g) (i32.const 42)) (then unreachable)) + ;; Suspend ourselves, which will cause the spawned thread to run + (drop (call $thread-suspend)) + ;; The spawned thread will resume us after incrementing the global, so check that it is now 43 + (if (i32.ne (global.get $g) (i32.const 43)) (then unreachable)) + ;; Suspend again, which will cause the spawned thread to run again + (drop (call $thread-suspend)) + ;; The spawned thread will reschedule us before it exits, so when we resume here the global should be 44 + (if (i32.ne (global.get $g) (i32.const 44)) (then unreachable)) + ;; Return success + (i32.const 42))) ;; Instantiate the libc module to get the table (core instance $libc (instantiate $libc)) @@ -65,18 +100,22 @@ async fn threads() -> Result<()> { (core func $thread-new-indirect (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) (core func $thread-yield (canon thread.yield)) + (core func $thread-index (canon thread.index)) (core func $thread-yield-to (canon thread.yield-to)) (core func $thread-resume-later (canon thread.resume-later)) (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) ;; Instantiate the main module (core instance $i ( instantiate $m - (with "" (instance + (with "" (instance (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) (export "thread.yield-to" (func $thread-yield-to)) (export "thread.yield" (func $thread-yield)) (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) (export "thread.resume-later" (func $thread-resume-later)))) (with "libc" (instance $libc))))