From 7e88888981de69d74f7f3660bd84b154dd118f58 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 16 Sep 2025 09:46:05 +0100 Subject: [PATCH 01/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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/63] 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)))) From 67c3e90b03d2b9d262344d413a8f48d856564e61 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 7 Oct 2025 15:24:42 +0100 Subject: [PATCH 14/63] Deletion almost there --- crates/environ/src/component.rs | 31 ++ crates/environ/src/fact.rs | 1 + crates/environ/src/fact/trampoline.rs | 20 +- .../src/runtime/component/concurrent.rs | 239 +++++++--- .../src/runtime/component/func/typed.rs | 17 +- .../src/runtime/vm/component/libcalls.rs | 2 + tests/all/component_model/async.rs | 354 +++++++++++++- tests/all/component_model/func.rs | 83 ++++ tests/all/component_model/threading.rs | 435 +++++++++++++----- .../task-deletion.wast | 305 ++++++++++++ 10 files changed, 1295 insertions(+), 192 deletions(-) create mode 100644 tests/misc_testsuite/component-model-threading/task-deletion.wast diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index dec960fee091..acbbc5352ce8 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -51,6 +51,36 @@ pub const PREPARE_ASYNC_WITH_RESULT: u32 = u32::MAX - 1; /// callee is an async-lifted export. pub const START_FLAG_ASYNC_CALLEE: i32 = 1 << 0; +/// The ABI used for lowering the import involved in a `prepare_call` call +#[derive(Copy, Clone)] +enum LowerABI { + Synchronous = 0, + Asynchronous = 1, +} + +/// The ABI used for lifting the export involved in a `prepare_call` call +#[derive(Copy, Clone)] +pub enum LiftABI { + /// Synchronous ABI + Synchronous = 0, + /// Stackless Asynchronous ABI + Stackless = 1, + /// Stackful Asynchronous ABI + Stackful = 2, +} + +impl LiftABI { + /// Convert from an integer representation to a LiftABI + pub fn from_u32(abi: u32) -> LiftABI { + match abi { + 0 => LiftABI::Synchronous, + 1 => LiftABI::Stackless, + 2 => LiftABI::Stackful, + _ => panic!("Unexpected lift ABI type"), + } + } +} + mod artifacts; mod info; mod names; @@ -132,6 +162,7 @@ macro_rules! foreach_builtin_component_function { task_return_type: u32, string_encoding: u32, result_count_or_max_if_async: u32, + lift_abi: u32, storage: ptr_u8, storage_len: size ) -> bool; diff --git a/crates/environ/src/fact.rs b/crates/environ/src/fact.rs index 9e97dc4274e7..9e4529d8b57c 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -49,6 +49,7 @@ pub static PREPARE_CALL_FIXED_PARAMS: &[ValType] = &[ ValType::I32, // task_return_type ValType::I32, // string_encoding ValType::I32, // result_count_or_max_if_async + ValType::I32, // lift_abi ]; /// Representation of an adapter module. diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 799e26036168..61a3a220330b 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -17,11 +17,11 @@ use crate::component::{ CanonicalAbiInfo, ComponentTypesBuilder, FLAG_MAY_ENTER, FLAG_MAY_LEAVE, FixedEncoding as FE, - FlatType, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, PREPARE_ASYNC_NO_RESULT, - PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, Transcode, - TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFlagsIndex, TypeFutureTableIndex, - TypeListIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, - TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, VariantInfo, + FlatType, InterfaceType, LiftABI, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, + PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, + Transcode, TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFlagsIndex, + TypeFutureTableIndex, TypeListIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTableIndex, + TypeResultIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, VariantInfo, }; use crate::fact::signature::Signature; use crate::fact::transcode::Transcoder; @@ -572,6 +572,16 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(I32Const(PREPARE_ASYNC_NO_RESULT.cast_signed())); } } + let lift_abi = if adapter.lift.options.async_ { + if adapter.lift.options.callback.is_some() { + LiftABI::Stackless + } else { + LiftABI::Stackful + } + } else { + LiftABI::Synchronous + }; + self.instruction(I32Const(lift_abi as i32)); // forward all our own arguments on to the host stub for index in 0..lower_sig.params.len() { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index f37cedf76d6b..66d2a15d1102 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -82,9 +82,10 @@ use std::sync::Arc; use std::task::{Context, Poll, Waker}; use std::vec::Vec; use table::{TableDebug, TableId}; +use wasmtime_environ::Trap; use wasmtime_environ::component::{ - CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, - OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, + CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, LiftABI, LowerABI, MAX_FLAT_PARAMS, + MAX_FLAT_RESULTS, OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, RuntimeComponentInstanceIndex, RuntimeTableIndex, StringEncoding, TypeComponentGlobalErrorContextTableIndex, TypeComponentLocalErrorContextTableIndex, TypeFuncIndex, TypeFutureTableIndex, TypeStreamTableIndex, TypeTupleIndex, @@ -741,10 +742,7 @@ impl ComponentInstance { 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 { - return Err(anyhow!(crate::Trap::NoAsyncResult)); - } + if !task.returned_or_cancelled() { if initial_call { // Notify any current or future waiters that this subtask has // started. @@ -769,14 +767,19 @@ impl ComponentInstance { match code { callback_code::EXIT => { + let task_id = state.get_task_id(guest_thread)?; + log::trace!("implicit thread {task_id:?}:{guest_thread:?} completed"); + state.get_mut(guest_thread)?.state = GuestThreadState::Completed; + state.get_mut(task_id)?.threads.remove(&guest_thread); + if state.get_mut(task_id)?.threads.is_empty() + && !state.get_mut(task_id)?.returned_or_cancelled() + { + bail!(Trap::NoAsyncResult); + } 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 {:?}", task_id); + Caller::Host { .. } => { + if state.get_mut(task_id)?.ready_to_delete() { Waitable::Guest(task_id).delete_from(state)?; } } @@ -978,7 +981,7 @@ impl ComponentInstance { } else { let id = TableId::::new(rep); let task = concurrent_state.get_mut(id)?; - if task.lift_result.is_some() { + if !task.returned_or_cancelled() { bail!("cannot drop a subtask which has not yet resolved"); } if let Caller::Guest { instance, .. } = &task.caller { @@ -1649,10 +1652,17 @@ impl Instance { old_task: Option>, ) -> Result<()> { let task = self.concurrent_state_mut(store).get_mut(guest_task)?; - if Some(guest_task) != old_task && task.lift_result.is_some() { + + if !task.returned_or_cancelled() { log::trace!("push call context for {guest_task:?}"); let call_context = task.call_context.take().unwrap(); store.component_resource_state().0.push(call_context); + } else { + log::trace!( + "not pushing call context: old={:?}, new={:?}", + old_task, + guest_task + ) } Ok(()) } @@ -1666,12 +1676,10 @@ impl Instance { guest_task: TableId, old_task: Option>, ) -> Result<()> { - if Some(guest_task) != old_task - && self - .concurrent_state_mut(store) - .get_mut(guest_task)? - .lift_result - .is_some() + if !self + .concurrent_state_mut(store) + .get_mut(guest_task)? + .returned_or_cancelled() { log::trace!("pop call context for {guest_task:?}"); let call_context = Some(store.component_resource_state().0.pop().unwrap()); @@ -1885,18 +1893,29 @@ impl Instance { // over must be valid. let storage = call(store, instance)?; + // This is a callback-less call, so the implicit thread has now completed + instance + .concurrent_state_mut(store) + .get_mut(guest_thread)? + .state = GuestThreadState::Completed; + instance + .concurrent_state_mut(store) + .get_mut(task_id)? + .threads + .remove(&guest_thread); + if async_ { - // This is a callback-less (i.e. stackful) async-lifted - // export, so there is no post-return function, and - // either `task.return` or `task.cancel` should have - // been called. if instance .concurrent_state_mut(store) .get_mut(task_id)? - .lift_result - .is_some() + .threads + .is_empty() + && !instance + .concurrent_state_mut(store) + .get_mut(task_id)? + .returned_or_cancelled() { - return Err(anyhow!(crate::Trap::NoAsyncResult)); + bail!(Trap::NoAsyncResult); } } else { // This is a sync-lifted export, so now is when we lift the @@ -1974,16 +1993,12 @@ impl Instance { instance.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; let state = instance.concurrent_state_mut(store); - state.get_mut(guest_thread)?.state = GuestThreadState::Completed; let task = state.get_mut(task_id)?; match &task.caller { - Caller::Host { - remove_task_automatically, - .. - } => { - if *remove_task_automatically { - //todo + Caller::Host { .. } => { + if task.ready_to_delete() { + Waitable::Guest(task_id).delete_from(state)?; } } Caller::Guest { .. } => { @@ -2027,6 +2042,7 @@ impl Instance { memory: *mut VMMemoryDefinition, string_encoding: u8, caller_info: CallerInfo, + lift_abi: LiftABI, ) -> Result<()> { self.id().get(store.0).check_may_leave(caller_instance)?; @@ -2144,8 +2160,8 @@ impl Instance { let state = instance.concurrent_state_mut(store.0); let thread = state.guest_thread.unwrap(); if sync_caller { - state.get_task_mut(thread)?.sync_result = - Some(if let ResultInfo::Stack { result_count } = &result_info { + state.get_task_mut(thread)?.sync_result = SyncResult::Produced( + if let ResultInfo::Stack { result_count } = &result_info { match result_count { 0 => None, 1 => Some(my_src[0]), @@ -2153,7 +2169,8 @@ impl Instance { } } else { None - }); + }, + ); } Ok(Box::new(DummyResult) as Box) }), @@ -2167,12 +2184,13 @@ impl Instance { }, None, callee_instance, + lift_abi, )?; let guest_task = state.push(new_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); + state.get_mut(guest_task)?.threads.insert(guest_thread); if let Some(old_thread) = old_thread { if !state.may_enter(guest_task) { @@ -2357,6 +2375,7 @@ impl Instance { let state = self.concurrent_state_mut(store.0); + log::trace!("taking event for {guest_task:?}"); let event = guest_waitable.take_event(state)?; let Some(Event::Subtask { status }) = event else { unreachable!(); @@ -2389,6 +2408,10 @@ impl Instance { guest_waitable.join(state, old_set)?; + // 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:?}"); + if let Some(storage) = storage { // The caller used a sync-lowered import to call an async-lifted // export, in which case the result, if any, has been stashed in @@ -2400,19 +2423,13 @@ impl Instance { } if task.exited { - Waitable::Guest(guest_task).delete_from(state)?; + if task.ready_to_delete() { + Waitable::Guest(guest_task).delete_from(state); + } } - } else { - // This means the callee failed to call either `task.return` or - // `task.cancel` before exiting. - return Err(anyhow!(crate::Trap::NoAsyncResult)); } } - // 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)) } @@ -2821,7 +2838,6 @@ impl Instance { .get_mut(guest_task)? .function_index .unwrap(); - self.id() .get_mut(store) .post_return_arg_set(function_index, post_return_arg); @@ -2957,6 +2973,7 @@ impl Instance { let new_thread = GuestThread::new_explicit(parent_task, start_func_table_idx, start_func_idx, context); let thread_id = state.push(new_thread)?; + state.get_mut(parent_task)?.threads.insert(thread_id); log::trace!("new thread with id {thread_id:?} created"); @@ -2964,7 +2981,7 @@ impl Instance { } fn start_thread( - &self, + self, store: &mut StoreContextMut, thread: TableId, start_func_table_idx: RuntimeTableIndex, @@ -2992,6 +3009,14 @@ impl Instance { log::trace!( "thread start: replaced {old_thread:?} with {thread:?} as current thread" ); + let old_task = old_thread.map(|thread| { + instance + .concurrent_state_mut(store) + .get_task_id(thread) + .unwrap() + }); + + self.maybe_push_call_context(store.store_opaque_mut(), task_id, old_task)?; let mut store = token.as_context_mut(store); @@ -3004,8 +3029,21 @@ impl Instance { )? } + self.maybe_pop_call_context(store.0, task_id, old_task)?; + let state = instance.concurrent_state_mut(store.0); + state.get_mut(thread)?.state = GuestThreadState::Completed; + state.get_mut(task_id)?.threads.remove(&thread); + log::trace!("explicit thread {task_id:?}:{thread:?} completed"); + if state.get_mut(task_id)?.threads.is_empty() + && !state.get_mut(task_id)?.returned_or_cancelled() + { + bail!(Trap::NoAsyncResult); + } state.guest_thread = old_thread; + if state.get_mut(task_id)?.ready_to_delete() { + Waitable::Guest(task_id).delete_from(state); + } log::trace!("thread start: restored {old_thread:?} as current thread"); Ok(()) @@ -3270,7 +3308,7 @@ impl Instance { 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() { + if !task.have_lowered_parameters() { task.lower_params = None; task.lift_result = None; @@ -3292,7 +3330,7 @@ impl Instance { .retain(|thread, _| thread.task != guest_task);*/ return Ok(Status::StartCancelled as u32); - } else if task.lift_result.is_some() { + } else if !task.returned_or_cancelled() { // Started, but not yet returned or cancelled; send the // `CANCELLED` event task.cancel_sent = true; @@ -3336,9 +3374,7 @@ impl Instance { let concurrent_state = self.concurrent_state_mut(store); let task = concurrent_state.get_mut(guest_task)?; - if task.lift_result.is_some() { - // Still not yet returned or cancelled; if `async_`, return - // `BLOCKED`; otherwise wait + if !task.returned_or_cancelled() { if async_ { return Ok(BLOCKED); } else { @@ -3453,6 +3489,7 @@ pub trait VMComponentAsyncStore { task_return_type: TypeTupleIndex, string_encoding: u8, result_count: u32, + lift_abi: LiftABI, storage: *mut ValRaw, storage_len: usize, ) -> Result<()>; @@ -3618,6 +3655,7 @@ impl VMComponentAsyncStore for StoreInner { task_return_type: TypeTupleIndex, string_encoding: u8, result_count_or_max_if_async: u32, + lift_abi: LiftABI, storage: *mut ValRaw, storage_len: usize, ) -> Result<()> { @@ -3650,6 +3688,7 @@ impl VMComponentAsyncStore for StoreInner { result_count, }, }, + lift_abi, ) } } @@ -3973,9 +4012,6 @@ enum Caller { /// dropping it when the refcount goes to zero is sufficient to notify /// the receiver. exit_tx: Arc>, - /// If true, remove the task from the concurrent state that owns it - /// automatically after it completes. - remove_task_automatically: bool, /// If true, call `post-return` function (if any) automatically. call_post_return_automatically: bool, }, @@ -4060,6 +4096,24 @@ impl TableDebug for GuestThread { } } +enum SyncResult { + NotProduced, + Produced(Option), + Taken, +} + +impl SyncResult { + fn take(&mut self) -> Option> { + match mem::replace(self, SyncResult::Taken) { + SyncResult::NotProduced => None, + SyncResult::Produced(val) => Some(val), + SyncResult::Taken => { + panic!("attempted to take a synchronous result that was already taken") + } + } + } +} + /// Represents a pending guest task. pub(crate) struct GuestTask { /// See `WaitableCommon` @@ -4081,7 +4135,7 @@ pub(crate) struct GuestTask { call_context: Option, /// A place to stash the lowered result for a sync-to-async call until it /// can be returned to the caller. - sync_result: Option>, + sync_result: SyncResult, /// Whether or not the task has been cancelled (i.e. whether the task is /// permitted to call `task.cancel`). cancel_sent: bool, @@ -4109,10 +4163,27 @@ pub(crate) struct GuestTask { /// Whether or not the task has exited. exited: bool, /// Threads belonging to this task - threads: Vec>, + threads: HashSet>, + /// The ABI used for lifting the export implementing this task + lift_abi: LiftABI, } impl GuestTask { + fn have_lowered_parameters(&self) -> bool { + // We reset `lower_params` after we lower the parameters + self.lower_params.is_none() + } + fn returned_or_cancelled(&self) -> bool { + // We reset `lift_result` after we return or exit + self.lift_result.is_none() + } + fn ready_to_delete(&self) -> bool { + let all_threads_completed = self.threads.is_empty(); + match &self.caller { + Caller::Guest { .. } => all_threads_completed, + Caller::Host { .. } => all_threads_completed, + } + } fn new( state: &mut ConcurrentState, lower_params: RawLower, @@ -4120,6 +4191,7 @@ impl GuestTask { caller: Caller, callback: Option, component_instance: RuntimeComponentInstanceIndex, + lift_abi: LiftABI, ) -> Result { let sync_call_set = state.push(WaitableSet::default())?; @@ -4131,7 +4203,7 @@ impl GuestTask { callback, caller, call_context: Some(CallContext::default()), - sync_result: None, + sync_result: SyncResult::NotProduced, cancel_sent: false, starting_sent: false, subtasks: HashSet::new(), @@ -4140,7 +4212,8 @@ impl GuestTask { event: None, function_index: None, exited: false, - threads: Vec::new(), + threads: HashSet::new(), + lift_abi, }) } @@ -4189,7 +4262,6 @@ impl GuestTask { // once all transitive subtasks of the host call have // exited: exit_tx: exit_tx.clone(), - remove_task_automatically: true, call_post_return_automatically: true, }; } @@ -4594,6 +4666,12 @@ impl ConcurrentState { fibers.push(fiber); } } + } else if let Some(thread) = entry.downcast_mut::() { + if let GuestThreadState::Suspended(fiber) = + mem::replace(&mut thread.state, GuestThreadState::Completed) + { + fibers.push(fiber); + } } } @@ -4944,15 +5022,21 @@ pub(crate) struct TaskId { impl TaskId { /// Remove the specified task from the concurrent state to which it belongs. /// - /// This must be used with care to avoid use-after-delete or double-delete - /// bugs. Specifically, it should only be called on tasks created with the - /// `remove_task_automatically` parameter to `prepare_call` set to `false`, - /// which tells the runtime that the caller is responsible for removing the - /// task from the state; otherwise, it will be removed automatically. Also, - /// it should only be called once for a given task, and only after either + /// Should only be called once for a given task, and only after either /// the task has completed or the instance has trapped. - pub(crate) fn remove(&self, store: StoreContextMut) -> Result<()> { - Waitable::Guest(self.task).delete_from(self.handle.instance().concurrent_state_mut(store.0)) + pub(crate) fn remove_if_not_lowered_params(&self, store: StoreContextMut) -> Result<()> { + if self + .handle + .instance() + .concurrent_state_mut(store.0) + .get_mut(self.task)? + .lower_params + .is_some() + { + Waitable::Guest(self.task) + .delete_from(self.handle.instance().concurrent_state_mut(store.0))? + } + Ok(()) } } @@ -4965,7 +5049,6 @@ pub(crate) fn prepare_call( mut store: StoreContextMut, handle: Func, param_count: usize, - remove_task_automatically: bool, call_post_return_automatically: bool, lower_params: impl FnOnce(Func, StoreContextMut, &mut [MaybeUninit]) -> Result<()> + Send @@ -4986,6 +5069,16 @@ pub(crate) fn prepare_call( let string_encoding = options.string_encoding(); let token = StoreToken::new(store.as_context_mut()); let state = handle.instance().concurrent_state_mut(store.0); + let async_ = options.async_(); + let lift_abi = if async_ { + if callback.is_some() { + LiftABI::Stackless + } else { + LiftABI::Stackful + } + } else { + LiftABI::Synchronous + }; assert!(state.guest_thread.is_none()); @@ -5010,7 +5103,6 @@ pub(crate) fn prepare_call( Caller::Host { tx: Some(tx), exit_tx: Arc::new(exit_tx), - remove_task_automatically, call_post_return_automatically, }, callback.map(|callback| { @@ -5038,12 +5130,13 @@ pub(crate) fn prepare_call( ) as CallbackFn }), component_instance, + lift_abi, )?; task.function_index = Some(handle.index()); let task = state.push(task)?; let thread = state.push(GuestThread::new_implicit(task))?; - state.get_mut(task)?.threads.push(thread); + state.get_mut(task)?.threads.insert(thread); Ok(PreparedCall { handle, diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index faa10f0cd57e..642a79219029 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -193,7 +193,7 @@ where let ptr = SendSyncPtr::from(NonNull::from(¶ms).cast::()); let prepared = - self.prepare_call(store.as_context_mut(), true, false, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), 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 @@ -208,9 +208,10 @@ where // Note the use of `RemoveOnDrop` in the code that follows // this closure, which ensures that the task will be removed // from the concurrent state to which it belongs when the - // containing `Future` is dropped, thereby ensuring that - // this closure will never be called if it hasn't already, - // meaning it will never see a dangling pointer. + // containing `Future` is dropped, so long as the parameters + // have not yet been lowered. This ensures that this closure will + // never be called if it hasn't already, meaning it will never see + // a dangling pointer. let params = unsafe { ptr.cast::().as_ref() }; Self::lower_args(cx, ty, dst, params) })?; @@ -222,7 +223,9 @@ 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_if_not_lowered_params(self.store.as_context_mut()) + .unwrap(); } } @@ -282,7 +285,7 @@ where ); let prepared = - self.prepare_call(store.as_context_mut(), true, true, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), true, move |cx, ty, dst| { Self::lower_args(cx, ty, dst, ¶ms) })?; concurrent::queue_call(store, prepared) @@ -319,7 +322,6 @@ where fn prepare_call( self, store: StoreContextMut<'_, T>, - remove_task_automatically: bool, call_post_return_automatically: bool, lower: impl FnOnce( &mut LowerContext, @@ -349,7 +351,6 @@ where store, self.func, param_count, - remove_task_automatically, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 2b68ee4c083e..bdc77c4a5f57 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -860,6 +860,7 @@ unsafe fn prepare_call( task_return_type: u32, string_encoding: u32, result_count_or_max_if_async: u32, + lift_abi: u32, storage: *mut u8, storage_len: usize, ) -> Result<()> { @@ -874,6 +875,7 @@ unsafe fn prepare_call( TypeTupleIndex::from_u32(task_return_type), u8::try_from(string_encoding).unwrap(), result_count_or_max_if_async, + LiftABI::from_u32(lift_abi), storage.cast::(), storage_len, ) diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index dee8ed514501..bdcf146eacb9 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -1,6 +1,6 @@ use crate::async_functions::{PollOnce, execute_across_threads}; use anyhow::Result; -use wasmtime::component::*; +use wasmtime::{AsContextMut, Config, component::*}; use wasmtime::{Engine, Store, StoreContextMut, Trap}; use wasmtime_component_util::REALLOC_AND_FREE; @@ -314,3 +314,355 @@ async fn drop_resource_async() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn task_deletion() -> Result<()> { + 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 + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start functions, of which there are two + (core module $libc + (table (export "__indirect_function_table") 3 funcref)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task-return (param i32))) + (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))) + (import "" "thread.resume-later" (func $thread-resume-later (param 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 3 funcref)) + + ;; Indices into the function table for the thread start functions + (global $call-return-ftbl-idx i32 (i32.const 0)) + (global $suspend-ftbl-idx i32 (i32.const 1)) + (global $yield-loop-ftbl-idx i32 (i32.const 2)) + + (func $call-return (param i32) + (call $task-return (local.get 0))) + + (func $suspend (param i32) + (drop (call $thread-suspend))) + + (func $yield-loop (param i32) + (loop $top + (drop (call $thread-yield)) + (br $top))) + + (func (export "explicit-thread-calls-return-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42)))) + + (func (export "explicit-thread-calls-return-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42))) + (i32.const 0 (; EXIT ;))) + + (func (export "cb") (param i32 i32 i32) (result i32) + (unreachable)) + + (func (export "explicit-thread-suspends-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-suspends-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-suspends-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0)) + + (func (export "explicit-thread-yield-loops-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-yield-loops-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-yield-loops-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0 (; EXIT ;))) + + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; call-return-ftbl-idx ;)) func $call-return) + (elem (table $indirect-function-table) (i32.const 1 (; suspend-ftbl-idx ;)) func $suspend) + (elem (table $indirect-function-table) (i32.const 2 (; yield-loop-ftbl-idx ;)) func $yield-loop) + ) + + ;; 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-return (canon task.return (result u32))) + (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-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 $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.return" (func $task-return)) + (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-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)) + (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 "explicit-thread-calls-return-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackful") async)) + (func (export "explicit-thread-calls-return-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-suspends-sync") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-sync"))) + (func (export "explicit-thread-suspends-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackful") async)) + (func (export "explicit-thread-suspends-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-yield-loops-sync") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-sync"))) + (func (export "explicit-thread-yield-loops-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackful") async)) + (func (export "explicit-thread-yield-loops-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackless") async (callback (func $cm "cb")))) + ) + + (component $D + (import "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result u32))) + (import "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result u32))) + (import "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result u32))) + (import "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result u32))) + (import "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result u32))) + (import "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result u32))) + (import "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result u32))) + (import "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (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))) + ;; sync lowered + (import "" "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result i32))) + (import "" "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result i32))) + (import "" "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result i32))) + (import "" "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result i32))) + (import "" "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result i32))) + (import "" "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result i32))) + (import "" "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result i32))) + (import "" "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result i32))) + ;; async lowered + (import "" "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async (param 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 "" "thread.yield" (func $thread-yield (result i32))) + + (func $check (param i32) + (if (i32.ne (local.get 0) (i32.const 42)) + (then unreachable)) + ) + + (func $check-async (param i32) + (local $retp i32) (local $ws i32) (local $ws-retp i32) + (local.set $retp (i32.const 8)) + (local.set $ws-retp (i32.const 16)) + (local.set $ws (call $waitable-set.new)) + + (if (i32.eq (i32.and (local.get 0) (i32.const 0xF)) (i32.const 2 (; RETURNED ;))) + (then (call $check (i32.load (local.get $retp)))) + (else + (call $waitable.join (i32.shr_u (local.get 0) (i32.const 4)) (local.get $ws)) + (drop (call $waitable-set.wait (local.get $ws) (local.get $ws-retp))) + (call $check (i32.load (local.get $retp))))) + ) + + (func $run (export "run") (result i32) + (local $retp i32) + (local.set $retp (i32.const 8)) + (call $check (call $explicit-thread-calls-return-stackless)) + (call $check (call $explicit-thread-calls-return-stackful)) + (call $check (call $explicit-thread-suspends-sync)) + (call $check (call $explicit-thread-suspends-stackful)) + (call $check (call $explicit-thread-suspends-stackless)) + (call $check (call $explicit-thread-yield-loops-sync)) + (call $check (call $explicit-thread-yield-loops-stackful)) + (call $check (call $explicit-thread-yield-loops-stackless)) + + (call $check-async (call $explicit-thread-calls-return-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-calls-return-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackless-async (local.get $retp))) + + (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 $thread.yield (canon thread.yield)) + ;; sync lowered + (canon lower (func $explicit-thread-calls-return-stackful) (memory $memory "mem") (core func $explicit-thread-calls-return-stackful')) + (canon lower (func $explicit-thread-calls-return-stackless) (memory $memory "mem") (core func $explicit-thread-calls-return-stackless')) + (canon lower (func $explicit-thread-suspends-sync) (memory $memory "mem") (core func $explicit-thread-suspends-sync')) + (canon lower (func $explicit-thread-suspends-stackful) (memory $memory "mem") (core func $explicit-thread-suspends-stackful')) + (canon lower (func $explicit-thread-suspends-stackless) (memory $memory "mem") (core func $explicit-thread-suspends-stackless')) + (canon lower (func $explicit-thread-yield-loops-sync) (memory $memory "mem") (core func $explicit-thread-yield-loops-sync')) + (canon lower (func $explicit-thread-yield-loops-stackful) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful')) + (canon lower (func $explicit-thread-yield-loops-stackless) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless')) + ;; async lowered + (canon lower (func $explicit-thread-calls-return-stackful) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackful-async')) + (canon lower (func $explicit-thread-calls-return-stackless) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackless-async')) + (canon lower (func $explicit-thread-suspends-sync) async (memory $memory "mem") (core func $explicit-thread-suspends-sync-async')) + (canon lower (func $explicit-thread-suspends-stackful) async (memory $memory "mem") (core func $explicit-thread-suspends-stackful-async')) + (canon lower (func $explicit-thread-suspends-stackless) async (memory $memory "mem") (core func $explicit-thread-suspends-stackless-async')) + (canon lower (func $explicit-thread-yield-loops-sync) async (memory $memory "mem") (core func $explicit-thread-yield-loops-sync-async')) + (canon lower (func $explicit-thread-yield-loops-stackful) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful-async')) + (canon lower (func $explicit-thread-yield-loops-stackless) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless-async')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful')) + (export "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless')) + (export "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync')) + (export "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful')) + (export "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless')) + (export "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync')) + (export "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful')) + (export "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless')) + (export "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async')) + (export "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async')) + (export "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async')) + (export "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async')) + (export "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async')) + (export "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async')) + (export "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async')) + (export "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async')) + (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 "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 "explicit-thread-calls-return-stackful" (func $c "explicit-thread-calls-return-stackful")) + (with "explicit-thread-calls-return-stackless" (func $c "explicit-thread-calls-return-stackless")) + (with "explicit-thread-suspends-sync" (func $c "explicit-thread-suspends-sync")) + (with "explicit-thread-suspends-stackful" (func $c "explicit-thread-suspends-stackful")) + (with "explicit-thread-suspends-stackless" (func $c "explicit-thread-suspends-stackless")) + (with "explicit-thread-yield-loops-sync" (func $c "explicit-thread-yield-loops-sync")) + (with "explicit-thread-yield-loops-stackful" (func $c "explicit-thread-yield-loops-stackful")) + (with "explicit-thread-yield-loops-stackless" (func $c "explicit-thread-yield-loops-stackless")) + )) + (func (export "run") (alias export $d "run")) + (func (export "explicit-thread-calls-return-stackful") (alias export $c "explicit-thread-calls-return-stackful")) + (func (export "explicit-thread-calls-return-stackless") (alias export $c "explicit-thread-calls-return-stackless")) + (func (export "explicit-thread-suspends-sync") (alias export $c "explicit-thread-suspends-sync")) + (func (export "explicit-thread-suspends-stackful") (alias export $c "explicit-thread-suspends-stackful")) + (func (export "explicit-thread-suspends-stackless") (alias export $c "explicit-thread-suspends-stackless")) + (func (export "explicit-thread-yield-loops-sync") (alias export $c "explicit-thread-yield-loops-sync")) + (func (export "explicit-thread-yield-loops-stackful") (alias export $c "explicit-thread-yield-loops-stackful")) + (func (export "explicit-thread-yield-loops-stackless") (alias export $c "explicit-thread-yield-loops-stackless")) +) + "#, + )? + .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 funcs = vec![ + "run", + "explicit-thread-calls-return-stackful", + "explicit-thread-calls-return-stackless", + "explicit-thread-suspends-sync", + "explicit-thread-suspends-stackful", + "explicit-thread-suspends-stackless", + "explicit-thread-yield-loops-sync", + "explicit-thread-yield-loops-stackful", + "explicit-thread-yield-loops-stackless", + ]; + for func in funcs { + let func = instance.get_typed_func::<(), (u32,)>(&mut store, func)?; + assert_eq!(func.call_async(&mut store, ()).await?, (42,)); + func.post_return_async(store.as_context_mut()).await?; + } + + Ok(()) +} diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 55d0b49cf7fe..26d59d7a1451 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -911,6 +911,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.wasm_component_model_threading(true); config.async_support(true); let engine = &Engine::new(&config)?; let component = Component::new(&engine, component)?; @@ -959,6 +960,88 @@ async fn missing_task_return_call_stackless() -> Result<()> { .await } +#[tokio::test] +async fn missing_task_return_call_stackless_explicit_thread() -> Result<()> { + task_return_trap( + r#"(component + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + (core module $m + (import "" "task.return" (func $task-return)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + (func $thread-start (param i32) (; empty ;)) + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + (func (export "foo") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (i32.const 0))) + i32.const 0 + ) + (func (export "callback") (param i32 i32 i32) (result i32) unreachable) + ) + (core instance $libc (instantiate $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-resume-later (canon thread.resume-later)) + (core func $task-return (canon task.return)) + (core instance $i (instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "task.return" (func $task-return)) + )) + (with "libc" (instance $libc)) + )) + (func (export "foo") (canon lift (core func $i "foo") async (callback (func $i "callback")))) + )"#, + "wasm trap: async-lifted export failed to produce a result", + ) + .await +} + +#[tokio::test] +async fn missing_task_return_call_stackful_explicit_thread() -> Result<()> { + task_return_trap( + r#"(component + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + (core module $m + (import "" "task.return" (func $task-return)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + (func $thread-start (param i32) (; empty ;)) + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + (func (export "foo") + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (i32.const 0))) + ) + ) + (core instance $libc (instantiate $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-resume-later (canon thread.resume-later)) + (core func $task-return (canon task.return)) + (core instance $i (instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "task.return" (func $task-return)) + )) + (with "libc" (instance $libc)) + )) + (func (export "foo") (canon lift (core func $i "foo") async)) + )"#, + "wasm trap: async-lifted export failed to produce a result", + ) + .await +} + #[tokio::test] async fn missing_task_return_call_stackful() -> Result<()> { task_return_trap( diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs index f64af0c6b7d1..947d507a8f98 100644 --- a/tests/all/component_model/threading.rs +++ b/tests/all/component_model/threading.rs @@ -1,7 +1,6 @@ use anyhow::Result; -use wasmtime::component::types::ComponentItem; -use wasmtime::component::{Component, Linker, Type}; -use wasmtime::{Config, Engine, Module, Precompiled, Store}; +use wasmtime::component::{Component, Linker}; +use wasmtime::{AsContextMut, Config, Engine, Store}; #[tokio::test] async fn threads() -> Result<()> { @@ -21,106 +20,318 @@ async fn threads() -> Result<()> { let engine = Engine::new(&config)?; let component = Component::new( &engine, - 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)) - ;; 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")))) + r#"(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start functions, of which there are two + (core module $libc + (table (export "__indirect_function_table") 3 funcref)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task-return (param i32))) + (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))) + (import "" "thread.resume-later" (func $thread-resume-later (param 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 3 funcref)) + + ;; Indices into the function table for the thread start functions + (global $call-return-ftbl-idx i32 (i32.const 0)) + (global $suspend-ftbl-idx i32 (i32.const 1)) + (global $yield-loop-ftbl-idx i32 (i32.const 2)) + + (func $call-return (param i32) + (call $task-return (local.get 0))) + + (func $suspend (param i32) + (drop (call $thread-suspend))) + + (func $yield-loop (param i32) + (loop $top + (drop (call $thread-yield)) + (br $top))) + + (func (export "explicit-thread-calls-return-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42)))) + + (func (export "explicit-thread-calls-return-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42))) + (i32.const 0 (; EXIT ;))) + + (func (export "cb") (param i32 i32 i32) (result i32) + (unreachable)) + + (func (export "explicit-thread-suspends-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-suspends-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-suspends-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0)) + + (func (export "explicit-thread-yield-loops-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-yield-loops-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-yield-loops-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0 (; EXIT ;))) + + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; call-return-ftbl-idx ;)) func $call-return) + (elem (table $indirect-function-table) (i32.const 1 (; suspend-ftbl-idx ;)) func $suspend) + (elem (table $indirect-function-table) (i32.const 2 (; yield-loop-ftbl-idx ;)) func $yield-loop) + ) + + ;; 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-return (canon task.return (result u32))) + (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-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 $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.return" (func $task-return)) + (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-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)) + (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 "explicit-thread-calls-return-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackful") async)) + (func (export "explicit-thread-calls-return-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-suspends-sync") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-sync"))) + (func (export "explicit-thread-suspends-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackful") async)) + (func (export "explicit-thread-suspends-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-yield-loops-sync") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-sync"))) + (func (export "explicit-thread-yield-loops-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackful") async)) + (func (export "explicit-thread-yield-loops-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackless") async (callback (func $cm "cb")))) + ) + + (component $D + (import "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result u32))) + (import "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result u32))) + (import "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result u32))) + (import "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result u32))) + (import "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result u32))) + (import "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result u32))) + (import "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result u32))) + (import "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (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))) + ;; sync lowered + (import "" "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result i32))) + (import "" "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result i32))) + (import "" "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result i32))) + (import "" "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result i32))) + (import "" "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result i32))) + (import "" "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result i32))) + (import "" "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result i32))) + (import "" "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result i32))) + ;; async lowered + (import "" "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async (param 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 "" "thread.yield" (func $thread-yield (result i32))) + + (func $check (param i32) + (if (i32.ne (local.get 0) (i32.const 42)) + (then unreachable)) + ) + + (func $check-async (param i32) + (local $retp i32) (local $ws i32) (local $ws-retp i32) + (local.set $retp (i32.const 8)) + (local.set $ws-retp (i32.const 16)) + (local.set $ws (call $waitable-set.new)) + + (if (i32.eq (i32.and (local.get 0) (i32.const 0xF)) (i32.const 2 (; RETURNED ;))) + (then (call $check (i32.load (local.get $retp)))) + (else + (call $waitable.join (i32.shr_u (local.get 0) (i32.const 4)) (local.get $ws)) + (drop (call $waitable-set.wait (local.get $ws) (local.get $ws-retp))) + (call $check (i32.load (local.get $retp))))) + ) + + (func $run (export "run") (result i32) + (local $retp i32) + (local.set $retp (i32.const 8)) + (call $check (call $explicit-thread-calls-return-stackless)) + (call $check (call $explicit-thread-calls-return-stackful)) + (call $check (call $explicit-thread-suspends-sync)) + (call $check (call $explicit-thread-suspends-stackful)) + (call $check (call $explicit-thread-suspends-stackless)) + (call $check (call $explicit-thread-yield-loops-sync)) + (call $check (call $explicit-thread-yield-loops-stackful)) + (call $check (call $explicit-thread-yield-loops-stackless)) + + (call $check-async (call $explicit-thread-calls-return-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-calls-return-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackless-async (local.get $retp))) + + (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 $thread.yield (canon thread.yield)) + ;; sync lowered + (canon lower (func $explicit-thread-calls-return-stackful) (memory $memory "mem") (core func $explicit-thread-calls-return-stackful')) + (canon lower (func $explicit-thread-calls-return-stackless) (memory $memory "mem") (core func $explicit-thread-calls-return-stackless')) + (canon lower (func $explicit-thread-suspends-sync) (memory $memory "mem") (core func $explicit-thread-suspends-sync')) + (canon lower (func $explicit-thread-suspends-stackful) (memory $memory "mem") (core func $explicit-thread-suspends-stackful')) + (canon lower (func $explicit-thread-suspends-stackless) (memory $memory "mem") (core func $explicit-thread-suspends-stackless')) + (canon lower (func $explicit-thread-yield-loops-sync) (memory $memory "mem") (core func $explicit-thread-yield-loops-sync')) + (canon lower (func $explicit-thread-yield-loops-stackful) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful')) + (canon lower (func $explicit-thread-yield-loops-stackless) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless')) + ;; async lowered + (canon lower (func $explicit-thread-calls-return-stackful) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackful-async')) + (canon lower (func $explicit-thread-calls-return-stackless) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackless-async')) + (canon lower (func $explicit-thread-suspends-sync) async (memory $memory "mem") (core func $explicit-thread-suspends-sync-async')) + (canon lower (func $explicit-thread-suspends-stackful) async (memory $memory "mem") (core func $explicit-thread-suspends-stackful-async')) + (canon lower (func $explicit-thread-suspends-stackless) async (memory $memory "mem") (core func $explicit-thread-suspends-stackless-async')) + (canon lower (func $explicit-thread-yield-loops-sync) async (memory $memory "mem") (core func $explicit-thread-yield-loops-sync-async')) + (canon lower (func $explicit-thread-yield-loops-stackful) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful-async')) + (canon lower (func $explicit-thread-yield-loops-stackless) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless-async')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful')) + (export "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless')) + (export "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync')) + (export "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful')) + (export "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless')) + (export "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync')) + (export "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful')) + (export "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless')) + (export "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async')) + (export "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async')) + (export "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async')) + (export "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async')) + (export "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async')) + (export "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async')) + (export "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async')) + (export "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async')) + (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 "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 "explicit-thread-calls-return-stackful" (func $c "explicit-thread-calls-return-stackful")) + (with "explicit-thread-calls-return-stackless" (func $c "explicit-thread-calls-return-stackless")) + (with "explicit-thread-suspends-sync" (func $c "explicit-thread-suspends-sync")) + (with "explicit-thread-suspends-stackful" (func $c "explicit-thread-suspends-stackful")) + (with "explicit-thread-suspends-stackless" (func $c "explicit-thread-suspends-stackless")) + (with "explicit-thread-yield-loops-sync" (func $c "explicit-thread-yield-loops-sync")) + (with "explicit-thread-yield-loops-stackful" (func $c "explicit-thread-yield-loops-stackful")) + (with "explicit-thread-yield-loops-stackless" (func $c "explicit-thread-yield-loops-stackless")) + )) + (func (export "run") (alias export $d "run")) + (func (export "explicit-thread-calls-return-stackful") (alias export $c "explicit-thread-calls-return-stackful")) + (func (export "explicit-thread-calls-return-stackless") (alias export $c "explicit-thread-calls-return-stackless")) + (func (export "explicit-thread-suspends-sync") (alias export $c "explicit-thread-suspends-sync")) + (func (export "explicit-thread-suspends-stackful") (alias export $c "explicit-thread-suspends-stackful")) + (func (export "explicit-thread-suspends-stackless") (alias export $c "explicit-thread-suspends-stackless")) + (func (export "explicit-thread-yield-loops-sync") (alias export $c "explicit-thread-yield-loops-sync")) + (func (export "explicit-thread-yield-loops-stackful") (alias export $c "explicit-thread-yield-loops-stackful")) + (func (export "explicit-thread-yield-loops-stackless") (alias export $c "explicit-thread-yield-loops-stackless")) +) "#, )? .serialize()?; @@ -130,8 +341,22 @@ async fn threads() -> Result<()> { 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?, (42,)); + let funcs = vec![ + "run", + // "explicit-thread-calls-return-stackful", + "explicit-thread-calls-return-stackless", + // "explicit-thread-suspends-sync", + // "explicit-thread-suspends-stackful", + // "explicit-thread-suspends-stackless", + // "explicit-thread-yield-loops-sync", + // "explicit-thread-yield-loops-stackful", + // "explicit-thread-yield-loops-stackless", + ]; + for func in funcs { + let func = instance.get_typed_func::<(), (u32,)>(&mut store, func)?; + assert_eq!(func.call_async(&mut store, ()).await?, (42,)); + func.post_return_async(store.as_context_mut()).await?; + } Ok(()) } diff --git a/tests/misc_testsuite/component-model-threading/task-deletion.wast b/tests/misc_testsuite/component-model-threading/task-deletion.wast new file mode 100644 index 000000000000..dd2ec55f7d5a --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/task-deletion.wast @@ -0,0 +1,305 @@ +;;! component_model_async = true +;;! component_model_async_stackful = true +;;! component_model_async_builtins = true +;;! component_model_threading = true +;;! reference_types = true + +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start functions, of which there are two + (core module $libc + (table (export "__indirect_function_table") 3 funcref)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task-return (param i32))) + (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))) + (import "" "thread.resume-later" (func $thread-resume-later (param 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 3 funcref)) + + ;; Indices into the function table for the thread start functions + (global $call-return-ftbl-idx i32 (i32.const 0)) + (global $suspend-ftbl-idx i32 (i32.const 1)) + (global $yield-loop-ftbl-idx i32 (i32.const 2)) + + (func $call-return (param i32) + (call $task-return (local.get 0))) + + (func $suspend (param i32) + (drop (call $thread-suspend))) + + (func $yield-loop (param i32) + (loop $top + (drop (call $thread-yield)) + (br $top))) + + (func (export "explicit-thread-calls-return-stackful") + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $call-return-ftbl-idx)))) + + (func (export "explicit-thread-calls-return-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $call-return-ftbl-idx))) + (i32.const 0 (; EXIT ;))) + + (func (export "cb") (param i32 i32 i32) (result i32) + (unreachable)) + + (func (export "explicit-thread-suspends-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) + (i32.const 42)) + + (func (export "explicit-thread-suspends-stackful") + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-suspends-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) + (call $task-return (i32.const 42)) + (i32.const 0)) + + (func (export "explicit-thread-yield-loops-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $yield-loop-ftbl-idx))) + (i32.const 42)) + + (func (export "explicit-thread-yield-loops-stackful") + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $yield-loop-ftbl-idx))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-yield-loops-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) + (call $task-return (i32.const 42)) + (i32.const 0 (; EXIT ;))) + + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; call-return-ftbl-idx ;)) func $call-return) + (elem (table $indirect-function-table) (i32.const 1 (; suspend-ftbl-idx ;)) func $suspend) + (elem (table $indirect-function-table) (i32.const 2 (; yield-loop-ftbl-idx ;)) func $yield-loop) + ) + + ;; 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-return (canon task.return (result u32))) + (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-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 $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.return" (func $task-return)) + (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-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)) + (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 "explicit-thread-calls-return-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackful") async)) + (func (export "explicit-thread-calls-return-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-suspends-sync") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-sync"))) + (func (export "explicit-thread-suspends-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackful") async)) + (func (export "explicit-thread-suspends-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-yield-loops-sync") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-sync"))) + (func (export "explicit-thread-yield-loops-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackful") async)) + (func (export "explicit-thread-yield-loops-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackless") async (callback (func $cm "cb")))) + ) + + (component $D + (import "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result u32))) + (import "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result u32))) + (import "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result u32))) + (import "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result u32))) + (import "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result u32))) + (import "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result u32))) + (import "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result u32))) + (import "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (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))) + ;; sync lowered + (import "" "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result i32))) + (import "" "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result i32))) + (import "" "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result i32))) + (import "" "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result i32))) + (import "" "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result i32))) + (import "" "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result i32))) + (import "" "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result i32))) + (import "" "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result i32))) + ;; async lowered + (import "" "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async (param 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 "" "thread.yield" (func $thread-yield (result i32))) + + (func $check (param i32) + (if (i32.ne (local.get 0) (i32.const 42)) + (then unreachable)) + ) + + (func $check-async (param i32) + (local $retp i32) (local $ws i32) (local $ws-retp i32) + (local.set $retp (i32.const 8)) + (local.set $ws-retp (i32.const 16)) + (local.set $ws (call $waitable-set.new)) + + (if (i32.eq (i32.and (local.get 0) (i32.const 0xF)) (i32.const 2 (; RETURNED ;))) + (then (call $check (i32.load (local.get $retp)))) + (else + (call $waitable.join (i32.shr_u (local.get 0) (i32.const 4)) (local.get $ws)) + (drop (call $waitable-set.wait (local.get $ws) (local.get $ws-retp))) + (call $check (i32.load offset=4 (local.get $ws-retp))))) + ) + + (func $run (export "run") (result i32) + (local $retp i32) + (local.set $retp (i32.const 8)) + (call $check (call $explicit-thread-calls-return-stackless)) + (call $check (call $explicit-thread-suspends-sync)) + (call $check (call $explicit-thread-suspends-stackful)) + (call $check (call $explicit-thread-suspends-stackless)) + (call $check (call $explicit-thread-yield-loops-sync)) + + (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) + + (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 $thread.yield (canon thread.yield)) + ;; sync lowered + (canon lower (func $explicit-thread-calls-return-stackful) (memory $memory "mem") (core func $explicit-thread-calls-return-stackful')) + (canon lower (func $explicit-thread-calls-return-stackless) (memory $memory "mem") (core func $explicit-thread-calls-return-stackless')) + (canon lower (func $explicit-thread-suspends-sync) (memory $memory "mem") (core func $explicit-thread-suspends-sync')) + (canon lower (func $explicit-thread-suspends-stackful) (memory $memory "mem") (core func $explicit-thread-suspends-stackful')) + (canon lower (func $explicit-thread-suspends-stackless) (memory $memory "mem") (core func $explicit-thread-suspends-stackless')) + (canon lower (func $explicit-thread-yield-loops-sync) (memory $memory "mem") (core func $explicit-thread-yield-loops-sync')) + (canon lower (func $explicit-thread-yield-loops-stackful) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful')) + (canon lower (func $explicit-thread-yield-loops-stackless) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless')) + ;; async lowered + (canon lower (func $explicit-thread-calls-return-stackful) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackful-async')) + (canon lower (func $explicit-thread-calls-return-stackless) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackless-async')) + (canon lower (func $explicit-thread-suspends-sync) async (memory $memory "mem") (core func $explicit-thread-suspends-sync-async')) + (canon lower (func $explicit-thread-suspends-stackful) async (memory $memory "mem") (core func $explicit-thread-suspends-stackful-async')) + (canon lower (func $explicit-thread-suspends-stackless) async (memory $memory "mem") (core func $explicit-thread-suspends-stackless-async')) + (canon lower (func $explicit-thread-yield-loops-sync) async (memory $memory "mem") (core func $explicit-thread-yield-loops-sync-async')) + (canon lower (func $explicit-thread-yield-loops-stackful) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful-async')) + (canon lower (func $explicit-thread-yield-loops-stackless) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless-async')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful')) + (export "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless')) + (export "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync')) + (export "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful')) + (export "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless')) + (export "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync')) + (export "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful')) + (export "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless')) + (export "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async')) + (export "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async')) + (export "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async')) + (export "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async')) + (export "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async')) + (export "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async')) + (export "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async')) + (export "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async')) + (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 "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 "explicit-thread-calls-return-stackful" (func $c "explicit-thread-calls-return-stackful")) + (with "explicit-thread-calls-return-stackless" (func $c "explicit-thread-calls-return-stackless")) + (with "explicit-thread-suspends-sync" (func $c "explicit-thread-suspends-sync")) + (with "explicit-thread-suspends-stackful" (func $c "explicit-thread-suspends-stackful")) + (with "explicit-thread-suspends-stackless" (func $c "explicit-thread-suspends-stackless")) + (with "explicit-thread-yield-loops-sync" (func $c "explicit-thread-yield-loops-sync")) + (with "explicit-thread-yield-loops-stackful" (func $c "explicit-thread-yield-loops-stackful")) + (with "explicit-thread-yield-loops-stackless" (func $c "explicit-thread-yield-loops-stackless")) + )) + (func (export "run") (alias export $d "run")) +) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From 4c811f7a59f04d3309d67934f52cc566b061d94b Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 7 Oct 2025 16:04:10 +0100 Subject: [PATCH 15/63] Tests all pass --- crates/environ/src/component.rs | 7 - .../src/runtime/component/concurrent.rs | 24 +- crates/wasmtime/src/runtime/component/func.rs | 1 - tests/all/component_model/threading.rs | 17 +- .../task-deletion.wast | 305 ------------------ 5 files changed, 24 insertions(+), 330 deletions(-) delete mode 100644 tests/misc_testsuite/component-model-threading/task-deletion.wast diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index acbbc5352ce8..7b2af1676f59 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -51,13 +51,6 @@ pub const PREPARE_ASYNC_WITH_RESULT: u32 = u32::MAX - 1; /// callee is an async-lifted export. pub const START_FLAG_ASYNC_CALLEE: i32 = 1 << 0; -/// The ABI used for lowering the import involved in a `prepare_call` call -#[derive(Copy, Clone)] -enum LowerABI { - Synchronous = 0, - Asynchronous = 1, -} - /// The ABI used for lifting the export involved in a `prepare_call` call #[derive(Copy, Clone)] pub enum LiftABI { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 66d2a15d1102..8bb8a4e3a4af 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -84,7 +84,7 @@ use std::vec::Vec; use table::{TableDebug, TableId}; use wasmtime_environ::Trap; use wasmtime_environ::component::{ - CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, LiftABI, LowerABI, MAX_FLAT_PARAMS, + CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, LiftABI, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, RuntimeComponentInstanceIndex, RuntimeTableIndex, StringEncoding, TypeComponentGlobalErrorContextTableIndex, TypeComponentLocalErrorContextTableIndex, @@ -2424,7 +2424,7 @@ impl Instance { if task.exited { if task.ready_to_delete() { - Waitable::Guest(guest_task).delete_from(state); + Waitable::Guest(guest_task).delete_from(state)?; } } } @@ -3042,7 +3042,7 @@ impl Instance { } state.guest_thread = old_thread; if state.get_mut(task_id)?.ready_to_delete() { - Waitable::Guest(task_id).delete_from(state); + Waitable::Guest(task_id).delete_from(state)?; } log::trace!("thread start: restored {old_thread:?} as current thread"); @@ -4178,11 +4178,17 @@ impl GuestTask { self.lift_result.is_none() } fn ready_to_delete(&self) -> bool { - let all_threads_completed = self.threads.is_empty(); - match &self.caller { - Caller::Guest { .. } => all_threads_completed, - Caller::Host { .. } => all_threads_completed, - } + let threads_completed = self.threads.is_empty(); + let has_sync_result = matches!(self.sync_result, SyncResult::Produced(_)); + let has_event = self.common.event.is_some(); + let ready = threads_completed && !has_sync_result && !has_event; + log::trace!( + "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, has_event: {})", + threads_completed, + has_sync_result, + has_event + ); + ready } fn new( state: &mut ConcurrentState, @@ -5025,6 +5031,7 @@ impl TaskId { /// Should only be called once for a given task, and only after either /// the task has completed or the instance has trapped. pub(crate) fn remove_if_not_lowered_params(&self, store: StoreContextMut) -> Result<()> { + /* if self .handle .instance() @@ -5036,6 +5043,7 @@ impl TaskId { Waitable::Guest(self.task) .delete_from(self.handle.instance().concurrent_state_mut(store.0))? } + Ok(())*/ Ok(()) } } diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 9a5f4b556544..a688b8bd0c8e 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -401,7 +401,6 @@ impl Func { store, self, MAX_FLAT_PARAMS, - true, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { diff --git a/tests/all/component_model/threading.rs b/tests/all/component_model/threading.rs index 947d507a8f98..7cc9293c28f9 100644 --- a/tests/all/component_model/threading.rs +++ b/tests/all/component_model/threading.rs @@ -9,8 +9,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); @@ -343,14 +342,14 @@ async fn threads() -> Result<()> { .await?; let funcs = vec![ "run", - // "explicit-thread-calls-return-stackful", + "explicit-thread-calls-return-stackful", "explicit-thread-calls-return-stackless", - // "explicit-thread-suspends-sync", - // "explicit-thread-suspends-stackful", - // "explicit-thread-suspends-stackless", - // "explicit-thread-yield-loops-sync", - // "explicit-thread-yield-loops-stackful", - // "explicit-thread-yield-loops-stackless", + "explicit-thread-suspends-sync", + "explicit-thread-suspends-stackful", + "explicit-thread-suspends-stackless", + "explicit-thread-yield-loops-sync", + "explicit-thread-yield-loops-stackful", + "explicit-thread-yield-loops-stackless", ]; for func in funcs { let func = instance.get_typed_func::<(), (u32,)>(&mut store, func)?; diff --git a/tests/misc_testsuite/component-model-threading/task-deletion.wast b/tests/misc_testsuite/component-model-threading/task-deletion.wast deleted file mode 100644 index dd2ec55f7d5a..000000000000 --- a/tests/misc_testsuite/component-model-threading/task-deletion.wast +++ /dev/null @@ -1,305 +0,0 @@ -;;! component_model_async = true -;;! component_model_async_stackful = true -;;! component_model_async_builtins = true -;;! component_model_threading = true -;;! reference_types = true - -(component - (component $C - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - ;; Defines the table for the thread start functions, of which there are two - (core module $libc - (table (export "__indirect_function_table") 3 funcref)) - (core module $CM - (import "" "mem" (memory 1)) - (import "" "task.return" (func $task-return (param i32))) - (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))) - (import "" "thread.resume-later" (func $thread-resume-later (param 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 3 funcref)) - - ;; Indices into the function table for the thread start functions - (global $call-return-ftbl-idx i32 (i32.const 0)) - (global $suspend-ftbl-idx i32 (i32.const 1)) - (global $yield-loop-ftbl-idx i32 (i32.const 2)) - - (func $call-return (param i32) - (call $task-return (local.get 0))) - - (func $suspend (param i32) - (drop (call $thread-suspend))) - - (func $yield-loop (param i32) - (loop $top - (drop (call $thread-yield)) - (br $top))) - - (func (export "explicit-thread-calls-return-stackful") - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $call-return-ftbl-idx)))) - - (func (export "explicit-thread-calls-return-stackless") (result i32) - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $call-return-ftbl-idx))) - (i32.const 0 (; EXIT ;))) - - (func (export "cb") (param i32 i32 i32) (result i32) - (unreachable)) - - (func (export "explicit-thread-suspends-sync") (result i32) - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) - (i32.const 42)) - - (func (export "explicit-thread-suspends-stackful") - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) - (call $task-return (i32.const 42))) - - (func (export "explicit-thread-suspends-stackless") (result i32) - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) - (call $task-return (i32.const 42)) - (i32.const 0)) - - (func (export "explicit-thread-yield-loops-sync") (result i32) - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $yield-loop-ftbl-idx))) - (i32.const 42)) - - (func (export "explicit-thread-yield-loops-stackful") - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $yield-loop-ftbl-idx))) - (call $task-return (i32.const 42))) - - (func (export "explicit-thread-yield-loops-stackless") (result i32) - (call $thread-resume-later - (call $thread-new-indirect (i32.const 0) (global.get $suspend-ftbl-idx))) - (call $task-return (i32.const 42)) - (i32.const 0 (; EXIT ;))) - - - ;; Initialize the function table that will be used by thread.new_indirect - (elem (table $indirect-function-table) (i32.const 0 (; call-return-ftbl-idx ;)) func $call-return) - (elem (table $indirect-function-table) (i32.const 1 (; suspend-ftbl-idx ;)) func $suspend) - (elem (table $indirect-function-table) (i32.const 2 (; yield-loop-ftbl-idx ;)) func $yield-loop) - ) - - ;; 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-return (canon task.return (result u32))) - (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-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 $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.return" (func $task-return)) - (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-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)) - (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 "explicit-thread-calls-return-stackful") (result u32) - (canon lift (core func $cm "explicit-thread-calls-return-stackful") async)) - (func (export "explicit-thread-calls-return-stackless") (result u32) - (canon lift (core func $cm "explicit-thread-calls-return-stackless") async (callback (func $cm "cb")))) - (func (export "explicit-thread-suspends-sync") (result u32) - (canon lift (core func $cm "explicit-thread-suspends-sync"))) - (func (export "explicit-thread-suspends-stackful") (result u32) - (canon lift (core func $cm "explicit-thread-suspends-stackful") async)) - (func (export "explicit-thread-suspends-stackless") (result u32) - (canon lift (core func $cm "explicit-thread-suspends-stackless") async (callback (func $cm "cb")))) - (func (export "explicit-thread-yield-loops-sync") (result u32) - (canon lift (core func $cm "explicit-thread-yield-loops-sync"))) - (func (export "explicit-thread-yield-loops-stackful") (result u32) - (canon lift (core func $cm "explicit-thread-yield-loops-stackful") async)) - (func (export "explicit-thread-yield-loops-stackless") (result u32) - (canon lift (core func $cm "explicit-thread-yield-loops-stackless") async (callback (func $cm "cb")))) - ) - - (component $D - (import "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result u32))) - (import "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result u32))) - (import "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result u32))) - (import "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result u32))) - (import "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result u32))) - (import "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result u32))) - (import "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result u32))) - (import "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (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))) - ;; sync lowered - (import "" "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result i32))) - (import "" "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result i32))) - (import "" "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result i32))) - (import "" "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result i32))) - (import "" "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result i32))) - (import "" "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result i32))) - (import "" "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result i32))) - (import "" "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result i32))) - ;; async lowered - (import "" "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async (param i32) (result i32))) - (import "" "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async (param i32) (result i32))) - (import "" "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async (param i32) (result i32))) - (import "" "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async (param i32) (result i32))) - (import "" "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async (param i32) (result i32))) - (import "" "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async (param i32) (result i32))) - (import "" "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async (param i32) (result i32))) - (import "" "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async (param 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 "" "thread.yield" (func $thread-yield (result i32))) - - (func $check (param i32) - (if (i32.ne (local.get 0) (i32.const 42)) - (then unreachable)) - ) - - (func $check-async (param i32) - (local $retp i32) (local $ws i32) (local $ws-retp i32) - (local.set $retp (i32.const 8)) - (local.set $ws-retp (i32.const 16)) - (local.set $ws (call $waitable-set.new)) - - (if (i32.eq (i32.and (local.get 0) (i32.const 0xF)) (i32.const 2 (; RETURNED ;))) - (then (call $check (i32.load (local.get $retp)))) - (else - (call $waitable.join (i32.shr_u (local.get 0) (i32.const 4)) (local.get $ws)) - (drop (call $waitable-set.wait (local.get $ws) (local.get $ws-retp))) - (call $check (i32.load offset=4 (local.get $ws-retp))))) - ) - - (func $run (export "run") (result i32) - (local $retp i32) - (local.set $retp (i32.const 8)) - (call $check (call $explicit-thread-calls-return-stackless)) - (call $check (call $explicit-thread-suspends-sync)) - (call $check (call $explicit-thread-suspends-stackful)) - (call $check (call $explicit-thread-suspends-stackless)) - (call $check (call $explicit-thread-yield-loops-sync)) - - (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) - (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) - (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) - (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) - - (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 $thread.yield (canon thread.yield)) - ;; sync lowered - (canon lower (func $explicit-thread-calls-return-stackful) (memory $memory "mem") (core func $explicit-thread-calls-return-stackful')) - (canon lower (func $explicit-thread-calls-return-stackless) (memory $memory "mem") (core func $explicit-thread-calls-return-stackless')) - (canon lower (func $explicit-thread-suspends-sync) (memory $memory "mem") (core func $explicit-thread-suspends-sync')) - (canon lower (func $explicit-thread-suspends-stackful) (memory $memory "mem") (core func $explicit-thread-suspends-stackful')) - (canon lower (func $explicit-thread-suspends-stackless) (memory $memory "mem") (core func $explicit-thread-suspends-stackless')) - (canon lower (func $explicit-thread-yield-loops-sync) (memory $memory "mem") (core func $explicit-thread-yield-loops-sync')) - (canon lower (func $explicit-thread-yield-loops-stackful) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful')) - (canon lower (func $explicit-thread-yield-loops-stackless) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless')) - ;; async lowered - (canon lower (func $explicit-thread-calls-return-stackful) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackful-async')) - (canon lower (func $explicit-thread-calls-return-stackless) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackless-async')) - (canon lower (func $explicit-thread-suspends-sync) async (memory $memory "mem") (core func $explicit-thread-suspends-sync-async')) - (canon lower (func $explicit-thread-suspends-stackful) async (memory $memory "mem") (core func $explicit-thread-suspends-stackful-async')) - (canon lower (func $explicit-thread-suspends-stackless) async (memory $memory "mem") (core func $explicit-thread-suspends-stackless-async')) - (canon lower (func $explicit-thread-yield-loops-sync) async (memory $memory "mem") (core func $explicit-thread-yield-loops-sync-async')) - (canon lower (func $explicit-thread-yield-loops-stackful) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful-async')) - (canon lower (func $explicit-thread-yield-loops-stackless) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless-async')) - (core instance $dm (instantiate $DM (with "" (instance - (export "mem" (memory $memory "mem")) - (export "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful')) - (export "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless')) - (export "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync')) - (export "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful')) - (export "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless')) - (export "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync')) - (export "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful')) - (export "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless')) - (export "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async')) - (export "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async')) - (export "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async')) - (export "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async')) - (export "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async')) - (export "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async')) - (export "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async')) - (export "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async')) - (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 "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 "explicit-thread-calls-return-stackful" (func $c "explicit-thread-calls-return-stackful")) - (with "explicit-thread-calls-return-stackless" (func $c "explicit-thread-calls-return-stackless")) - (with "explicit-thread-suspends-sync" (func $c "explicit-thread-suspends-sync")) - (with "explicit-thread-suspends-stackful" (func $c "explicit-thread-suspends-stackful")) - (with "explicit-thread-suspends-stackless" (func $c "explicit-thread-suspends-stackless")) - (with "explicit-thread-yield-loops-sync" (func $c "explicit-thread-yield-loops-sync")) - (with "explicit-thread-yield-loops-stackful" (func $c "explicit-thread-yield-loops-stackful")) - (with "explicit-thread-yield-loops-stackless" (func $c "explicit-thread-yield-loops-stackless")) - )) - (func (export "run") (alias export $d "run")) -) - -(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file From bac5c5a6ecfd659e728275c22eb89e17de88a7a0 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 09:55:14 +0100 Subject: [PATCH 16/63] Tighten up task deletion --- .../src/runtime/component/concurrent.rs | 68 ++++++++++++++----- crates/wasmtime/src/runtime/component/func.rs | 1 + .../src/runtime/component/func/typed.rs | 14 ++-- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 8bb8a4e3a4af..523908ad1964 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -4012,6 +4012,9 @@ enum Caller { /// dropping it when the refcount goes to zero is sufficient to notify /// the receiver. exit_tx: Arc>, + /// If true, there's a host future that must be dropped before the task + /// can be deleted. + host_future_present: bool, /// If true, call `post-return` function (if any) automatically. call_post_return_automatically: bool, }, @@ -4114,6 +4117,13 @@ impl SyncResult { } } +#[derive(Debug)] +enum HostFutureState { + NotApplicable, + Live, + Dropped, +} + /// Represents a pending guest task. pub(crate) struct GuestTask { /// See `WaitableCommon` @@ -4166,6 +4176,9 @@ pub(crate) struct GuestTask { threads: HashSet>, /// The ABI used for lifting the export implementing this task lift_abi: LiftABI, + /// The state of the host future that represents an async task, which must + /// be dropped before we can delete the task. + host_future_state: HostFutureState, } impl GuestTask { @@ -4181,12 +4194,16 @@ impl GuestTask { let threads_completed = self.threads.is_empty(); let has_sync_result = matches!(self.sync_result, SyncResult::Produced(_)); let has_event = self.common.event.is_some(); - let ready = threads_completed && !has_sync_result && !has_event; + let ready = threads_completed + && !has_sync_result + && !has_event + && !matches!(self.host_future_state, HostFutureState::Live); log::trace!( - "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, has_event: {})", + "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, has_event: {}, host_future_state: {:?})", threads_completed, has_sync_result, - has_event + has_event, + self.host_future_state ); ready } @@ -4200,7 +4217,19 @@ impl GuestTask { lift_abi: LiftABI, ) -> Result { let sync_call_set = state.push(WaitableSet::default())?; - + let host_future_state = match &caller { + Caller::Guest { .. } => HostFutureState::NotApplicable, + Caller::Host { + host_future_present, + .. + } => { + if *host_future_present { + HostFutureState::Live + } else { + HostFutureState::NotApplicable + } + } + }; Ok(Self { common: WaitableCommon::default(), lower_params: Some(lower_params), @@ -4220,6 +4249,7 @@ impl GuestTask { exited: false, threads: HashSet::new(), lift_abi, + host_future_state, }) } @@ -4268,6 +4298,7 @@ impl GuestTask { // once all transitive subtasks of the host call have // exited: exit_tx: exit_tx.clone(), + host_future_present: false, call_post_return_automatically: true, }; } @@ -5026,24 +5057,27 @@ pub(crate) struct TaskId { } impl TaskId { - /// Remove the specified task from the concurrent state to which it belongs. - /// - /// Should only be called once for a given task, and only after either - /// the task has completed or the instance has trapped. - pub(crate) fn remove_if_not_lowered_params(&self, store: StoreContextMut) -> Result<()> { - /* - if self + /// The host future for an async task was dropped. If the parameters have not been lowered yet, + /// it is no longer valid to do so, as the lowering closure would see a dangling pointer. In this case, + /// we delete the task eagerly. Otherwise, there may be running threads, or ones that are suspended + /// and can be resumed by other tasks for this component, so we mark the future as dropped + /// and delete the task when all threads are done. + pub(crate) fn host_future_dropped(&self, store: StoreContextMut) -> Result<()> { + let task = self .handle .instance() .concurrent_state_mut(store.0) - .get_mut(self.task)? - .lower_params - .is_some() - { + .get_mut(self.task)?; + if !task.have_lowered_parameters() { Waitable::Guest(self.task) .delete_from(self.handle.instance().concurrent_state_mut(store.0))? + } else { + task.host_future_state = HostFutureState::Dropped; + if task.ready_to_delete() { + Waitable::Guest(self.task) + .delete_from(self.handle.instance().concurrent_state_mut(store.0))? + } } - Ok(())*/ Ok(()) } } @@ -5057,6 +5091,7 @@ pub(crate) fn prepare_call( mut store: StoreContextMut, handle: Func, param_count: usize, + host_future_present: bool, call_post_return_automatically: bool, lower_params: impl FnOnce(Func, StoreContextMut, &mut [MaybeUninit]) -> Result<()> + Send @@ -5111,6 +5146,7 @@ pub(crate) fn prepare_call( Caller::Host { tx: Some(tx), exit_tx: Arc::new(exit_tx), + host_future_present, call_post_return_automatically, }, callback.map(|callback| { diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index a688b8bd0c8e..0c9d0b1e9051 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -401,6 +401,7 @@ impl Func { store, self, MAX_FLAT_PARAMS, + false, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 642a79219029..17e65b3be12d 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -193,7 +193,7 @@ where let ptr = SendSyncPtr::from(NonNull::from(¶ms).cast::()); let prepared = - self.prepare_call(store.as_context_mut(), 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 @@ -209,9 +209,9 @@ where // this closure, which ensures that the task will be removed // from the concurrent state to which it belongs when the // containing `Future` is dropped, so long as the parameters - // have not yet been lowered. This ensures that this closure will - // never be called if it hasn't already, meaning it will never see - // a dangling pointer. + // have not yet been lowered. Since this closure is removed from + // the task after the parameters are lowered, it will never be called + // after the containing `Future` is dropped. let params = unsafe { ptr.cast::().as_ref() }; Self::lower_args(cx, ty, dst, params) })?; @@ -224,7 +224,7 @@ where impl<'a, T> Drop for RemoveOnDrop<'a, T> { fn drop(&mut self) { self.task - .remove_if_not_lowered_params(self.store.as_context_mut()) + .host_future_dropped(self.store.as_context_mut()) .unwrap(); } } @@ -285,7 +285,7 @@ where ); let prepared = - self.prepare_call(store.as_context_mut(), true, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), false, true, move |cx, ty, dst| { Self::lower_args(cx, ty, dst, ¶ms) })?; concurrent::queue_call(store, prepared) @@ -322,6 +322,7 @@ where fn prepare_call( self, store: StoreContextMut<'_, T>, + host_future_present: bool, call_post_return_automatically: bool, lower: impl FnOnce( &mut LowerContext, @@ -351,6 +352,7 @@ where store, self.func, param_count, + host_future_present, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { From af20cc701aa7c82d808d5e16eed5d8dd3444e938 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 10:08:29 +0100 Subject: [PATCH 17/63] Set thread state correctly --- crates/wasmtime/src/runtime/component/concurrent.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 523908ad1964..8293b82724a6 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1459,6 +1459,7 @@ impl Instance { let state = self.concurrent_state_mut(store); state.guest_thread = old_thread; + old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { @@ -1477,7 +1478,6 @@ impl Instance { state.push_low_priority(WorkItem::ResumeFiber(fiber)); } SuspendReason::ExplicitlySuspending { thread, .. } => { - // TODO do this earlier? state.get_mut(thread)?.state = GuestThreadState::Suspended(fiber); } SuspendReason::Waiting { set, thread } => { @@ -1740,6 +1740,10 @@ impl Instance { let token = StoreToken::new(store); move |store: &mut dyn VMStore, instance: Instance| { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; + instance + .concurrent_state_mut(store) + .get_mut(guest_thread)? + .state = GuestThreadState::Running; let task = instance .concurrent_state_mut(store) .get_task_mut(guest_thread)?; @@ -1756,7 +1760,6 @@ 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(), @@ -1836,6 +1839,7 @@ impl Instance { let state = instance.concurrent_state_mut(store); state.guest_thread = old_thread; + old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); log::trace!("stackless call: restored {old_thread:?} as current thread"); // SAFETY: `wasmparser` will have validated that the callback @@ -2410,6 +2414,7 @@ impl Instance { // Reset the current thread to point to the caller as it resumes control. state.guest_thread = Some(caller); + state.get_mut(caller)?.state = GuestThreadState::Running; log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); if let Some(storage) = storage { @@ -3041,6 +3046,7 @@ impl Instance { bail!(Trap::NoAsyncResult); } state.guest_thread = old_thread; + old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); if state.get_mut(task_id)?.ready_to_delete() { Waitable::Guest(task_id).delete_from(state)?; } From 6c5c242b1025c8f550c17b158f44f6e1a8fddbac Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:12:56 +0100 Subject: [PATCH 18/63] Store pairs of thread and task ids --- .../src/runtime/component/concurrent.rs | 404 +++++++++--------- 1 file changed, 195 insertions(+), 209 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 8293b82724a6..4f2a01776c94 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -596,7 +596,7 @@ enum SuspendReason { /// waitable set or task. Waiting { set: TableId, - thread: TableId, + thread: QualifiedThreadId, }, /// 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). @@ -604,14 +604,14 @@ enum SuspendReason { /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. Yielding { - thread: TableId, - to: Option>, + thread: QualifiedThreadId, + to: Option, cancellable: bool, }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. ExplicitlySuspending { - thread: TableId, - to: Option>, + thread: QualifiedThreadId, + to: Option, cancellable: bool, }, } @@ -643,10 +643,10 @@ impl fmt::Debug for GuestCallKind { } } -/// Represents a pending call into guest code for a given guest task. +/// Represents a pending call into guest code for a given guest thread. #[derive(Debug)] struct GuestCall { - thread: TableId, + thread: QualifiedThreadId, kind: GuestCallKind, } @@ -661,7 +661,7 @@ 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_task_mut(self.thread)?.instance; + let task_instance = state.get_mut(self.thread.task)?.instance; let state = state.instance_state(task_instance); let ready = match &self.kind { @@ -689,7 +689,7 @@ enum WorkerItem { #[derive(Debug)] struct PollParams { /// Identifies the polling thread. - thread: TableId, + thread: QualifiedThreadId, /// The waitable set being polled. set: TableId, } @@ -729,7 +729,7 @@ impl ComponentInstance { /// async-lifted export; otherwise, it was received from its callback. fn handle_callback_code( mut self: Pin<&mut Self>, - guest_thread: TableId, + guest_thread: QualifiedThreadId, runtime_instance: RuntimeComponentInstanceIndex, code: u32, initial_call: bool, @@ -739,14 +739,13 @@ impl ComponentInstance { log::trace!("received callback code from {guest_thread:?}: {code} (set: {set})"); let state = self.as_mut().concurrent_state_mut(); - let task_id = state.get_task_id(guest_thread)?; - let task = state.get_mut(task_id)?; + let task = state.get_mut(guest_thread.task)?; if !task.returned_or_cancelled() { if initial_call { // Notify any current or future waiters that this subtask has // started. - Waitable::Guest(task_id).set_event( + Waitable::Guest(guest_thread.task).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -767,20 +766,22 @@ impl ComponentInstance { match code { callback_code::EXIT => { - let task_id = state.get_task_id(guest_thread)?; - log::trace!("implicit thread {task_id:?}:{guest_thread:?} completed"); - state.get_mut(guest_thread)?.state = GuestThreadState::Completed; - state.get_mut(task_id)?.threads.remove(&guest_thread); - if state.get_mut(task_id)?.threads.is_empty() - && !state.get_mut(task_id)?.returned_or_cancelled() + log::trace!("implicit thread {guest_thread:?} completed"); + state.get_mut(guest_thread.thread)?.state = GuestThreadState::Completed; + state + .get_mut(guest_thread.task)? + .threads + .remove(&guest_thread.thread); + if state.get_mut(guest_thread.task)?.threads.is_empty() + && !state.get_mut(guest_thread.task)?.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } - let task = state.get_task_mut(guest_thread)?; + let task = state.get_mut(guest_thread.task)?; match &task.caller { Caller::Host { .. } => { - if state.get_mut(task_id)?.ready_to_delete() { - Waitable::Guest(task_id).delete_from(state)?; + if state.get_mut(guest_thread.task)?.ready_to_delete() { + Waitable::Guest(guest_thread.task).delete_from(state)?; } } Caller::Guest { .. } => { @@ -792,7 +793,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_task_mut(guest_thread)?; + 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 { @@ -804,7 +805,7 @@ impl ComponentInstance { let set = get_set(self.as_mut(), set)?; let state = self.concurrent_state_mut(); - if state.get_task_mut(guest_thread)?.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. @@ -830,7 +831,10 @@ impl ComponentInstance { // Here we also set `GuestTask::wake_on_cancel` // which allows `subtask.cancel` to interrupt the // wait. - let old = state.get_mut(guest_thread)?.wake_on_cancel.replace(set); + let old = state + .get_mut(guest_thread.thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); let old = state .get_mut(set)? @@ -1394,12 +1398,11 @@ impl Instance { self.run_on_worker(store, WorkerItem::GuestCall(call)) .await?; } else { - let task_id = state.get_task_id(call.thread)?; - let task = state.get_mut(task_id)?; + let task = state.get_mut(call.thread.task)?; if !task.starting_sent { task.starting_sent = true; if let GuestCallKind::StartImplicit(_) = &call.kind { - Waitable::Guest(task_id).set_event( + Waitable::Guest(call.thread.task).set_event( state, Some(Event::Subtask { status: Status::Starting, @@ -1408,7 +1411,7 @@ impl Instance { } } - let runtime_instance = state.get_mut(task_id)?.instance; + let runtime_instance = state.get_mut(call.thread.task)?.instance; state .instance_state(runtime_instance) .pending @@ -1417,7 +1420,7 @@ impl Instance { } WorkItem::Poll(params) => { let state = self.concurrent_state_mut(store.0); - if state.get_task_mut(params.thread)?.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 @@ -1431,7 +1434,7 @@ impl Instance { } else { // There are no events immediately available; deliver // `Event::None` to the guest. - state.get_task_mut(params.thread)?.event = Some(Event::None); + state.get_mut(params.thread.task)?.event = Some(Event::None); state.push_high_priority(WorkItem::GuestCall(GuestCall { thread: params.thread, kind: GuestCallKind::DeliverEvent { @@ -1459,7 +1462,7 @@ impl Instance { let state = self.concurrent_state_mut(store); state.guest_thread = old_thread; - old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); + old_thread.map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { @@ -1474,11 +1477,11 @@ impl Instance { } } SuspendReason::Yielding { thread, .. } => { - state.get_mut(thread)?.state = GuestThreadState::Pending; + state.get_mut(thread.thread)?.state = GuestThreadState::Pending; state.push_low_priority(WorkItem::ResumeFiber(fiber)); } SuspendReason::ExplicitlySuspending { thread, .. } => { - state.get_mut(thread)?.state = GuestThreadState::Suspended(fiber); + state.get_mut(thread.thread)?.state = GuestThreadState::Suspended(fiber); } SuspendReason::Waiting { set, thread } => { let old = state @@ -1527,14 +1530,13 @@ 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(task_id, set, true)? + .get_event(call.thread.task, set, true)? .unwrap(); let state = self.concurrent_state_mut(store); - let runtime_instance = state.get_mut(task_id)?.instance; + let runtime_instance = state.get_mut(call.thread.task)?.instance; let handle = waitable.map(|(_, v)| v).unwrap_or(0); log::trace!( @@ -1547,23 +1549,22 @@ 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(), task_id, old_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(task_id)?.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(task_id)?.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(), task_id, old_task)?; + self.maybe_pop_call_context(store.store_opaque_mut(), call.thread.task)?; self.id().get_mut(store).handle_callback_code( call.thread, @@ -1596,28 +1597,19 @@ 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(state.get_task_id(*thread)?) - } + | SuspendReason::ExplicitlySuspending { thread, .. } => Some(thread.task), SuspendReason::NeedWork => None, }; let old_guest_thread = if let Some(task) = task { 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)?; + self.maybe_pop_call_context(store, task)?; old_thread } else { None @@ -1631,12 +1623,7 @@ impl Instance { if let Some(task) = task { self.concurrent_state_mut(store).guest_thread = old_guest_thread; - 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)?; + self.maybe_push_call_context(store, task)?; } Ok(()) @@ -1649,7 +1636,6 @@ impl Instance { self, store: &mut StoreOpaque, guest_task: TableId, - old_task: Option>, ) -> Result<()> { let task = self.concurrent_state_mut(store).get_mut(guest_task)?; @@ -1657,12 +1643,6 @@ impl Instance { log::trace!("push call context for {guest_task:?}"); let call_context = task.call_context.take().unwrap(); store.component_resource_state().0.push(call_context); - } else { - log::trace!( - "not pushing call context: old={:?}, new={:?}", - old_task, - guest_task - ) } Ok(()) } @@ -1674,7 +1654,6 @@ impl Instance { self, store: &mut StoreOpaque, guest_task: TableId, - old_task: Option>, ) -> Result<()> { if !self .concurrent_state_mut(store) @@ -1699,7 +1678,7 @@ impl Instance { unsafe fn queue_call( self, mut store: StoreContextMut, - guest_thread: TableId, + guest_thread: QualifiedThreadId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1724,7 +1703,7 @@ impl Instance { /// the returned closure is called. unsafe fn make_call( store: StoreContextMut, - guest_thread: TableId, + guest_thread: QualifiedThreadId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1742,11 +1721,11 @@ impl Instance { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; instance .concurrent_state_mut(store) - .get_mut(guest_thread)? + .get_mut(guest_thread.thread)? .state = GuestThreadState::Running; let task = instance .concurrent_state_mut(store) - .get_task_mut(guest_thread)?; + .get_mut(guest_thread.task)?; let may_enter_after_call = task.call_post_return_automatically(); let lower = task.lower_params.take().unwrap(); @@ -1794,7 +1773,7 @@ impl Instance { let callee_instance = self .concurrent_state_mut(store.0) - .get_task_mut(guest_thread)? + .get_mut(guest_thread.task)? .instance; let fun = if callback.is_some() { assert!(async_); @@ -1807,17 +1786,8 @@ 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(), task_id, old_task)?; + instance.maybe_push_call_context(store.store_opaque_mut(), guest_thread.task)?; instance .concurrent_state_mut(store) @@ -1835,11 +1805,12 @@ impl Instance { .concurrent_state_mut(store) .exit_instance(callee_instance)?; - instance.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; let state = instance.concurrent_state_mut(store); state.guest_thread = old_thread; - old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); + old_thread + .map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); log::trace!("stackless call: restored {old_thread:?} as current thread"); // SAFETY: `wasmparser` will have validated that the callback @@ -1866,19 +1837,9 @@ 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(), task_id, old_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 @@ -1900,23 +1861,23 @@ impl Instance { // This is a callback-less call, so the implicit thread has now completed instance .concurrent_state_mut(store) - .get_mut(guest_thread)? + .get_mut(guest_thread.thread)? .state = GuestThreadState::Completed; instance .concurrent_state_mut(store) - .get_mut(task_id)? + .get_mut(guest_thread.task)? .threads - .remove(&guest_thread); + .remove(&guest_thread.thread); if async_ { if instance .concurrent_state_mut(store) - .get_mut(task_id)? + .get_mut(guest_thread.task)? .threads .is_empty() && !instance .concurrent_state_mut(store) - .get_mut(task_id)? + .get_mut(guest_thread.task)? .returned_or_cancelled() { bail!(Trap::NoAsyncResult); @@ -1931,9 +1892,13 @@ impl Instance { let state = instance.concurrent_state_mut(store); state.exit_instance(callee_instance)?; - assert!(state.get_mut(task_id)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); - state.get_mut(task_id)?.lift_result.take().unwrap() + state + .get_mut(guest_thread.task)? + .lift_result + .take() + .unwrap() }; // SAFETY: `result_count` represents the number of core Wasm @@ -1954,7 +1919,7 @@ impl Instance { if instance .concurrent_state_mut(store) - .get_mut(task_id)? + .get_mut(guest_thread.task)? .call_post_return_automatically() { unsafe { @@ -1987,22 +1952,22 @@ impl Instance { instance.task_complete( store, - task_id, + guest_thread.task, result, Status::Returned, post_return_arg, )?; } - instance.maybe_pop_call_context(store.store_opaque_mut(), task_id, old_task)?; + instance.maybe_pop_call_context(store.store_opaque_mut(), guest_thread.task)?; let state = instance.concurrent_state_mut(store); - let task = state.get_mut(task_id)?; + let task = state.get_mut(guest_thread.task)?; match &task.caller { Caller::Host { .. } => { if task.ready_to_delete() { - Waitable::Guest(task_id).delete_from(state)?; + Waitable::Guest(guest_thread.task).delete_from(state)?; } } Caller::Guest { .. } => { @@ -2130,8 +2095,7 @@ impl Instance { } dst.copy_from_slice(&src[..dst.len()]); let state = instance.concurrent_state_mut(store.0); - let task = state.get_task_id(state.guest_thread.unwrap())?; - Waitable::Guest(task).set_event( + Waitable::Guest(state.guest_thread.unwrap().task).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -2164,7 +2128,7 @@ impl Instance { let state = instance.concurrent_state_mut(store.0); let thread = state.guest_thread.unwrap(); if sync_caller { - state.get_task_mut(thread)?.sync_result = SyncResult::Produced( + state.get_mut(thread.task)?.sync_result = SyncResult::Produced( if let ResultInfo::Stack { result_count } = &result_info { match result_count { 0 => None, @@ -2201,12 +2165,15 @@ impl Instance { bail!(crate::Trap::CannotEnterComponent); } - state.get_task_mut(old_thread)?.subtasks.insert(guest_task); + state.get_mut(old_thread.task)?.subtasks.insert(guest_task); }; // Make the new thread the current one so that `Self::start_call` knows // which one to start. - state.guest_thread = Some(guest_thread); + state.guest_thread = Some(QualifiedThreadId { + task: guest_task, + thread: guest_thread, + }); log::trace!( "pushed {guest_task:?}:{guest_thread:?} as current thread; old thread was {old_thread:?}" ); @@ -2278,15 +2245,16 @@ impl Instance { let async_caller = storage.is_none(); let state = self.concurrent_state_mut(store.0); let guest_thread = state.guest_thread.unwrap(); - let guest_task = state.get_task_id(guest_thread)?; - let may_enter_after_call = state.get_mut(guest_task)?.call_post_return_automatically(); + let may_enter_after_call = state + .get_mut(guest_thread.task)? + .call_post_return_automatically(); let callee = SendSyncPtr::new(NonNull::new(callee).unwrap()); let param_count = usize::try_from(param_count).unwrap(); assert!(param_count <= MAX_FLAT_PARAMS); let result_count = usize::try_from(result_count).unwrap(); assert!(result_count <= MAX_FLAT_RESULTS); - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if !callback.is_null() { // We're calling an async-lifted export with a callback, so store // the callback and related context as part of the task so we can @@ -2348,9 +2316,9 @@ impl Instance { // Use the caller's `GuestTask::sync_call_set` to register interest in // the subtask... - let guest_waitable = Waitable::Guest(guest_task); + let guest_waitable = Waitable::Guest(guest_thread.task); let old_set = guest_waitable.common(state)?.set; - let set = state.get_task_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. @@ -2379,13 +2347,13 @@ impl Instance { let state = self.concurrent_state_mut(store.0); - log::trace!("taking event for {guest_task:?}"); + log::trace!("taking event for {:?}", guest_thread.task); let event = guest_waitable.take_event(state)?; let Some(Event::Subtask { status }) = event else { unreachable!(); }; - log::trace!("status {status:?} for {guest_task:?}"); + log::trace!("status {status:?} for {:?}", guest_thread.task); if status == Status::Returned { // It returned, so we can stop waiting. @@ -2395,9 +2363,9 @@ impl Instance { // async-lowered import, so we generate a handle for the task // waitable and return the status. let handle = self.id().get_mut(store.0).guest_tables().0[caller_instance] - .subtask_insert_guest(guest_task.rep())?; + .subtask_insert_guest(guest_thread.task.rep())?; self.concurrent_state_mut(store.0) - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .common .handle = Some(handle); break (status, Some(handle)); @@ -2414,14 +2382,14 @@ impl Instance { // Reset the current thread to point to the caller as it resumes control. state.guest_thread = Some(caller); - state.get_mut(caller)?.state = GuestThreadState::Running; + state.get_mut(caller.thread)?.state = GuestThreadState::Running; log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); if let Some(storage) = storage { // The caller used a sync-lowered import to call an async-lifted // export, in which case the result, if any, has been stashed in // `GuestTask::sync_result`. - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if let Some(result) = task.sync_result.take() { if let Some(result) = result { storage[0] = MaybeUninit::new(result); @@ -2429,7 +2397,7 @@ impl Instance { if task.exited { if task.ready_to_delete() { - Waitable::Guest(guest_task).delete_from(state)?; + Waitable::Guest(guest_thread.task).delete_from(state)?; } } } @@ -2636,7 +2604,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_task_mut(caller) + .get_mut(caller.task) .with_context(|| format!("bad handle: {caller:?}"))? .result .take(); @@ -2655,7 +2623,7 @@ impl Instance { let result = future.await?; tls::get(move |store| { let state = self.concurrent_state_mut(store); - state.get_task_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, @@ -2694,7 +2662,7 @@ impl Instance { let state = self.concurrent_state_mut(store); state.push_future(future); - let set = state.get_task_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; Waitable::Host(task).join(state, Some(set))?; self.suspend( @@ -2711,7 +2679,7 @@ impl Instance { Ok(*mem::replace( &mut self .concurrent_state_mut(store) - .get_task_mut(caller)? + .get_mut(caller.task)? .result, old_result, ) @@ -2738,15 +2706,14 @@ 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_task_mut(guest_thread)? + .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_task_mut(guest_thread)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); let invalid = ty != lift.ty || string_encoding != lift.string_encoding @@ -2772,7 +2739,13 @@ impl Instance { 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. @@ -2784,7 +2757,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_task_mut(guest_thread)?; + let task = state.get_mut(guest_thread.task)?; if !task.cancel_sent { bail!("`task.cancel` called by task which has not been cancelled") } @@ -2796,10 +2769,9 @@ impl Instance { log::trace!("task.cancel for {guest_thread:?}"); - let guest_task = state.get_task_id(guest_thread)?; self.task_complete( store, - guest_task, + guest_thread.task, Box::new(DummyResult), Status::ReturnCancelled, ValRaw::i32(0), @@ -2973,7 +2945,7 @@ 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)?; + let parent_task = current_thread.task; let new_thread = GuestThread::new_explicit(parent_task, start_func_table_idx, start_func_idx, context); @@ -2988,17 +2960,13 @@ impl Instance { fn start_thread( self, store: &mut StoreContextMut, - thread: TableId, + thread: QualifiedThreadId, 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(); - let task_id = self - .concurrent_state_mut(store.0) - .get_task_id(guest_thread)?; - log::trace!("starting thread {task_id:?}:{thread:?}"); + log::trace!("starting thread {thread:?}"); let callee = unsafe { self.read_funcref_from_table(store.0, start_func_table_idx, start_func_idx as u64)? }; @@ -3014,14 +2982,8 @@ impl Instance { log::trace!( "thread start: replaced {old_thread:?} with {thread:?} as current thread" ); - let old_task = old_thread.map(|thread| { - instance - .concurrent_state_mut(store) - .get_task_id(thread) - .unwrap() - }); - self.maybe_push_call_context(store.store_opaque_mut(), task_id, old_task)?; + self.maybe_push_call_context(store.store_opaque_mut(), thread.task)?; let mut store = token.as_context_mut(store); @@ -3034,21 +2996,22 @@ impl Instance { )? } - self.maybe_pop_call_context(store.0, task_id, old_task)?; + self.maybe_pop_call_context(store.0, thread.task)?; let state = instance.concurrent_state_mut(store.0); - state.get_mut(thread)?.state = GuestThreadState::Completed; - state.get_mut(task_id)?.threads.remove(&thread); - log::trace!("explicit thread {task_id:?}:{thread:?} completed"); - if state.get_mut(task_id)?.threads.is_empty() - && !state.get_mut(task_id)?.returned_or_cancelled() + state.get_mut(thread.thread)?.state = GuestThreadState::Completed; + state.get_mut(thread.task)?.threads.remove(&thread.thread); + log::trace!("explicit thread {thread:?} completed"); + if state.get_mut(thread.task)?.threads.is_empty() + && !state.get_mut(thread.task)?.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } state.guest_thread = old_thread; - old_thread.map(|t| state.get_mut(t).unwrap().state = GuestThreadState::Running); - if state.get_mut(task_id)?.ready_to_delete() { - Waitable::Guest(task_id).delete_from(state)?; + old_thread + .map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); + if state.get_mut(thread.task)?.ready_to_delete() { + Waitable::Guest(thread.task).delete_from(state)?; } log::trace!("thread start: restored {old_thread:?} as current thread"); @@ -3077,7 +3040,8 @@ impl Instance { high_priority: bool, ) -> Result<()> { let state = self.concurrent_state_mut(store.0); - let thread = state.get_mut(thread_id)?; + let guest_thread = QualifiedThreadId::qualify(state, thread_id)?; + let thread = state.get_mut(guest_thread.thread)?; match mem::replace(&mut thread.state, GuestThreadState::Running) { GuestThreadState::NotStartedExplicit { @@ -3087,7 +3051,7 @@ impl Instance { } => { self.start_thread( &mut store, - thread_id, + guest_thread, start_func_table_idx, start_func_idx, context, @@ -3095,8 +3059,7 @@ impl Instance { )?; } GuestThreadState::Suspended(fiber) => { - let task_id = state.get_task_id(thread_id)?; - log::trace!("resuming thread {task_id:?}:{thread_id:?} that was suspended"); + log::trace!("resuming thread {thread_id:?} that was suspended"); if high_priority { self.concurrent_state_mut(store.0) .push_high_priority(WorkItem::ResumeFiber(fiber)); @@ -3115,7 +3078,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_task_mut(thread).unwrap().event.take() { + if let Some(event) = state.get_mut(thread.task).unwrap().event.take() { assert!(matches!(event, Event::Cancelled)); true } else { @@ -3144,7 +3107,12 @@ impl Instance { self.resume_suspended_thread(store.as_context_mut(), thread, true)?; } - let guest_thread = self.concurrent_state_mut(store.0).guest_thread.unwrap(); + let state = self.concurrent_state_mut(store.0); + let guest_thread = state.guest_thread.unwrap(); + let to_thread = match to_thread { + Some(t) => Some(QualifiedThreadId::qualify(state, t)?), + None => None, + }; let reason = if yielding { SuspendReason::Yielding { thread: guest_thread, @@ -3192,7 +3160,7 @@ impl Instance { log::trace!("waitable check for {guest_thread:?}; set {set:?}"); let state = self.concurrent_state_mut(store); - let task = state.get_task_mut(guest_thread)?; + 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"); @@ -3208,7 +3176,10 @@ impl Instance { && state.get_mut(set)?.ready.is_empty() { if cancellable { - let old = state.get_mut(guest_thread)?.wake_on_cancel.replace(set); + let old = state + .get_mut(guest_thread.thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); } @@ -3227,11 +3198,11 @@ impl Instance { let result = match check { // Deliver any pending events to the guest and return. WaitableCheck::Wait(params) | WaitableCheck::Poll(params) => { - 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 event = self.id().get_mut(store).get_event( + guest_thread.task, + Some(params.set), + cancellable, + )?; let (ordinal, handle, result) = if wait { let (event, waitable) = event.unwrap(); @@ -3246,7 +3217,7 @@ impl Instance { } else { log::trace!( "no events ready to deliver via waitable-set.poll to {:?}; set {:?}", - task_id, + guest_thread.task, params.set ); let (ordinal, result) = Event::None.parts(); @@ -3321,7 +3292,6 @@ impl Instance { // Not yet started; cancel and remove from pending let callee_instance = task.instance; - /* todo if !concurrent_state .instance_state(callee_instance) .pending @@ -3333,7 +3303,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.returned_or_cancelled() { @@ -3346,8 +3316,12 @@ impl Instance { // okay -- this should supersede the previous state. task.event = Some(Event::Cancelled); for thread in task.threads.clone() { + let thread = QualifiedThreadId { + task: guest_task, + thread, + }; if let Some(set) = concurrent_state - .get_mut(thread) + .get_mut(thread.thread) .unwrap() .wake_on_cancel .take() @@ -3405,7 +3379,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_task_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; waitable.join(state, Some(set))?; self.suspend( store, @@ -4027,7 +4001,7 @@ enum Caller { /// Another guest thread called the guest task Guest { /// The id of the caller - thread: TableId, + thread: QualifiedThreadId, /// The instance to use to enforce reentrance rules. /// /// Note that this might not be the same as the instance the caller task @@ -4046,6 +4020,37 @@ struct LiftResult { string_encoding: StringEncoding, } +/// The table ID for a guest thread, qualified by the task to which it belongs. +/// +/// This exists to minimize table lookups and the necessity to pass stores around mutably +/// for the common case of identifying the task to which a thread belongs. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +struct QualifiedThreadId { + task: TableId, + thread: TableId, +} + +impl QualifiedThreadId { + fn qualify( + state: &mut ConcurrentState, + thread: TableId, + ) -> Result { + Ok(QualifiedThreadId { + task: state.get_mut(thread)?.parent_task, + thread, + }) + } +} + +impl fmt::Debug for QualifiedThreadId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("QualifiedThreadId") + .field(&self.task.rep()) + .field(&self.thread.rep()) + .finish() + } +} + enum GuestThreadState { NotStartedImplicit, NotStartedExplicit { @@ -4281,7 +4286,7 @@ impl GuestTask { thread, instance: runtime_instance, } => { - let task_mut = state.get_task_mut(*thread)?; + let task_mut = state.get_mut(thread.task)?; let present = task_mut.subtasks.remove(&me); assert!(present); @@ -4450,7 +4455,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_mut(thread)?.wake_on_cancel.take(); + let wake_on_cancel = state.get_mut(thread.thread)?.wake_on_cancel.take(); assert!(wake_on_cancel.is_none() || wake_on_cancel == Some(set)); let item = match mode { @@ -4551,7 +4556,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, WaitMode>, + waiting: BTreeMap, } impl TableDebug for WaitableSet { @@ -4607,14 +4612,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, GuestCallKind>, + pending: BTreeMap, } /// 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. /// @@ -4763,22 +4768,6 @@ impl ConcurrentState { self.table.get_mut().get_mut(&Resource::from(id)) } - fn get_task_id( - &mut self, - id: TableId, - ) -> Result, ResourceTableError> { - let thread = self.get_mut(id)?; - Ok(thread.parent_task) - } - - fn get_task_mut( - &mut self, - id: TableId, - ) -> Result<&mut GuestTask, ResourceTableError> { - self.get_task_id(id) - .and_then(|task_id| self.get_mut(task_id)) - } - pub fn add_child( &mut self, child: TableId, @@ -4853,8 +4842,7 @@ impl ConcurrentState { } } }; - let task = self.get_task_id(next_thread).unwrap(); - guest_task = task; + guest_task = next_thread.task; } } @@ -4915,7 +4903,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_mut(thread)?.context[usize::try_from(slot).unwrap()]; + let val = self.get_mut(thread.thread)?.context[usize::try_from(slot).unwrap()]; log::trace!("context_get {thread:?} slot {slot} val {val:#x}"); Ok(val) } @@ -4924,7 +4912,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_mut(thread)?.context[usize::try_from(slot).unwrap()] = val; + self.get_mut(thread.thread)?.context[usize::try_from(slot).unwrap()] = val; Ok(()) } @@ -4933,6 +4921,7 @@ impl ConcurrentState { Ok(self .guest_thread .ok_or_else(|| anyhow!("no current thread"))? + .thread .rep()) } @@ -5031,10 +5020,8 @@ 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: TableId, + thread: QualifiedThreadId, /// 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 @@ -5051,7 +5038,7 @@ impl PreparedCall { pub(crate) fn task_id(&self) -> TaskId { TaskId { handle: self.handle, - task: self.task, + task: self.thread.task, } } } @@ -5190,8 +5177,7 @@ pub(crate) fn prepare_call( Ok(PreparedCall { handle, - task, - thread, + thread: QualifiedThreadId { task, thread }, param_count, rx, exit_rx, @@ -5235,7 +5221,7 @@ pub(crate) fn queue_call( fn queue_call0( store: StoreContextMut, handle: Func, - guest_thread: TableId, + guest_thread: QualifiedThreadId, param_count: usize, ) -> Result<()> { let (options, flags, _ty, raw_options) = handle.abi_info(store.0); From c53402317c0866de3b160f6b674b1a2e47ce97b0 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:15:27 +0100 Subject: [PATCH 19/63] Remove lift abi members --- crates/environ/src/component.rs | 24 ------------------- crates/environ/src/fact.rs | 1 - crates/environ/src/fact/trampoline.rs | 10 -------- .../src/runtime/component/concurrent.rs | 23 ++---------------- .../src/runtime/vm/component/libcalls.rs | 2 -- 5 files changed, 2 insertions(+), 58 deletions(-) diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 7b2af1676f59..dec960fee091 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -51,29 +51,6 @@ pub const PREPARE_ASYNC_WITH_RESULT: u32 = u32::MAX - 1; /// callee is an async-lifted export. pub const START_FLAG_ASYNC_CALLEE: i32 = 1 << 0; -/// The ABI used for lifting the export involved in a `prepare_call` call -#[derive(Copy, Clone)] -pub enum LiftABI { - /// Synchronous ABI - Synchronous = 0, - /// Stackless Asynchronous ABI - Stackless = 1, - /// Stackful Asynchronous ABI - Stackful = 2, -} - -impl LiftABI { - /// Convert from an integer representation to a LiftABI - pub fn from_u32(abi: u32) -> LiftABI { - match abi { - 0 => LiftABI::Synchronous, - 1 => LiftABI::Stackless, - 2 => LiftABI::Stackful, - _ => panic!("Unexpected lift ABI type"), - } - } -} - mod artifacts; mod info; mod names; @@ -155,7 +132,6 @@ macro_rules! foreach_builtin_component_function { task_return_type: u32, string_encoding: u32, result_count_or_max_if_async: u32, - lift_abi: u32, storage: ptr_u8, storage_len: size ) -> bool; diff --git a/crates/environ/src/fact.rs b/crates/environ/src/fact.rs index 9e4529d8b57c..9e97dc4274e7 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -49,7 +49,6 @@ pub static PREPARE_CALL_FIXED_PARAMS: &[ValType] = &[ ValType::I32, // task_return_type ValType::I32, // string_encoding ValType::I32, // result_count_or_max_if_async - ValType::I32, // lift_abi ]; /// Representation of an adapter module. diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 61a3a220330b..0c6dbc3f302f 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -572,16 +572,6 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(I32Const(PREPARE_ASYNC_NO_RESULT.cast_signed())); } } - let lift_abi = if adapter.lift.options.async_ { - if adapter.lift.options.callback.is_some() { - LiftABI::Stackless - } else { - LiftABI::Stackful - } - } else { - LiftABI::Synchronous - }; - self.instruction(I32Const(lift_abi as i32)); // forward all our own arguments on to the host stub for index in 0..lower_sig.params.len() { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 4f2a01776c94..454df1127865 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -84,8 +84,8 @@ use std::vec::Vec; use table::{TableDebug, TableId}; use wasmtime_environ::Trap; use wasmtime_environ::component::{ - CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, LiftABI, 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, @@ -2011,7 +2011,6 @@ impl Instance { memory: *mut VMMemoryDefinition, string_encoding: u8, caller_info: CallerInfo, - lift_abi: LiftABI, ) -> Result<()> { self.id().get(store.0).check_may_leave(caller_instance)?; @@ -2152,7 +2151,6 @@ impl Instance { }, None, callee_instance, - lift_abi, )?; let guest_task = state.push(new_task)?; @@ -3469,7 +3467,6 @@ pub trait VMComponentAsyncStore { task_return_type: TypeTupleIndex, string_encoding: u8, result_count: u32, - lift_abi: LiftABI, storage: *mut ValRaw, storage_len: usize, ) -> Result<()>; @@ -3635,7 +3632,6 @@ impl VMComponentAsyncStore for StoreInner { task_return_type: TypeTupleIndex, string_encoding: u8, result_count_or_max_if_async: u32, - lift_abi: LiftABI, storage: *mut ValRaw, storage_len: usize, ) -> Result<()> { @@ -3668,7 +3664,6 @@ impl VMComponentAsyncStore for StoreInner { result_count, }, }, - lift_abi, ) } } @@ -4185,8 +4180,6 @@ pub(crate) struct GuestTask { exited: bool, /// Threads belonging to this task threads: HashSet>, - /// The ABI used for lifting the export implementing this task - lift_abi: LiftABI, /// The state of the host future that represents an async task, which must /// be dropped before we can delete the task. host_future_state: HostFutureState, @@ -4225,7 +4218,6 @@ impl GuestTask { caller: Caller, callback: Option, component_instance: RuntimeComponentInstanceIndex, - lift_abi: LiftABI, ) -> Result { let sync_call_set = state.push(WaitableSet::default())?; let host_future_state = match &caller { @@ -4259,7 +4251,6 @@ impl GuestTask { function_index: None, exited: false, threads: HashSet::new(), - lift_abi, host_future_state, }) } @@ -5106,15 +5097,6 @@ pub(crate) fn prepare_call( let token = StoreToken::new(store.as_context_mut()); let state = handle.instance().concurrent_state_mut(store.0); let async_ = options.async_(); - let lift_abi = if async_ { - if callback.is_some() { - LiftABI::Stackless - } else { - LiftABI::Stackful - } - } else { - LiftABI::Synchronous - }; assert!(state.guest_thread.is_none()); @@ -5167,7 +5149,6 @@ pub(crate) fn prepare_call( ) as CallbackFn }), component_instance, - lift_abi, )?; task.function_index = Some(handle.index()); diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index bdc77c4a5f57..2b68ee4c083e 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -860,7 +860,6 @@ unsafe fn prepare_call( task_return_type: u32, string_encoding: u32, result_count_or_max_if_async: u32, - lift_abi: u32, storage: *mut u8, storage_len: usize, ) -> Result<()> { @@ -875,7 +874,6 @@ unsafe fn prepare_call( TypeTupleIndex::from_u32(task_return_type), u8::try_from(string_encoding).unwrap(), result_count_or_max_if_async, - LiftABI::from_u32(lift_abi), storage.cast::(), storage_len, ) From 2d66c89511abc729916f3d2adbce0a053486ba5f Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:18:53 +0100 Subject: [PATCH 20/63] Cleanup unnecessary change --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3ba92fb883b..2d8cc6061212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4457,7 +4457,6 @@ dependencies = [ "target-lexicon", "tempfile", "tokio", - "tracing-subscriber", "wasm-encoder", "wasm-wave", "wasmparser 0.239.0", @@ -4566,7 +4565,6 @@ dependencies = [ "tokio", "toml", "tracing", - "tracing-subscriber", "walkdir", "wasi-common", "wasm-encoder", @@ -4615,6 +4613,8 @@ name = "wasmtime-environ" version = "38.0.0" dependencies = [ "anyhow", + "arbitrary", + "arbtest", "clap", "cpp_demangle", "cranelift-bitset", @@ -4989,6 +4989,7 @@ dependencies = [ "async-trait", "base64", "bytes", + "env_logger 0.11.5", "flate2", "futures", "http", From a650fe922756fbbbe0952bed46fca6b50f70841b Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:30:32 +0100 Subject: [PATCH 21/63] More cleanup --- crates/environ/src/fact/trampoline.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 0c6dbc3f302f..799e26036168 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -17,11 +17,11 @@ use crate::component::{ CanonicalAbiInfo, ComponentTypesBuilder, FLAG_MAY_ENTER, FLAG_MAY_LEAVE, FixedEncoding as FE, - FlatType, InterfaceType, LiftABI, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, - PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, - Transcode, TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFlagsIndex, - TypeFutureTableIndex, TypeListIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTableIndex, - TypeResultIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, VariantInfo, + FlatType, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, PREPARE_ASYNC_NO_RESULT, + PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, Transcode, + TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFlagsIndex, TypeFutureTableIndex, + TypeListIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex, + TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, VariantInfo, }; use crate::fact::signature::Signature; use crate::fact::transcode::Transcoder; From 2718e6522d3edc438c522cc3f130398f6ceae190 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:40:47 +0100 Subject: [PATCH 22/63] Cleanup --- Cargo.lock | 3 - Cargo.toml | 100 +++++------------- .../environ/src/component/translate/adapt.rs | 1 - crates/environ/src/fact.rs | 2 - crates/environ/src/trap_encoding.rs | 12 +-- .../tests/scenario/coop_threads.rs | 19 ---- .../tests/scenario/mod.rs | 1 - .../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/wasmtime/Cargo.toml | 40 +++---- crates/wasmtime/src/runtime/.DS_Store | Bin 8196 -> 0 bytes .../wasmtime/src/runtime/component/.DS_Store | Bin 6148 -> 0 bytes crates/wasmtime/src/runtime/gc/.DS_Store | Bin 6148 -> 0 bytes crates/wasmtime/src/runtime/vm/.DS_Store | Bin 8196 -> 0 bytes crates/wasmtime/src/runtime/vm/sys/.DS_Store | Bin 6148 -> 0 bytes 18 files changed, 44 insertions(+), 209 deletions(-) delete mode 100644 crates/misc/component-async-tests/tests/scenario/coop_threads.rs delete mode 100644 crates/test-programs/src/bin/async_coop_threads_callee.rs delete mode 100644 crates/test-programs/src/bin/async_coop_threads_caller.rs delete mode 100644 crates/wasmtime/src/runtime/.DS_Store delete mode 100644 crates/wasmtime/src/runtime/component/.DS_Store delete mode 100644 crates/wasmtime/src/runtime/gc/.DS_Store delete mode 100644 crates/wasmtime/src/runtime/vm/.DS_Store delete mode 100644 crates/wasmtime/src/runtime/vm/sys/.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 2d8cc6061212..45df23c9b941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4613,8 +4613,6 @@ name = "wasmtime-environ" version = "38.0.0" dependencies = [ "anyhow", - "arbitrary", - "arbtest", "clap", "cpp_demangle", "cranelift-bitset", @@ -4989,7 +4987,6 @@ dependencies = [ "async-trait", "base64", "bytes", - "env_logger 0.11.5", "flate2", "futures", "http", diff --git a/Cargo.toml b/Cargo.toml index eb496e36e9c1..8b0a18df76ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,10 +50,7 @@ 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 } @@ -85,36 +82,22 @@ 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 } -tracing-subscriber.workspace = true [target.'cfg(unix)'.dependencies] 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" @@ -124,10 +107,7 @@ 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 } @@ -137,11 +117,7 @@ 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 } @@ -194,7 +170,9 @@ members = [ "fuzz", "winch/codegen", ] -exclude = ['docs/rust_wasi_markdown_parser'] +exclude = [ + 'docs/rust_wasi_markdown_parser', +] [workspace.package] version = "38.0.0" @@ -275,8 +253,8 @@ wasmtime-wmemcheck = { path = "crates/wmemcheck", version = "=38.0.0", package = wasmtime-c-api-macros = { path = "crates/c-api-macros", version = "=38.0.0", package = 'wasmtime-internal-c-api-macros' } wasmtime-cache = { path = "crates/cache", version = "=38.0.0", package = 'wasmtime-internal-cache' } wasmtime-cranelift = { path = "crates/cranelift", version = "=38.0.0", package = 'wasmtime-internal-cranelift' } -wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } -wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } +wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } +wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } wasmtime-fiber = { path = "crates/fiber", version = "=38.0.0", package = 'wasmtime-internal-fiber' } wasmtime-jit-debug = { path = "crates/jit-debug", version = "=38.0.0", package = 'wasmtime-internal-jit-debug' } wasmtime-component-util = { path = "crates/component-util", version = "=38.0.0", package = 'wasmtime-internal-component-util' } @@ -285,7 +263,7 @@ wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", ve wasmtime-slab = { path = "crates/slab", version = "=38.0.0", package = 'wasmtime-internal-slab' } wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=38.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=38.0.0", package = 'wasmtime-internal-wit-bindgen' } -wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } +wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } wasmtime-unwinder = { path = "crates/unwinder", version = "=38.0.0", package = 'wasmtime-internal-unwinder' } # Miscellaneous crates without a `wasmtime-*` prefix in their name but still @@ -299,10 +277,7 @@ pulley-macros = { path = 'pulley/macros', version = "=38.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.125.0" } -cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = [ - "std", - "unwind", -] } +cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = ["std", "unwind"] } cranelift-frontend = { path = "cranelift/frontend", version = "0.125.0" } cranelift-entity = { path = "cranelift/entity", version = "0.125.0" } cranelift-native = { path = "cranelift/native", version = "0.125.0" } @@ -353,9 +328,7 @@ wit-bindgen = { version = "0.46.0", default-features = false } wit-bindgen-rust-macro = { version = "0.46.0", default-features = false } # wasm-tools family: -wasmparser = { version = "0.239.0", default-features = false, features = [ - 'simd', -] } +wasmparser = { version = "0.239.0", default-features = false, features = ['simd'] } wat = "1.239.0" wast = "239.0.0" wasmprinter = "0.239.0" @@ -373,20 +346,14 @@ json-from-wast = "0.239.0" 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" @@ -415,7 +382,7 @@ tempfile = "3.21.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" @@ -427,18 +394,10 @@ 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 } @@ -454,6 +413,7 @@ base64 = "0.22.1" termcolor = "1.4.1" flate2 = "1.0.30" tokio-util = "0.7.4" +arbtest = "0.3.1" # ============================================================================= # @@ -546,23 +506,14 @@ 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"] @@ -579,10 +530,7 @@ 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/environ/src/component/translate/adapt.rs b/crates/environ/src/component/translate/adapt.rs index d9c7a474f96d..64c9e1a9c6e4 100644 --- a/crates/environ/src/component/translate/adapt.rs +++ b/crates/environ/src/component/translate/adapt.rs @@ -345,7 +345,6 @@ 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/fact.rs b/crates/environ/src/fact.rs index 9e97dc4274e7..d2a8689f2ab4 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -870,8 +870,6 @@ 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 939a6f5c515b..8db629bec55b 100644 --- a/crates/environ/src/trap_encoding.rs +++ b/crates/environ/src/trap_encoding.rs @@ -89,11 +89,6 @@ 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, @@ -112,6 +107,11 @@ 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 one component tried to call an import at a time when it + /// was not legal to do so. + CannotLeaveComponent, // if adding a variant here be sure to update the `check!` macro below } @@ -184,12 +184,12 @@ 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", 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/misc/component-async-tests/tests/scenario/coop_threads.rs b/crates/misc/component-async-tests/tests/scenario/coop_threads.rs deleted file mode 100644 index d9ce940b2482..000000000000 --- a/crates/misc/component-async-tests/tests/scenario/coop_threads.rs +++ /dev/null @@ -1,19 +0,0 @@ -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 aab3fd3f69b2..e3481c18bf58 100644 --- a/crates/misc/component-async-tests/tests/scenario/mod.rs +++ b/crates/misc/component-async-tests/tests/scenario/mod.rs @@ -2,7 +2,6 @@ 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/test_all.rs b/crates/misc/component-async-tests/tests/test_all.rs index dfc3eee2f0fb..1c4c76e35d50 100644 --- a/crates/misc/component-async-tests/tests/test_all.rs +++ b/crates/misc/component-async-tests/tests/test_all.rs @@ -13,7 +13,6 @@ 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 a949a64d9ec3..6c07c7e88c1c 100644 --- a/crates/misc/component-async-tests/wit/test.wit +++ b/crates/misc/component-async-tests/wit/test.wit @@ -347,17 +347,4 @@ world sleep-post-return-caller { world closed-stream-guest { export closed-stream; -} - -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 96fe914883d1..1ec58158d27d 100644 --- a/crates/test-programs/src/async_.rs +++ b/crates/test-programs/src/async_.rs @@ -114,17 +114,6 @@ 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 deleted file mode 100644 index 0b1c8b871f34..000000000000 --- a/crates/test-programs/src/bin/async_coop_threads_callee.rs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 12e00b1cca56..000000000000 --- a/crates/test-programs/src/bin/async_coop_threads_caller.rs +++ /dev/null @@ -1,25 +0,0 @@ -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/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index b07d94d90f98..cd27da0825a0 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -22,7 +22,9 @@ wasmtime-jit-debug = { workspace = true, optional = true } wasmtime-jit-icache-coherence = { workspace = true, optional = true } wasmtime-cache = { workspace = true, optional = true } wasmtime-fiber = { workspace = true, optional = true } -wasmtime-cranelift = { workspace = true, optional = true, features = ['pulley'] } +wasmtime-cranelift = { workspace = true, optional = true, features = [ + 'pulley', +] } wasmtime-unwinder = { workspace = true, optional = true } wasmtime-winch = { workspace = true, optional = true } wasmtime-component-macro = { workspace = true, optional = true } @@ -61,7 +63,6 @@ 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 @@ -198,22 +199,22 @@ cache = ["dep:wasmtime-cache", "std"] # Enables support for "async stores" as well as defining host functions as # `async fn` and calling functions asynchronously. -async = [ - "dep:wasmtime-fiber", - "wasmtime-component-macro?/async", - "runtime", -] +async = ["dep:wasmtime-fiber", "wasmtime-component-macro?/async", "runtime"] # Enables support for the pooling instance allocation strategy pooling-allocator = [ "runtime", - "std", # not ported to no_std yet + "std", # not ported to no_std yet ] # Enables support for all architectures in Cranelift, allowing # cross-compilation using the `wasmtime` crate's API, notably the # `Engine::precompile_module` function. -all-arch = ["wasmtime-cranelift?/all-arch", "wasmtime-winch?/all-arch", "pulley"] +all-arch = [ + "wasmtime-cranelift?/all-arch", + "wasmtime-winch?/all-arch", + "pulley", +] # Enables in-progress support for the component model. Note that this feature is # in-progress, buggy, and incomplete. This is primarily here for internal @@ -248,9 +249,7 @@ coredump = ["dep:wasm-encoder", "runtime", "std"] # Export some symbols from the final binary to assist in debugging # Cranelift-generated code with native debuggers like GDB and LLDB. -debug-builtins = [ - "wasmtime-jit-debug/gdb_jit_int", -] +debug-builtins = ["wasmtime-jit-debug/gdb_jit_int"] # Enable support for executing compiled Wasm modules. runtime = [ @@ -283,11 +282,7 @@ runtime = [ # # You can additionally configure which GC implementations are enabled via the # `gc-drc` and `gc-null` features. -gc = [ - "wasmtime-environ/gc", - "wasmtime-cranelift?/gc", - "wasmtime-winch?/gc", -] +gc = ["wasmtime-environ/gc", "wasmtime-cranelift?/gc", "wasmtime-winch?/gc"] # Enable the deferred reference counting garbage collector. gc-drc = [ @@ -306,11 +301,7 @@ gc-null = [ ] # Enable runtime support for the WebAssembly threads proposal. -threads = [ - "wasmtime-cranelift?/threads", - "wasmtime-winch?/threads", - "std", -] +threads = ["wasmtime-cranelift?/threads", "wasmtime-winch?/threads", "std"] stack-switching = [ "runtime", @@ -405,7 +396,4 @@ component-model-async = [ ] # Enables support for `stream` interop with the `bytes` crate. -component-model-async-bytes = [ - "component-model-async", - "dep:bytes", -] +component-model-async-bytes = ["component-model-async", "dep:bytes"] diff --git a/crates/wasmtime/src/runtime/.DS_Store b/crates/wasmtime/src/runtime/.DS_Store deleted file mode 100644 index 070e0289cbc150ef11f6bfc5e3d0397f0710836c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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^&= diff --git a/crates/wasmtime/src/runtime/gc/.DS_Store b/crates/wasmtime/src/runtime/gc/.DS_Store deleted file mode 100644 index 9429fe79751df48df500f7285c8dbec63f395e77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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|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 From 474e15657fa0c47127006fe368e966ff075dd3f8 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:45:55 +0100 Subject: [PATCH 23/63] Revert cargo changes --- Cargo.lock | 3 ++ Cargo.toml | 100 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45df23c9b941..2d8cc6061212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4613,6 +4613,8 @@ name = "wasmtime-environ" version = "38.0.0" dependencies = [ "anyhow", + "arbitrary", + "arbtest", "clap", "cpp_demangle", "cranelift-bitset", @@ -4987,6 +4989,7 @@ dependencies = [ "async-trait", "base64", "bytes", + "env_logger 0.11.5", "flate2", "futures", "http", diff --git a/Cargo.toml b/Cargo.toml index 8b0a18df76ec..eb496e36e9c1 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,22 +85,36 @@ 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 } +tracing-subscriber.workspace = true [target.'cfg(unix)'.dependencies] 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 +124,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 +137,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 +194,7 @@ members = [ "fuzz", "winch/codegen", ] -exclude = [ - 'docs/rust_wasi_markdown_parser', -] +exclude = ['docs/rust_wasi_markdown_parser'] [workspace.package] version = "38.0.0" @@ -253,8 +275,8 @@ wasmtime-wmemcheck = { path = "crates/wmemcheck", version = "=38.0.0", package = wasmtime-c-api-macros = { path = "crates/c-api-macros", version = "=38.0.0", package = 'wasmtime-internal-c-api-macros' } wasmtime-cache = { path = "crates/cache", version = "=38.0.0", package = 'wasmtime-internal-cache' } wasmtime-cranelift = { path = "crates/cranelift", version = "=38.0.0", package = 'wasmtime-internal-cranelift' } -wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } -wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } +wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } +wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } wasmtime-fiber = { path = "crates/fiber", version = "=38.0.0", package = 'wasmtime-internal-fiber' } wasmtime-jit-debug = { path = "crates/jit-debug", version = "=38.0.0", package = 'wasmtime-internal-jit-debug' } wasmtime-component-util = { path = "crates/component-util", version = "=38.0.0", package = 'wasmtime-internal-component-util' } @@ -263,7 +285,7 @@ wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", ve wasmtime-slab = { path = "crates/slab", version = "=38.0.0", package = 'wasmtime-internal-slab' } wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=38.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=38.0.0", package = 'wasmtime-internal-wit-bindgen' } -wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } +wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } wasmtime-unwinder = { path = "crates/unwinder", version = "=38.0.0", package = 'wasmtime-internal-unwinder' } # Miscellaneous crates without a `wasmtime-*` prefix in their name but still @@ -277,7 +299,10 @@ pulley-macros = { path = 'pulley/macros', version = "=38.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.125.0" } -cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = ["std", "unwind"] } +cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = [ + "std", + "unwind", +] } cranelift-frontend = { path = "cranelift/frontend", version = "0.125.0" } cranelift-entity = { path = "cranelift/entity", version = "0.125.0" } cranelift-native = { path = "cranelift/native", version = "0.125.0" } @@ -328,7 +353,9 @@ wit-bindgen = { version = "0.46.0", default-features = false } wit-bindgen-rust-macro = { version = "0.46.0", default-features = false } # wasm-tools family: -wasmparser = { version = "0.239.0", default-features = false, features = ['simd'] } +wasmparser = { version = "0.239.0", default-features = false, features = [ + 'simd', +] } wat = "1.239.0" wast = "239.0.0" wasmprinter = "0.239.0" @@ -346,14 +373,20 @@ json-from-wast = "0.239.0" 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" @@ -382,7 +415,7 @@ tempfile = "3.21.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" @@ -394,10 +427,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 } @@ -413,7 +454,6 @@ base64 = "0.22.1" termcolor = "1.4.1" flate2 = "1.0.30" tokio-util = "0.7.4" -arbtest = "0.3.1" # ============================================================================= # @@ -506,14 +546,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"] @@ -530,7 +579,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. From 02c1303ecc1b029acb14f6af48beb3f9b556ca9d Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:47:11 +0100 Subject: [PATCH 24/63] Revert cargo changes --- Cargo.toml | 100 +++++++++++++---------------------------------------- 1 file changed, 24 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb496e36e9c1..8b0a18df76ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,10 +50,7 @@ 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 } @@ -85,36 +82,22 @@ 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 } -tracing-subscriber.workspace = true [target.'cfg(unix)'.dependencies] 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" @@ -124,10 +107,7 @@ 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 } @@ -137,11 +117,7 @@ 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 } @@ -194,7 +170,9 @@ members = [ "fuzz", "winch/codegen", ] -exclude = ['docs/rust_wasi_markdown_parser'] +exclude = [ + 'docs/rust_wasi_markdown_parser', +] [workspace.package] version = "38.0.0" @@ -275,8 +253,8 @@ wasmtime-wmemcheck = { path = "crates/wmemcheck", version = "=38.0.0", package = wasmtime-c-api-macros = { path = "crates/c-api-macros", version = "=38.0.0", package = 'wasmtime-internal-c-api-macros' } wasmtime-cache = { path = "crates/cache", version = "=38.0.0", package = 'wasmtime-internal-cache' } wasmtime-cranelift = { path = "crates/cranelift", version = "=38.0.0", package = 'wasmtime-internal-cranelift' } -wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } -wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } +wasmtime-winch = { path = "crates/winch", version = "=38.0.0", package = 'wasmtime-internal-winch' } +wasmtime-explorer = { path = "crates/explorer", version = "=38.0.0", package = 'wasmtime-internal-explorer' } wasmtime-fiber = { path = "crates/fiber", version = "=38.0.0", package = 'wasmtime-internal-fiber' } wasmtime-jit-debug = { path = "crates/jit-debug", version = "=38.0.0", package = 'wasmtime-internal-jit-debug' } wasmtime-component-util = { path = "crates/component-util", version = "=38.0.0", package = 'wasmtime-internal-component-util' } @@ -285,7 +263,7 @@ wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", ve wasmtime-slab = { path = "crates/slab", version = "=38.0.0", package = 'wasmtime-internal-slab' } wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=38.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=38.0.0", package = 'wasmtime-internal-wit-bindgen' } -wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } +wasmtime-math = { path = "crates/math", version = "=38.0.0", package = 'wasmtime-internal-math' } wasmtime-unwinder = { path = "crates/unwinder", version = "=38.0.0", package = 'wasmtime-internal-unwinder' } # Miscellaneous crates without a `wasmtime-*` prefix in their name but still @@ -299,10 +277,7 @@ pulley-macros = { path = 'pulley/macros', version = "=38.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.125.0" } -cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = [ - "std", - "unwind", -] } +cranelift-codegen = { path = "cranelift/codegen", version = "0.125.0", default-features = false, features = ["std", "unwind"] } cranelift-frontend = { path = "cranelift/frontend", version = "0.125.0" } cranelift-entity = { path = "cranelift/entity", version = "0.125.0" } cranelift-native = { path = "cranelift/native", version = "0.125.0" } @@ -353,9 +328,7 @@ wit-bindgen = { version = "0.46.0", default-features = false } wit-bindgen-rust-macro = { version = "0.46.0", default-features = false } # wasm-tools family: -wasmparser = { version = "0.239.0", default-features = false, features = [ - 'simd', -] } +wasmparser = { version = "0.239.0", default-features = false, features = ['simd'] } wat = "1.239.0" wast = "239.0.0" wasmprinter = "0.239.0" @@ -373,20 +346,14 @@ json-from-wast = "0.239.0" 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" @@ -415,7 +382,7 @@ tempfile = "3.21.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" @@ -427,18 +394,10 @@ 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 } @@ -454,6 +413,7 @@ base64 = "0.22.1" termcolor = "1.4.1" flate2 = "1.0.30" tokio-util = "0.7.4" +arbtest = "0.3.1" # ============================================================================= # @@ -546,23 +506,14 @@ 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"] @@ -579,10 +530,7 @@ 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. From 8dded53d813c2deb6b58e9576427b607edbe95fb Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:55:32 +0100 Subject: [PATCH 25/63] Comments --- crates/wasmtime/src/runtime/component/concurrent.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 454df1127865..24add7a32ef4 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -604,14 +604,23 @@ enum SuspendReason { /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. Yielding { + /// The thread that is yielding thread: QualifiedThreadId, + /// The thread it is yielding to, if any (used for debug output) + #[allow(dead_code)] to: Option, + /// Whether the yield is cancellable (used for debug output) + #[allow(dead_code)] cancellable: bool, }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. ExplicitlySuspending { thread: QualifiedThreadId, + /// The thread it is yielding to, if any (used for debug output) + #[allow(dead_code)] to: Option, + /// Whether the suspend is cancellable (used for debug output) + #[allow(dead_code)] cancellable: bool, }, } @@ -4058,7 +4067,7 @@ enum GuestThreadState { Pending, Completed, } -pub(crate) struct GuestThread { +pub struct GuestThread { /// Context-local state used to implement the `context.{get,set}` /// intrinsics. context: [u32; 2], From 8a9725556f9c59b6fe7b70cd1490a69afd97ee20 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 9 Oct 2025 15:58:06 +0100 Subject: [PATCH 26/63] Comments on test --- tests/all/component_model/async.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index bdcf146eacb9..3fec76889731 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -315,6 +315,10 @@ async fn drop_resource_async() -> Result<()> { Ok(()) } +/// Test task deletion in three situations, for every combination of lift/lower/(guest/host): +/// 1. An explicit thread calls task.return +/// 2. An explicit thread suspends indefinitely +/// 3. An explicit thread yield loops indefinitely #[tokio::test] async fn task_deletion() -> Result<()> { let mut config = Config::new(); @@ -330,7 +334,7 @@ async fn task_deletion() -> Result<()> { (component $C (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) - ;; Defines the table for the thread start functions, of which there are two + ;; Defines the table for the thread start functions (core module $libc (table (export "__indirect_function_table") 3 funcref)) (core module $CM From a9a6ac7924d0262778856c2d80129d313a1779cd Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 16:55:23 +0100 Subject: [PATCH 27/63] Update comments --- crates/environ/src/component/info.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 28e363c69913..074e87ffb396 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -824,7 +824,7 @@ pub enum Trampoline { ThreadYield { /// The specific component instance which is calling the intrinsic. instance: RuntimeComponentInstanceIndex, - /// If `true`, indicates the caller instance maybe receive notification + /// If `true`, indicates the caller instance may receive notification /// of task cancellation. cancellable: bool, }, @@ -1134,7 +1134,8 @@ pub enum Trampoline { ThreadSwitchTo { /// The specific component instance which is calling the intrinsic. instance: RuntimeComponentInstanceIndex, - /// If `true`, indicates the caller instance maybe reentered. + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. cancellable: bool, }, @@ -1142,7 +1143,8 @@ pub enum Trampoline { ThreadSuspend { /// The specific component instance which is calling the intrinsic. instance: RuntimeComponentInstanceIndex, - /// If `true`, indicates the caller instance maybe reentered. + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. cancellable: bool, }, @@ -1153,7 +1155,8 @@ pub enum Trampoline { ThreadYieldTo { /// The specific component instance which is calling the intrinsic. instance: RuntimeComponentInstanceIndex, - /// If `true`, indicates the caller instance maybe reentered. + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. cancellable: bool, }, } From 13fd954e2e9f774e0eee9e3d0194076c32bcf411 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 16:56:34 +0100 Subject: [PATCH 28/63] Update comments --- crates/environ/src/trap_encoding.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/environ/src/trap_encoding.rs b/crates/environ/src/trap_encoding.rs index 8db629bec55b..15efa99b40fa 100644 --- a/crates/environ/src/trap_encoding.rs +++ b/crates/environ/src/trap_encoding.rs @@ -109,8 +109,8 @@ pub enum Trap { AsyncDeadlock, /// 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. + /// 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 } From d287f8492c881f5a2e29e1c11124ec552bb3eda0 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 16:57:11 +0100 Subject: [PATCH 29/63] Add space that was removed in an earlier commit --- crates/misc/component-async-tests/wit/test.wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/misc/component-async-tests/wit/test.wit b/crates/misc/component-async-tests/wit/test.wit index 6c07c7e88c1c..88d52af74602 100644 --- a/crates/misc/component-async-tests/wit/test.wit +++ b/crates/misc/component-async-tests/wit/test.wit @@ -347,4 +347,4 @@ world sleep-post-return-caller { world closed-stream-guest { export closed-stream; -} \ No newline at end of file +} From 74fe6afd30de996847ef62dfb6f6f490b7996146 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 17:33:10 +0100 Subject: [PATCH 30/63] Cleanup --- .../src/runtime/component/concurrent.rs | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index fc4f3e4c0c9f..05d5c4991e8e 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1597,19 +1597,14 @@ impl Instance { callback_code::EXIT => { log::trace!("implicit thread {guest_thread:?} completed"); state.get_mut(guest_thread.thread)?.state = GuestThreadState::Completed; - state - .get_mut(guest_thread.task)? - .threads - .remove(&guest_thread.thread); - if state.get_mut(guest_thread.task)?.threads.is_empty() - && !state.get_mut(guest_thread.task)?.returned_or_cancelled() - { + let task = state.get_mut(guest_thread.task)?; + task.threads.remove(&guest_thread.thread); + if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } - let task = state.get_mut(guest_thread.task)?; match &task.caller { Caller::Host { .. } => { - if state.get_mut(guest_thread.task)?.ready_to_delete() { + if task.ready_to_delete() { Waitable::Guest(guest_thread.task).delete_from(state)?; } } @@ -1868,34 +1863,11 @@ impl Instance { .concurrent_state_mut() .get_mut(guest_thread.thread)? .state = GuestThreadState::Completed; - store - .concurrent_state_mut() - .get_mut(guest_thread.task)? - .threads - .remove(&guest_thread.thread); - - // This is a callback-less call, so the implicit thread has now completed - store - .concurrent_state_mut() - .get_mut(guest_thread.thread)? - .state = GuestThreadState::Completed; - store - .concurrent_state_mut() - .get_mut(guest_thread.task)? - .threads - .remove(&guest_thread.thread); + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; + task.threads.remove(&guest_thread.thread); if async_ { - if store - .concurrent_state_mut() - .get_mut(guest_thread.task)? - .threads - .is_empty() - && !store - .concurrent_state_mut() - .get_mut(guest_thread.task)? - .returned_or_cancelled() - { + if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } } else { @@ -2393,11 +2365,6 @@ impl Instance { guest_waitable.join(state, old_set)?; - // Reset the current thread to point to the caller as it resumes control. - state.guest_thread = Some(caller); - state.get_mut(caller.thread)?.state = GuestThreadState::Running; - log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); - if let Some(storage) = storage { // The caller used a sync-lowered import to call an async-lifted // export, in which case the result, if any, has been stashed in @@ -2416,6 +2383,11 @@ impl Instance { } } + // Reset the current thread to point to the caller as it resumes control. + state.guest_thread = Some(caller); + state.get_mut(caller.thread)?.state = GuestThreadState::Running; + log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); + Ok(status.pack(waitable)) } @@ -2989,11 +2961,10 @@ impl Instance { let state = store.0.concurrent_state_mut(); state.get_mut(thread.thread)?.state = GuestThreadState::Completed; - state.get_mut(thread.task)?.threads.remove(&thread.thread); + let task = state.get_mut(thread.task)?; + task.threads.remove(&thread.thread); log::trace!("explicit thread {thread:?} completed"); - if state.get_mut(thread.task)?.threads.is_empty() - && !state.get_mut(thread.task)?.returned_or_cancelled() - { + if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } state.guest_thread = old_thread; @@ -4953,8 +4924,7 @@ impl TaskId { pub(crate) fn host_future_dropped(&self, store: StoreContextMut) -> Result<()> { let task = store.0.concurrent_state_mut().get_mut(self.task)?; if !task.have_lowered_parameters() { - Waitable::Guest(self.task) - .delete_from(store.0.concurrent_state_mut())? + Waitable::Guest(self.task).delete_from(store.0.concurrent_state_mut())? } else { task.host_future_state = HostFutureState::Dropped; if task.ready_to_delete() { From 17a481c5b53c59ed77e6e9b2dd1ec3ccc586dbc1 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 18:01:47 +0100 Subject: [PATCH 31/63] Delete threads from the instance table --- crates/wasmtime/src/runtime/component/concurrent.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 05d5c4991e8e..e765c49703d7 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1596,7 +1596,7 @@ impl Instance { match code { callback_code::EXIT => { log::trace!("implicit thread {guest_thread:?} completed"); - state.get_mut(guest_thread.thread)?.state = GuestThreadState::Completed; + state.delete(guest_thread.thread)?; let task = state.get_mut(guest_thread.task)?; task.threads.remove(&guest_thread.thread); if task.threads.is_empty() && !task.returned_or_cancelled() { @@ -1859,10 +1859,7 @@ impl Instance { let storage = call(store)?; // This is a callback-less call, so the implicit thread has now completed - store - .concurrent_state_mut() - .get_mut(guest_thread.thread)? - .state = GuestThreadState::Completed; + store.concurrent_state_mut().delete(guest_thread.thread)?; let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; task.threads.remove(&guest_thread.thread); @@ -2960,7 +2957,7 @@ impl Instance { store.0.maybe_pop_call_context(thread.task)?; let state = store.0.concurrent_state_mut(); - state.get_mut(thread.thread)?.state = GuestThreadState::Completed; + state.delete(thread.thread)?; let task = state.get_mut(thread.task)?; task.threads.remove(&thread.thread); log::trace!("explicit thread {thread:?} completed"); @@ -4170,6 +4167,10 @@ impl GuestTask { } } + for thread in self.threads { + state.delete(thread)?; + } + state.delete(self.sync_call_set)?; // Reparent any pending subtasks to the caller. From 9fea7c3d0b349f37b1605b3e4eb76eaa8d4331da Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 18:08:35 +0100 Subject: [PATCH 32/63] Revert cargo file --- crates/wasmtime/Cargo.toml | 39 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index b243750877e0..fd9c51f93248 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -22,9 +22,7 @@ wasmtime-jit-debug = { workspace = true, optional = true } wasmtime-jit-icache-coherence = { workspace = true, optional = true } wasmtime-cache = { workspace = true, optional = true } wasmtime-fiber = { workspace = true, optional = true } -wasmtime-cranelift = { workspace = true, optional = true, features = [ - 'pulley', -] } +wasmtime-cranelift = { workspace = true, optional = true, features = ['pulley'] } wasmtime-unwinder = { workspace = true, optional = true } wasmtime-winch = { workspace = true, optional = true } wasmtime-component-macro = { workspace = true, optional = true } @@ -200,22 +198,22 @@ cache = ["dep:wasmtime-cache", "std"] # Enables support for "async stores" as well as defining host functions as # `async fn` and calling functions asynchronously. -async = ["dep:wasmtime-fiber", "wasmtime-component-macro?/async", "runtime"] +async = [ + "dep:wasmtime-fiber", + "wasmtime-component-macro?/async", + "runtime", +] # Enables support for the pooling instance allocation strategy pooling-allocator = [ "runtime", - "std", # not ported to no_std yet + "std", # not ported to no_std yet ] # Enables support for all architectures in Cranelift, allowing # cross-compilation using the `wasmtime` crate's API, notably the # `Engine::precompile_module` function. -all-arch = [ - "wasmtime-cranelift?/all-arch", - "wasmtime-winch?/all-arch", - "pulley", -] +all-arch = ["wasmtime-cranelift?/all-arch", "wasmtime-winch?/all-arch", "pulley"] # Enables in-progress support for the component model. Note that this feature is # in-progress, buggy, and incomplete. This is primarily here for internal @@ -250,7 +248,9 @@ coredump = ["dep:wasm-encoder", "runtime", "std"] # Export some symbols from the final binary to assist in debugging # Cranelift-generated code with native debuggers like GDB and LLDB. -debug-builtins = ["wasmtime-jit-debug/gdb_jit_int"] +debug-builtins = [ + "wasmtime-jit-debug/gdb_jit_int", +] # Enable support for executing compiled Wasm modules. runtime = [ @@ -283,7 +283,11 @@ runtime = [ # # You can additionally configure which GC implementations are enabled via the # `gc-drc` and `gc-null` features. -gc = ["wasmtime-environ/gc", "wasmtime-cranelift?/gc", "wasmtime-winch?/gc"] +gc = [ + "wasmtime-environ/gc", + "wasmtime-cranelift?/gc", + "wasmtime-winch?/gc", +] # Enable the deferred reference counting garbage collector. gc-drc = [ @@ -302,7 +306,11 @@ gc-null = [ ] # Enable runtime support for the WebAssembly threads proposal. -threads = ["wasmtime-cranelift?/threads", "wasmtime-winch?/threads", "std"] +threads = [ + "wasmtime-cranelift?/threads", + "wasmtime-winch?/threads", + "std", +] stack-switching = [ "runtime", @@ -397,4 +405,7 @@ component-model-async = [ ] # Enables support for `stream` interop with the `bytes` crate. -component-model-async-bytes = ["component-model-async", "dep:bytes"] +component-model-async-bytes = [ + "component-model-async", + "dep:bytes", +] From 6622b48649010f80c75a2b8587f67ef008e63ef2 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 18:15:44 +0100 Subject: [PATCH 33/63] Remove dead code --- .../src/runtime/component/concurrent.rs | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index e765c49703d7..ec1f0ee0b7ed 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -576,26 +576,9 @@ enum SuspendReason { NeedWork, /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. - Yielding { - /// The thread that is yielding - thread: QualifiedThreadId, - /// The thread it is yielding to, if any (used for debug output) - #[allow(dead_code)] - to: Option, - /// Whether the yield is cancellable (used for debug output) - #[allow(dead_code)] - cancellable: bool, - }, + Yielding { thread: QualifiedThreadId }, /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. - ExplicitlySuspending { - thread: QualifiedThreadId, - /// The thread it is yielding to, if any (used for debug output) - #[allow(dead_code)] - to: Option, - /// Whether the suspend is cancellable (used for debug output) - #[allow(dead_code)] - cancellable: bool, - }, + ExplicitlySuspending { thread: QualifiedThreadId }, } /// Represents a pending call into guest code for a given guest task. @@ -3069,21 +3052,13 @@ impl Instance { let state = store.0.concurrent_state_mut(); let guest_thread = state.guest_thread.unwrap(); - let to_thread = match to_thread { - Some(t) => Some(QualifiedThreadId::qualify(state, t)?), - None => None, - }; let reason = if yielding { SuspendReason::Yielding { thread: guest_thread, - to: to_thread, - cancellable, } } else { SuspendReason::ExplicitlySuspending { thread: guest_thread, - to: to_thread, - cancellable, } }; @@ -3110,8 +3085,6 @@ impl Instance { // First, suspend this fiber, allowing any other threads to run. store.suspend(SuspendReason::Yielding { thread: guest_thread, - to: None, - cancellable, })?; log::trace!("waitable check for {guest_thread:?}; set {set:?}"); @@ -3287,11 +3260,7 @@ impl Instance { }; concurrent_state.push_high_priority(item); - store.suspend(SuspendReason::Yielding { - thread: caller, - to: None, - cancellable: false, - })?; + store.suspend(SuspendReason::Yielding { thread: caller })?; break; } } From a6a7af38d1a5cede6015e4cfb11f633cc04ec300 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 18:20:54 +0100 Subject: [PATCH 34/63] Clippy changes --- crates/wasmtime/src/runtime/component/concurrent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index ec1f0ee0b7ed..c41e3e879693 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3941,7 +3941,7 @@ impl GuestThread { fn new_implicit(parent_task: TableId) -> Self { Self { context: [0; 2], - parent_task: parent_task, + parent_task, wake_on_cancel: None, state: GuestThreadState::NotStartedImplicit, } From ebe6237396156ffe8f95c7ce22f04497cff917fb Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 10 Oct 2025 18:25:07 +0100 Subject: [PATCH 35/63] Clippy --- crates/wasmtime/src/runtime/component/concurrent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index c41e3e879693..e001c99e300f 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3955,7 +3955,7 @@ impl GuestThread { ) -> Self { Self { context: [0; 2], - parent_task: parent_task, + parent_task, wake_on_cancel: None, state: GuestThreadState::NotStartedExplicit { start_func_table_idx, From 8eae4433b2aee5f14444ca7a0176383e0f7ba1c4 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 10:13:28 +0100 Subject: [PATCH 36/63] Revert unnecessary changes --- tests/misc_testsuite/component-model-async/streams.wast | 1 - tests/misc_testsuite/component-model-async/task-builtins.wast | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/misc_testsuite/component-model-async/streams.wast b/tests/misc_testsuite/component-model-async/streams.wast index 3e3f500642f1..fa0b2e02c0a6 100644 --- a/tests/misc_testsuite/component-model-async/streams.wast +++ b/tests/misc_testsuite/component-model-async/streams.wast @@ -1,6 +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 fa327b08a387..b451707f0acb 100644 --- a/tests/misc_testsuite/component-model-async/task-builtins.wast +++ b/tests/misc_testsuite/component-model-async/task-builtins.wast @@ -1,6 +1,5 @@ ;;! component_model_async = true ;;! component_model_async_stackful = true -;;! component_model_threading = true ;; backpressure.set (component From 2c448f138532861c09e48653b5aa1a1864e9aa3d Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 10:15:48 +0100 Subject: [PATCH 37/63] Revert unnecessary changes --- tests/all/pooling_allocator.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/all/pooling_allocator.rs b/tests/all/pooling_allocator.rs index 7f9412a725e3..fddd26bf64e8 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", - "thread-yield", + "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" "thread-yield" (func $thread-yield)) + (import "async" "yield" (func $yield)) (func (export "run") - call $thread-yield + call $yield ) (func $empty) From 5445d85ef37d3d9d5034285be09498d7ca2d605d Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 10:23:04 +0100 Subject: [PATCH 38/63] Revert unnecessary changes --- tests/all/async_functions.rs | 6 +++--- tests/all/component_model/async.rs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/all/async_functions.rs b/tests/all/async_functions.rs index 3dbeb856cf66..06a657fd1ef3 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 "" "thread-yield" (func $thread-yield)) + (import "" "yield" (func $yield)) (func $run_async (export "run_async") - call $thread-yield + call $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("", "thread-yield", { + .func_wrap_async("", "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 3fec76889731..80601194dade 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 "thread-yield" (func $thread-yield (result (list u8)))) + (import "yield" (func $yield (result (list u8)))) (core module $libc (memory (export "memory") 1) {REALLOC_AND_FREE} ) (core instance $libc (instantiate $libc)) - (core func $thread-yield + (core func $yield (canon lower - (func $thread-yield) + (func $yield) (memory $libc "memory") (realloc (func $libc "realloc")) ) ) (core module $m - (import "" "thread-yield" (func $thread-yield (param i32))) + (import "" "yield" (func $yield (param i32))) (import "libc" "memory" (memory 0)) (func $start i32.const 8 - call $thread-yield + call $yield ) (start $start) ) (core instance (instantiate $m - (with "" (instance (export "thread-yield" (func $thread-yield)))) + (with "" (instance (export "yield" (func $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("thread-yield", |_: StoreContextMut<()>, _: ()| { + .func_wrap_async("yield", |_: StoreContextMut<()>, _: ()| { Box::new(async { tokio::task::yield_now().await; Ok((vec![1u8, 2u8],)) From a9a0e269adda2326036ba964455f2cf08e6212a4 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 10:24:55 +0100 Subject: [PATCH 39/63] Revert unnecessary changes --- tests/misc_testsuite/component-model-async/fused.wast | 1 - tests/misc_testsuite/component-model-async/futures.wast | 1 - tests/misc_testsuite/component-model-async/lift.wast | 1 - 3 files changed, 3 deletions(-) diff --git a/tests/misc_testsuite/component-model-async/fused.wast b/tests/misc_testsuite/component-model-async/fused.wast index 3ced2e1ed228..00e8a8d9d3d2 100644 --- a/tests/misc_testsuite/component-model-async/fused.wast +++ b/tests/misc_testsuite/component-model-async/fused.wast @@ -1,6 +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 062b53a55dcc..ec5c3457c7d0 100644 --- a/tests/misc_testsuite/component-model-async/futures.wast +++ b/tests/misc_testsuite/component-model-async/futures.wast @@ -1,6 +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 d7682a33a7ca..f1175b0e89a4 100644 --- a/tests/misc_testsuite/component-model-async/lift.wast +++ b/tests/misc_testsuite/component-model-async/lift.wast @@ -1,6 +1,5 @@ ;;! component_model_async = true ;;! component_model_async_stackful = true -;;! component_model_threading = true ;; async lift; no callback (component From 80c130ed05978a7105c10249b3212f59c2684192 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 10:53:10 +0100 Subject: [PATCH 40/63] Revert unnecessary changes --- tests/component-model | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/component-model b/tests/component-model index 3870cbe964d8..8021b5101bfe 160000 --- a/tests/component-model +++ b/tests/component-model @@ -1 +1 @@ -Subproject commit 3870cbe964d8bc84777c182a883a9da00f6ea2cb +Subproject commit 8021b5101bfefd00048230f92a206486341e8b0d From f34811e98f3a841a098bdbc790a37f6bd96f9cf9 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 11:01:58 +0100 Subject: [PATCH 41/63] Review comments --- .../src/runtime/component/concurrent.rs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index e001c99e300f..12224919330f 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3018,17 +3018,6 @@ impl Instance { Ok(()) } - fn pending_cancellation(&self, store: &mut StoreOpaque) -> bool { - let state = store.concurrent_state_mut(); - 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( @@ -3040,7 +3029,7 @@ impl Instance { to_thread: Option>, ) -> Result { // There could be a pending cancellation from a previous uncancellable wait - if cancellable && self.pending_cancellation(store.0) { + if cancellable && store.0.concurrent_state_mut().pending_cancellation() { return Ok(true); } @@ -3064,7 +3053,7 @@ impl Instance { store.0.suspend(reason)?; - Ok(cancellable && self.pending_cancellation(store.0)) + Ok(cancellable && store.0.concurrent_state_mut().pending_cancellation()) } /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. @@ -4484,9 +4473,9 @@ pub struct ConcurrentState { /// populated as needed. // TODO: this can and should be a `PrimaryMap` instance_states: HashMap, - /// The "high priority" work queue for this instance's event loop. + /// The "high priority" work queue for this store's event loop. high_priority: Vec, - /// The "low priority" work queue for this instance's event loop. + /// The "low priority" work queue for this store'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 @@ -4771,6 +4760,18 @@ impl ConcurrentState { .thread .rep()) } + + /// Returns whether there's a pending cancellation on the current guest thread, + /// consuming the event if so. + fn pending_cancellation(&mut self) -> bool { + let thread = self.guest_thread.unwrap(); + if let Some(event) = self.get_mut(thread.task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + true + } else { + false + } + } } /// Provide a type hint to compiler about the shape of a parameter lower From 5aeaf25f12b68f465ec9ab6b432107a937c2d67a Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 17 Oct 2025 11:05:40 +0100 Subject: [PATCH 42/63] Review feedback --- crates/wasmtime/src/runtime/component/concurrent.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 12224919330f..844c5fd4fdd4 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3191,7 +3191,7 @@ impl Instance { let caller = concurrent_state.guest_thread.unwrap(); let guest_task = TableId::::new(rep); let task = concurrent_state.get_mut(guest_task)?; - if !task.have_lowered_parameters() { + if !task.already_lowered_parameters() { task.lower_params = None; task.lift_result = None; @@ -4042,7 +4042,7 @@ pub(crate) struct GuestTask { } impl GuestTask { - fn have_lowered_parameters(&self) -> bool { + fn already_lowered_parameters(&self) -> bool { // We reset `lower_params` after we lower the parameters self.lower_params.is_none() } @@ -4894,7 +4894,7 @@ impl TaskId { /// and delete the task when all threads are done. pub(crate) fn host_future_dropped(&self, store: StoreContextMut) -> Result<()> { let task = store.0.concurrent_state_mut().get_mut(self.task)?; - if !task.have_lowered_parameters() { + if !task.already_lowered_parameters() { Waitable::Guest(self.task).delete_from(store.0.concurrent_state_mut())? } else { task.host_future_state = HostFutureState::Dropped; From d01a51d1f8114e9d19aad82fd100f4e5d528f0d1 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 20 Oct 2025 15:49:53 +0100 Subject: [PATCH 43/63] Make thread IDs per-component-instance --- crates/cranelift/src/compiler/component.rs | 6 +- crates/environ/src/component.rs | 2 +- crates/environ/src/component/dfg.rs | 8 +- crates/environ/src/component/info.rs | 7 +- .../environ/src/component/translate/inline.rs | 10 +- .../src/runtime/component/concurrent.rs | 130 ++++++++++++++---- .../src/runtime/vm/component/handle_table.rs | 32 +++++ .../src/runtime/vm/component/libcalls.rs | 25 ++-- .../futures-must-write.wast | 4 +- .../partial-stream-copies.wast | 12 +- .../component-model/resources.wast | 22 +-- 11 files changed, 188 insertions(+), 70 deletions(-) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 18efa8cce7fb..d40c9cccdbbf 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -849,12 +849,14 @@ impl<'a> TrampolineCompiler<'a> { }, ); } - Trampoline::ThreadResumeLater => { + Trampoline::ThreadResumeLater { instance } => { self.translate_libcall( host::thread_resume_later, TrapSentinel::Falsy, WasmArgs::InRegisters, - |_, _| {}, + |me, params| { + params.push(me.index_value(*instance)); + }, ); } Trampoline::ThreadYieldTo { diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index dec960fee091..e67804c94c5b 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_suspend(vmctx: vmctx, caller_instance: u32, cancellable: u8) -> u32; #[cfg(feature = "component-model-async")] - thread_resume_later(vmctx: vmctx, thread_idx: u32) -> bool; + thread_resume_later(vmctx: vmctx, caller_instance: u32, thread_idx: u32) -> bool; #[cfg(feature = "component-model-async")] thread_yield_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index bb3f9cdcef39..f7483ee52fe0 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -493,7 +493,9 @@ pub enum Trampoline { instance: RuntimeComponentInstanceIndex, cancellable: bool, }, - ThreadResumeLater, + ThreadResumeLater { + instance: RuntimeComponentInstanceIndex, + }, ThreadYieldTo { instance: RuntimeComponentInstanceIndex, cancellable: bool, @@ -1173,7 +1175,9 @@ impl LinearizeDfg<'_> { instance: *instance, cancellable: *cancellable, }, - Trampoline::ThreadResumeLater => info::Trampoline::ThreadResumeLater, + Trampoline::ThreadResumeLater { instance } => info::Trampoline::ThreadResumeLater { + instance: *instance, + }, Trampoline::ThreadYieldTo { instance, cancellable, diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 074e87ffb396..4dbdc32f3899 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -1149,7 +1149,10 @@ pub enum Trampoline { }, /// Intrinsic used to implement the `thread.resume-later` component model builtin. - ThreadResumeLater, + ThreadResumeLater { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + }, /// Intrinsic used to implement the `thread.yield-to` component model builtin. ThreadYieldTo { @@ -1228,7 +1231,7 @@ impl Trampoline { ThreadNewIndirect { .. } => format!("thread-new-indirect"), ThreadSwitchTo { .. } => format!("thread-switch-to"), ThreadSuspend { .. } => format!("thread-suspend"), - ThreadResumeLater => format!("thread-resume-later"), + ThreadResumeLater { .. } => format!("thread-resume-later"), ThreadYieldTo { .. } => format!("thread-yield-to"), } } diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index b469382c2e24..b45fc028d8af 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1160,10 +1160,12 @@ impl<'a> Inliner<'a> { frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } ThreadResumeLater { func } => { - let index = self - .result - .trampolines - .push((*func, dfg::Trampoline::ThreadResumeLater)); + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadResumeLater { + instance: frame.instance, + }, + )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } ThreadYieldTo { func, cancellable } => { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 844c5fd4fdd4..a1483accd77f 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1550,9 +1550,7 @@ impl Instance { log::trace!("received callback code from {guest_thread:?}: {code} (set: {set})"); let state = store.concurrent_state_mut(); - let task = state.get_mut(guest_thread.task)?; - - if !task.returned_or_cancelled() { + if !state.get_mut(guest_thread.task)?.returned_or_cancelled() { if initial_call { // Notify any current or future waiters that this subtask has // started. @@ -1579,16 +1577,16 @@ impl Instance { match code { callback_code::EXIT => { log::trace!("implicit thread {guest_thread:?} completed"); - state.delete(guest_thread.thread)?; - let task = state.get_mut(guest_thread.task)?; - task.threads.remove(&guest_thread.thread); + self.cleanup_thread(store, guest_thread, runtime_instance)?; + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } match &task.caller { Caller::Host { .. } => { if task.ready_to_delete() { - Waitable::Guest(guest_thread.task).delete_from(state)?; + Waitable::Guest(guest_thread.task) + .delete_from(store.concurrent_state_mut())?; } } Caller::Guest { .. } => { @@ -1666,6 +1664,25 @@ impl Instance { Ok(()) } + fn cleanup_thread( + self, + store: &mut StoreOpaque, + guest_thread: QualifiedThreadId, + runtime_instance: RuntimeComponentInstanceIndex, + ) -> Result<()> { + let guest_id = store + .concurrent_state_mut() + .get_mut(guest_thread.thread)? + .instance_rep; + self.id().get_mut(store).guest_tables().0[runtime_instance] + .guest_thread_remove(guest_id.unwrap())?; + + store.concurrent_state_mut().delete(guest_thread.thread)?; + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; + task.threads.remove(&guest_thread.thread); + Ok(()) + } + /// Add the specified guest call to the "high priority" work item queue, to /// be started as soon as backpressure and/or reentrance rules allow. /// @@ -1713,6 +1730,7 @@ impl Instance { let token = StoreToken::new(store); move |store: &mut dyn VMStore| { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; + store .concurrent_state_mut() .get_mut(guest_thread.thread)? @@ -1772,6 +1790,14 @@ impl Instance { assert!(async_); Box::new(move |store: &mut dyn VMStore| { + let guest_id = self.id().get_mut(store).guest_tables().0[callee_instance] + .guest_thread_insert(guest_thread.thread.rep())?; + + store + .concurrent_state_mut() + .get_mut(guest_thread.thread)? + .instance_rep = Some(guest_id); + let old_thread = store .concurrent_state_mut() .guest_thread @@ -1815,6 +1841,13 @@ impl Instance { } else { let token = StoreToken::new(store.as_context_mut()); Box::new(move |store: &mut dyn VMStore| { + let guest_id = self.id().get_mut(store).guest_tables().0[callee_instance] + .guest_thread_insert(guest_thread.thread.rep())?; + + store + .concurrent_state_mut() + .get_mut(guest_thread.thread)? + .instance_rep = Some(guest_id); let old_thread = store .concurrent_state_mut() .guest_thread @@ -1842,11 +1875,10 @@ impl Instance { let storage = call(store)?; // This is a callback-less call, so the implicit thread has now completed - store.concurrent_state_mut().delete(guest_thread.thread)?; - let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; - task.threads.remove(&guest_thread.thread); + self.cleanup_thread(store, guest_thread, callee_instance)?; if async_ { + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } @@ -2128,6 +2160,7 @@ impl Instance { let guest_thread = state.push(new_thread)?; state.get_mut(guest_task)?.threads.insert(guest_thread); + let state = store.0.concurrent_state_mut(); if let Some(old_thread) = old_thread { if !state.may_enter(guest_task) { bail!(crate::Trap::CannotEnterComponent); @@ -2866,6 +2899,21 @@ impl Instance { } } + /// Implements the `thread.index` intrinsic. + pub(crate) fn thread_index(&self, store: &mut dyn VMStore) -> Result { + let thread_id = store + .concurrent_state_mut() + .guest_thread + .ok_or_else(|| anyhow!("no current thread"))? + .thread; + // The unwrap is safe because `instance_rep` must be `Some` by this point + Ok(store + .concurrent_state_mut() + .get_mut(thread_id)? + .instance_rep + .unwrap()) + } + /// Implements the `thread.new_indirect` intrinsic. pub(crate) fn thread_new_indirect( self, @@ -2901,13 +2949,20 @@ impl Instance { log::trace!("new thread with id {thread_id:?} created"); - Ok(thread_id.rep()) + let guest_id = self.id().get_mut(store).guest_tables().0[caller] + .guest_thread_insert(thread_id.rep())?; + store + .concurrent_state_mut() + .get_mut(thread_id)? + .instance_rep = Some(guest_id); + Ok(guest_id) } fn start_thread( self, store: &mut StoreContextMut, thread: QualifiedThreadId, + runtime_instance: RuntimeComponentInstanceIndex, start_func_table_idx: RuntimeTableIndex, start_func_idx: u32, context: i32, @@ -2939,11 +2994,10 @@ impl Instance { store.0.maybe_pop_call_context(thread.task)?; + self.cleanup_thread(store.0, thread, runtime_instance)?; + log::trace!("explicit thread {thread:?} completed"); let state = store.0.concurrent_state_mut(); - state.delete(thread.thread)?; let task = state.get_mut(thread.task)?; - task.threads.remove(&thread.thread); - log::trace!("explicit thread {thread:?} completed"); if task.threads.is_empty() && !task.returned_or_cancelled() { bail!(Trap::NoAsyncResult); } @@ -2975,6 +3029,7 @@ impl Instance { pub(crate) fn resume_suspended_thread( self, mut store: StoreContextMut, + runtime_instance: RuntimeComponentInstanceIndex, thread_id: TableId, high_priority: bool, ) -> Result<()> { @@ -2991,6 +3046,7 @@ impl Instance { self.start_thread( &mut store, guest_thread, + runtime_instance, start_func_table_idx, start_func_idx, context, @@ -3026,17 +3082,20 @@ 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 && store.0.concurrent_state_mut().pending_cancellation() { return Ok(true); } + let to_thread = to_thread + .and_then(|id| GuestThread::from_instance(self.id().get_mut(store.0), caller, id).ok()); + 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)?; + self.resume_suspended_thread(store.as_context_mut(), caller, thread, true)?; } let state = store.0.concurrent_state_mut(); @@ -3464,14 +3523,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: TableId, + caller: RuntimeComponentInstanceIndex, + thread: u32, ) -> Result<()>; } @@ -3775,7 +3835,7 @@ impl VMComponentAsyncStore for StoreInner { caller: RuntimeComponentInstanceIndex, cancellable: bool, yielding: bool, - to_thread: Option>, + to_thread: Option, ) -> Result { instance.suspension_intrinsic( StoreContextMut(self), @@ -3789,9 +3849,11 @@ impl VMComponentAsyncStore for StoreInner { fn thread_resume_later( &mut self, instance: Instance, - thread: TableId, + caller: RuntimeComponentInstanceIndex, + thread: u32, ) -> Result<()> { - instance.resume_suspended_thread(StoreContextMut(self), thread, false) + let thread = GuestThread::from_instance(instance.id().get_mut(self), caller, thread)?; + instance.resume_suspended_thread(StoreContextMut(self), caller, thread, false) } } @@ -3923,16 +3985,32 @@ pub struct GuestThread { /// 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 execution state of this guest thread state: GuestThreadState, + /// The index of this thread in the component instance's handle table. + /// This must always be `Some` after initialization. + instance_rep: Option, } impl GuestThread { + /// Retrieve the `GuestThread` corresponding to the specified guest-visible + /// handle. + fn from_instance( + state: Pin<&mut ComponentInstance>, + caller_instance: RuntimeComponentInstanceIndex, + guest_thread: u32, + ) -> Result> { + let rep = state.guest_tables().0[caller_instance].guest_thread_rep(guest_thread)?; + Ok(TableId::new(rep)) + } + fn new_implicit(parent_task: TableId) -> Self { Self { context: [0; 2], parent_task, wake_on_cancel: None, state: GuestThreadState::NotStartedImplicit, + instance_rep: None, } } @@ -3951,6 +4029,7 @@ impl GuestThread { start_func_idx, context, }, + instance_rep: None, } } } @@ -4752,15 +4831,6 @@ impl ConcurrentState { Ok(()) } - /// Implements the `thread.index` intrinsic. - pub(crate) fn thread_index(&self) -> Result { - Ok(self - .guest_thread - .ok_or_else(|| anyhow!("no current thread"))? - .thread - .rep()) - } - /// Returns whether there's a pending cancellation on the current guest thread, /// consuming the event if so. fn pending_cancellation(&mut self) -> bool { diff --git a/crates/wasmtime/src/runtime/vm/component/handle_table.rs b/crates/wasmtime/src/runtime/vm/component/handle_table.rs index c550e2fde871..60ce64793c30 100644 --- a/crates/wasmtime/src/runtime/vm/component/handle_table.rs +++ b/crates/wasmtime/src/runtime/vm/component/handle_table.rs @@ -46,6 +46,7 @@ pub enum Waitable { Stream, } +#[derive(Debug)] enum Slot { Free { next: u32, @@ -80,6 +81,11 @@ enum Slot { rep: u32, }, + /// Represents a guest thread handle. + GuestThread { + rep: u32, + }, + /// Represents a stream handle. Stream { ty: TypeStreamTableIndex, @@ -565,4 +571,30 @@ impl HandleTable { _ => bail!("handle is not a waitable"), } } + + /// Inserts the guest thread `rep` into this table, returning the index it + /// now resides at. + pub fn guest_thread_insert(&mut self, rep: u32) -> Result { + self.insert(Slot::GuestThread { rep }) + } + + /// Returns the `rep` of a guest thread pointed to by `idx`. + pub fn guest_thread_rep(&mut self, idx: u32) -> Result { + match self.get_mut(idx)? { + Slot::GuestThread { rep } => Ok(*rep), + _ => bail!("handle is not a guest thread"), + } + } + + /// Removes the guest thread pointed to by `idx`. + /// + /// Returns the internal `rep`. + pub fn guest_thread_remove(&mut self, idx: u32) -> Result { + let rep = match self.get_mut(idx)? { + Slot::GuestThread { rep } => *rep, + _ => bail!("handle is not a guest thread"), + }; + self.remove(idx)?; + Ok(rep) + } } diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index f42b6207ce73..27f2cdf2a44a 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -1,8 +1,6 @@ //! Implementation of string transcoding required by the component model. use crate::component::Instance; -#[cfg(feature = "component-model-async")] -use crate::component::concurrent::table::TableId; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; @@ -1372,8 +1370,8 @@ fn context_set( } #[cfg(feature = "component-model-async")] -fn thread_index(store: &mut dyn VMStore, _instance: Instance) -> Result { - store.concurrent_state_mut().thread_index() +fn thread_index(store: &mut dyn VMStore, instance: Instance) -> Result { + instance.thread_index(store) } #[cfg(feature = "component-model-async")] @@ -1409,7 +1407,7 @@ fn thread_switch_to( RuntimeComponentInstanceIndex::from_u32(caller), cancellable != 0, false, - Some(TableId::new(thread_idx)), + Some(thread_idx), ) } @@ -1430,10 +1428,17 @@ fn thread_suspend( } #[cfg(feature = "component-model-async")] -fn thread_resume_later(store: &mut dyn VMStore, instance: Instance, thread_idx: u32) -> Result<()> { - store - .component_async_store() - .thread_resume_later(instance, TableId::new(thread_idx)) +fn thread_resume_later( + store: &mut dyn VMStore, + instance: Instance, + caller_instance: u32, + thread_idx: u32, +) -> Result<()> { + store.component_async_store().thread_resume_later( + instance, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + thread_idx, + ) } #[cfg(feature = "component-model-async")] @@ -1449,6 +1454,6 @@ fn thread_yield_to( RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, true, - Some(TableId::new(thread_idx)), + Some(thread_idx), ) } diff --git a/tests/misc_testsuite/component-model-async/futures-must-write.wast b/tests/misc_testsuite/component-model-async/futures-must-write.wast index 2158c888a8fe..3855558356dd 100644 --- a/tests/misc_testsuite/component-model-async/futures-must-write.wast +++ b/tests/misc_testsuite/component-model-async/futures-must-write.wast @@ -79,7 +79,7 @@ ;; call 'start-future' to get the future we'll be working with (local $fr i32) (local.set $fr (call $start-future)) - (if (i32.ne (i32.const 1) (local.get $fr)) + (if (i32.ne (i32.const 2) (local.get $fr)) (then unreachable)) ;; ok to immediately drop the readable end @@ -92,7 +92,7 @@ ;; call 'start-future' to get the future we'll be working with (local $fr i32) (local.set $fr (call $start-future)) - (if (i32.ne (i32.const 1) (local.get $fr)) + (if (i32.ne (i32.const 2) (local.get $fr)) (then unreachable)) ;; boom diff --git a/tests/misc_testsuite/component-model-async/partial-stream-copies.wast b/tests/misc_testsuite/component-model-async/partial-stream-copies.wast index e6410b0d1cad..314a614e0a62 100644 --- a/tests/misc_testsuite/component-model-async/partial-stream-copies.wast +++ b/tests/misc_testsuite/component-model-async/partial-stream-copies.wast @@ -43,16 +43,16 @@ ;; check the incoming readable stream end (global.set $insr (local.get 0)) - (if (i32.ne (i32.const 2) (global.get $insr)) + (if (i32.ne (i32.const 3) (global.get $insr)) (then unreachable)) ;; create a new stream r/w pair $outsr/$outsw (local.set $ret64 (call $stream.new)) (local.set $outsr (i32.wrap_i64 (local.get $ret64))) - (if (i32.ne (i32.const 3) (local.get $outsr)) + (if (i32.ne (i32.const 4) (local.get $outsr)) (then unreachable)) (global.set $outsw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (if (i32.ne (i32.const 4) (global.get $outsw)) + (if (i32.ne (i32.const 5) (global.get $outsw)) (then unreachable)) ;; start async read on $insr which will block @@ -161,10 +161,10 @@ ;; create a new stream r/w pair $insr/$insw (local.set $ret64 (call $stream.new)) (local.set $insr (i32.wrap_i64 (local.get $ret64))) - (if (i32.ne (i32.const 1) (local.get $insr)) + (if (i32.ne (i32.const 2) (local.get $insr)) (then unreachable)) (local.set $insw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (if (i32.ne (i32.const 2) (local.get $insw)) + (if (i32.ne (i32.const 3) (local.get $insw)) (then unreachable)) ;; call 'transform' which will return a readable stream $outsr eagerly @@ -173,7 +173,7 @@ (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (local.get $ret)) (then unreachable)) (local.set $outsr (i32.load (local.get $retp))) - (if (i32.ne (i32.const 1) (local.get $outsr)) + (if (i32.ne (i32.const 2) (local.get $outsr)) (then unreachable)) ;; multiple write calls succeed until 12-byte buffer is filled diff --git a/tests/misc_testsuite/component-model/resources.wast b/tests/misc_testsuite/component-model/resources.wast index c9f94feca1cd..9c5ce16ac8b0 100644 --- a/tests/misc_testsuite/component-model/resources.wast +++ b/tests/misc_testsuite/component-model/resources.wast @@ -455,7 +455,7 @@ (instance $i (instantiate $inner)) ;; the rest of this component which is a single function that invokes `drop` - ;; for index 1. The index 1 should be valid within the above component, but + ;; for index 2. The index 2 should be valid within the above component, but ;; it is not valid within this component (alias export $i "r" (type $r)) (core func $drop (canon resource.drop $r)) @@ -464,7 +464,7 @@ (import "" "drop" (func $drop (param i32))) (func (export "f") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -476,7 +476,7 @@ (func (export "f") (canon lift (core func $i "f"))) ) -(assert_trap (invoke "f") "unknown handle index 1") +(assert_trap (invoke "f") "unknown handle index 2") ;; Each instantiation of a component generates a unique resource type, so ;; allocating in one component and deallocating in another should fail. @@ -492,10 +492,10 @@ (import "" "drop" (func $drop (param i32))) (func (export "alloc") - (if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable))) + (if (i32.ne (call $ctor (i32.const 100)) (i32.const 2)) (then (unreachable))) ) (func (export "dealloc") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -552,10 +552,10 @@ (import "" "drop" (func $drop (param i32))) (func (export "alloc") - (if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable))) + (if (i32.ne (call $ctor (i32.const 100)) (i32.const 2)) (then (unreachable))) ) (func (export "dealloc") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -1025,8 +1025,8 @@ (local.set $r1 (call $ctor (i32.const 100))) (local.set $r2 (call $ctor (i32.const 200))) - (if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable))) - (if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get $r1) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get $r2) (i32.const 3)) (then (unreachable))) (call $assert-borrow (local.get $r2) (i32.const 200)) (call $assert-borrow (local.get $r1) (i32.const 100)) @@ -1086,6 +1086,6 @@ ) (component instance $C1 $C) -(assert_trap (invoke "drop-r1-as-r2") "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") +(assert_trap (invoke "drop-r1-as-r2") "handle index 2 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") (component instance $C1 $C) -(assert_trap (invoke "return-r1-as-r2") "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") +(assert_trap (invoke "return-r1-as-r2") "handle index 2 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") From 5a5f67b34ddbe693a2bc2ec6352d93c324c955f3 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 09:41:36 +0100 Subject: [PATCH 44/63] Fix config --- crates/wasmtime/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index d8548100f5cf..6be6a5ab0c6b 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1217,7 +1217,7 @@ impl Config { /// https://github.com/WebAssembly/component-model/pull/557 #[cfg(feature = "component-model-async")] pub fn wasm_component_model_threading(&mut self, enable: bool) -> &mut Self { - self.wasm_feature(WasmFeatures::CM_THREADING, enable); + self.wasm_features(WasmFeatures::CM_THREADING, enable); self } From 249f76be232ed1927e358796051c2333dbeecc2f Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 12:30:36 +0100 Subject: [PATCH 45/63] Tighten up completion --- crates/wasmtime/src/runtime/component/concurrent.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 54c4bbdf19ea..a746a3c0c37c 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -4169,16 +4169,21 @@ impl GuestTask { fn ready_to_delete(&self) -> bool { let threads_completed = self.threads.is_empty(); let has_sync_result = matches!(self.sync_result, SyncResult::Produced(_)); - let has_event = self.common.event.is_some(); + let pending_completion_event = matches!( + self.common.event, + Some(Event::Subtask { + status: Status::Returned | Status::ReturnCancelled + }) + ); let ready = threads_completed && !has_sync_result - && !has_event + && !pending_completion_event && !matches!(self.host_future_state, HostFutureState::Live); log::trace!( - "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, has_event: {}, host_future_state: {:?})", + "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, pending_completion_event: {}, host_future_state: {:?})", threads_completed, has_sync_result, - has_event, + pending_completion_event, self.host_future_state ); ready From a3ed6d7e8ed057a8eddd06a96316e948b7f6666a Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 12:46:35 +0100 Subject: [PATCH 46/63] Clippy --- crates/wasmtime/src/runtime/vm/component.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index d88364a74a72..d428c719aea7 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -725,6 +725,7 @@ impl ComponentInstance { let i = RuntimeTableIndex::from_u32(i); let offset = self.offsets.runtime_table(i); // SAFETY: see above + #[allow(clippy::cast_possible_truncation, reason = "known to not overflow")] unsafe { *self.as_mut().vmctx_plus_offset_mut::( offset + offset_of!(VMTableImport, from) as u32, From 6de172f1fa39e3ba05b32fc27e479badf3cc257e Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 13:03:53 +0100 Subject: [PATCH 47/63] Trigger full PR test From ea94efbc07e298a02f24a0217a07da74f1a7e436 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 14:16:08 +0100 Subject: [PATCH 48/63] Move funcref table reading --- .../src/runtime/component/concurrent.rs | 58 ++++--------------- crates/wasmtime/src/runtime/vm/component.rs | 34 +++++++++++ 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index a746a3c0c37c..e0fd46b1dc94 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -2906,36 +2906,6 @@ impl Instance { ) } - 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.index` intrinsic. pub(crate) fn thread_index(&self, store: &mut dyn VMStore) -> Result { let thread_id = store @@ -2966,10 +2936,10 @@ impl Instance { log::trace!("creating new thread"); let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); - 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() { + let instance = self.id().get_mut(store); + let table = instance.runtime_table(start_func_table_idx); + let func = instance.index_runtime_func_table(&table, start_func_idx as u64)?; + if func.type_index(store) != start_func_ty.type_index() { bail!( "start function does not match expected type (currently only `(i32) -> ()` is supported)" ); @@ -3006,12 +2976,11 @@ impl Instance { high_priority: bool, ) -> Result<()> { log::trace!("starting thread {thread:?}"); - let callee = unsafe { - self.read_funcref_from_table(store.0, start_func_table_idx, start_func_idx as u64)? - }; + let instance = self.id().get_mut(store.0); + let table = instance.runtime_table(start_func_table_idx); + let callee = instance.index_runtime_func_table(&table, start_func_idx as u64)?; let token = StoreToken::new(store.as_context_mut()); - let callee = SendSyncPtr::new(callee); let start_func = Box::new(move |store: &mut dyn VMStore| -> Result<()> { let old_thread = store.concurrent_state_mut().guest_thread.replace(thread); log::trace!("thread start: replaced {old_thread:?} with {thread:?} as current thread"); @@ -3019,15 +2988,10 @@ impl Instance { store.maybe_push_call_context(thread.task)?; let mut store = token.as_context_mut(store); - - let params = [ValRaw::i32(context)]; - unsafe { - crate::Func::call_unchecked_raw( - &mut store, - callee.as_non_null(), - params.as_slice().into(), - )? - } + let mut params = [ValRaw::i32(context)]; + // Use call_unchecked rather than call or call_async, as we don't want to run the function + // on a separate fiber if we're running in an async store. + unsafe { callee.call_unchecked(store.as_context_mut(), &mut params)? }; store.0.maybe_pop_call_context(thread.task)?; diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index d428c719aea7..e125183cc9c6 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -16,6 +16,7 @@ use crate::runtime::vm::{ ValRaw, VmPtr, VmSafe, catch_unwind_and_record_trap, }; use crate::store::InstanceId; +use crate::{vm, Func}; use alloc::alloc::Layout; use alloc::sync::Arc; use core::mem; @@ -23,6 +24,7 @@ use core::mem::offset_of; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::*; +use anyhow::{Result, anyhow}; use wasmtime_environ::{HostPtr, PrimaryMap, VMSharedTypeIndex}; #[allow( @@ -357,6 +359,38 @@ impl ComponentInstance { } } + /// Returns the `Func` at index `func_idx` in the funcref table `table`. + pub fn index_runtime_func_table( + &self, + table: &VMTableImport, + func_idx: u64, + ) -> Result { + unsafe { + let store = self.store.0.as_ref(); + let vmctx = table.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 = + 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 = Pin::new_unchecked(instance_ptr.as_mut()); + let table = instance.get_defined_table_with_lazy_init(table.index, [func_idx]); + let funcref = 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`. + 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}")), + }?; + + Ok(Func::from_vm_func_ref(store.id(), funcref)) + } + } + /// Returns the realloc pointer corresponding to the index provided. /// /// This can only be called after `idx` has been initialized at runtime From 72292d8beacd6de1815f1d97c542e3f73821aed9 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 14:18:55 +0100 Subject: [PATCH 49/63] Remove unused import --- crates/wasmtime/src/runtime/component/concurrent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index e0fd46b1dc94..7c16a6d8c561 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -57,7 +57,7 @@ use crate::store::{Store, StoreId, StoreInner, StoreOpaque, StoreToken}; use crate::vm::component::{ CallContext, ComponentInstance, InstanceFlags, ResourceTables, TransmitLocalState, }; -use crate::vm::{self, AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; +use crate::vm::{AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; use crate::{AsContext, AsContextMut, FuncType, StoreContext, StoreContextMut, ValRaw, ValType}; use anyhow::{Context as _, Result, anyhow, bail}; use error_contexts::GlobalErrorContextRefCount; From 866cde6b2e029109b1f95e55d22b13d9ca0a26d0 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 21 Oct 2025 14:20:42 +0100 Subject: [PATCH 50/63] Formatting --- crates/wasmtime/src/runtime/vm/component.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index e125183cc9c6..d0d479880a76 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -16,15 +16,15 @@ use crate::runtime::vm::{ ValRaw, VmPtr, VmSafe, catch_unwind_and_record_trap, }; use crate::store::InstanceId; -use crate::{vm, Func}; +use crate::{Func, vm}; use alloc::alloc::Layout; use alloc::sync::Arc; +use anyhow::{Result, anyhow}; use core::mem; use core::mem::offset_of; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::*; -use anyhow::{Result, anyhow}; use wasmtime_environ::{HostPtr, PrimaryMap, VMSharedTypeIndex}; #[allow( @@ -360,21 +360,16 @@ impl ComponentInstance { } /// Returns the `Func` at index `func_idx` in the funcref table `table`. - pub fn index_runtime_func_table( - &self, - table: &VMTableImport, - func_idx: u64, - ) -> Result { + pub fn index_runtime_func_table(&self, table: &VMTableImport, func_idx: u64) -> Result { unsafe { let store = self.store.0.as_ref(); let vmctx = table.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 = - vmctx - .byte_sub(mem::size_of::()) - .cast::(); + let mut instance_ptr = 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 = Pin::new_unchecked(instance_ptr.as_mut()); @@ -386,7 +381,7 @@ impl ComponentInstance { Ok(None) => Err(anyhow!("function index {func_idx} out of bounds")), Err(e) => Err(anyhow!("failed to get function from table: {e}")), }?; - + Ok(Func::from_vm_func_ref(store.id(), funcref)) } } From 9eb87272f225b5ed30be9a4aac1201a353349f02 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 10:32:37 +0100 Subject: [PATCH 51/63] Rename RemoveOnDrop --- crates/wasmtime/src/runtime/component/func/typed.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 70f6c7c2a089..8970c0c3d4c7 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -205,7 +205,7 @@ where // through a `SendSyncPtr` to thwart the `'static` check // of rustc and the signature of `prepare_call`. // - // Note the use of `RemoveOnDrop` in the code that follows + // Note the use of `SignalOnDrop` in the code that follows // this closure, which ensures that the task will be removed // from the concurrent state to which it belongs when the // containing `Future` is dropped, so long as the parameters @@ -216,12 +216,12 @@ where Self::lower_args(cx, ty, dst, params) })?; - struct RemoveOnDrop<'a, T: 'static> { + struct SignalOnDrop<'a, T: 'static> { store: StoreContextMut<'a, T>, task: TaskId, } - impl<'a, T> Drop for RemoveOnDrop<'a, T> { + impl<'a, T> Drop for SignalOnDrop<'a, T> { fn drop(&mut self) { self.task .host_future_dropped(self.store.as_context_mut()) @@ -229,7 +229,7 @@ where } } - let mut wrapper = RemoveOnDrop { + let mut wrapper = SignalOnDrop { store, task: prepared.task_id(), }; From 6eb5f6510214ddd9d5283badf6abf5eb320fbf6e Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 10:33:23 +0100 Subject: [PATCH 52/63] Review feedback --- crates/wasmtime/src/runtime/component/concurrent.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 7c16a6d8c561..f7e49477c768 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1452,9 +1452,8 @@ impl StoreOpaque { }; let old_guest_thread = if let Some(task) = task { - let old_thread = self.concurrent_state_mut().guest_thread; self.maybe_pop_call_context(task)?; - old_thread + self.concurrent_state_mut().guest_thread } else { None }; @@ -1890,7 +1889,7 @@ impl Instance { .guest_thread .replace(guest_thread); log::trace!( - "stackful call: replaced {old_thread:?} with {guest_thread:?} as current thread", + "sync/async-stackful call: replaced {old_thread:?} with {guest_thread:?} as current thread", ); let mut flags = self.id().get(store).instance_flags(callee_instance); From 44ea1c886025a62ce8a301ecdd12360f99ed1417 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 10:37:32 +0100 Subject: [PATCH 53/63] Review feedback --- crates/wasmtime/src/runtime/component/concurrent.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index f7e49477c768..2de9f9d07a05 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3085,7 +3085,7 @@ impl Instance { to_thread: Option, ) -> Result { // There could be a pending cancellation from a previous uncancellable wait - if cancellable && store.0.concurrent_state_mut().pending_cancellation() { + if cancellable && store.0.concurrent_state_mut().take_pending_cancellation() { return Ok(true); } @@ -3112,7 +3112,7 @@ impl Instance { store.0.suspend(reason)?; - Ok(cancellable && store.0.concurrent_state_mut().pending_cancellation()) + Ok(cancellable && store.0.concurrent_state_mut().take_pending_cancellation()) } /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. @@ -4209,9 +4209,7 @@ impl GuestTask { } } - for thread in self.threads { - state.delete(thread)?; - } + assert!(self.threads.is_empty()); state.delete(self.sync_call_set)?; @@ -4838,7 +4836,7 @@ impl ConcurrentState { /// Returns whether there's a pending cancellation on the current guest thread, /// consuming the event if so. - fn pending_cancellation(&mut self) -> bool { + fn take_pending_cancellation(&mut self) -> bool { let thread = self.guest_thread.unwrap(); if let Some(event) = self.get_mut(thread.task).unwrap().event.take() { assert!(matches!(event, Event::Cancelled)); From 7de1cbc82cf3c6c18453a10772de83dab02ff249 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 11:19:25 +0100 Subject: [PATCH 54/63] Move start thread closure --- .../src/runtime/component/concurrent.rs | 245 +++++++++--------- .../src/runtime/vm/component/libcalls.rs | 73 +++--- 2 files changed, 172 insertions(+), 146 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 2de9f9d07a05..0e5d598409aa 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -694,6 +694,13 @@ impl fmt::Debug for WorkItem { } } +/// Whether a suspension intrinsic was cancelled or completed +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) enum WaitResult { + Cancelled, + Completed, +} + /// Poll the specified future until it completes on behalf of a guest->host call /// using a sync-lowered import. /// @@ -2921,109 +2928,88 @@ impl Instance { } /// Implements the `thread.new_indirect` intrinsic. - pub(crate) fn thread_new_indirect( + pub(crate) fn thread_new_indirect( self, - store: &mut dyn VMStore, - caller: RuntimeComponentInstanceIndex, + mut store: StoreContextMut, + runtime_instance: RuntimeComponentInstanceIndex, _func_ty_idx: TypeFuncIndex, // currently unused start_func_table_idx: RuntimeTableIndex, start_func_idx: u32, context: i32, ) -> Result { - self.id().get(store).check_may_leave(caller)?; + self.id().get(store.0).check_may_leave(runtime_instance)?; log::trace!("creating new thread"); let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); - let instance = self.id().get_mut(store); + let instance = self.id().get_mut(store.0); let table = instance.runtime_table(start_func_table_idx); - let func = instance.index_runtime_func_table(&table, start_func_idx as u64)?; - if func.type_index(store) != start_func_ty.type_index() { + let callee = instance.index_runtime_func_table(&table, start_func_idx as u64)?; + if callee.type_index(store.0) != start_func_ty.type_index() { bail!( "start function does not match expected type (currently only `(i32) -> ()` is supported)" ); } - let state = store.concurrent_state_mut(); + let token = StoreToken::new(store.as_context_mut()); + let start_func = Box::new( + move |store: &mut dyn VMStore, guest_thread: QualifiedThreadId| -> Result<()> { + let old_thread = store + .concurrent_state_mut() + .guest_thread + .replace(guest_thread); + log::trace!( + "thread start: replaced {old_thread:?} with {guest_thread:?} as current thread" + ); + + store.maybe_push_call_context(guest_thread.task)?; + + let mut store = token.as_context_mut(store); + let mut params = [ValRaw::i32(context)]; + // Use call_unchecked rather than call or call_async, as we don't want to run the function + // on a separate fiber if we're running in an async store. + unsafe { callee.call_unchecked(store.as_context_mut(), &mut params)? }; + + store.0.maybe_pop_call_context(guest_thread.task)?; + + self.cleanup_thread(store.0, guest_thread, runtime_instance)?; + log::trace!("explicit thread {guest_thread:?} completed"); + let state = store.0.concurrent_state_mut(); + let task = state.get_mut(guest_thread.task)?; + if task.threads.is_empty() && !task.returned_or_cancelled() { + bail!(Trap::NoAsyncResult); + } + state.guest_thread = old_thread; + old_thread + .map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); + if state.get_mut(guest_thread.task)?.ready_to_delete() { + Waitable::Guest(guest_thread.task).delete_from(state)?; + } + log::trace!("thread start: restored {old_thread:?} as current thread"); + + Ok(()) + }, + ); + + let state = store.0.concurrent_state_mut(); let current_thread = state.guest_thread.unwrap(); let parent_task = current_thread.task; - let new_thread = - GuestThread::new_explicit(parent_task, start_func_table_idx, start_func_idx, context); + let new_thread = GuestThread::new_explicit(parent_task, start_func); let thread_id = state.push(new_thread)?; state.get_mut(parent_task)?.threads.insert(thread_id); log::trace!("new thread with id {thread_id:?} created"); - let guest_id = self.id().get_mut(store).guest_tables().0[caller] + let guest_id = self.id().get_mut(store.0).guest_tables().0[runtime_instance] .guest_thread_insert(thread_id.rep())?; store + .0 .concurrent_state_mut() .get_mut(thread_id)? .instance_rep = Some(guest_id); - Ok(guest_id) - } - - fn start_thread( - self, - store: &mut StoreContextMut, - thread: QualifiedThreadId, - runtime_instance: RuntimeComponentInstanceIndex, - start_func_table_idx: RuntimeTableIndex, - start_func_idx: u32, - context: i32, - high_priority: bool, - ) -> Result<()> { - log::trace!("starting thread {thread:?}"); - let instance = self.id().get_mut(store.0); - let table = instance.runtime_table(start_func_table_idx); - let callee = instance.index_runtime_func_table(&table, start_func_idx as u64)?; - - let token = StoreToken::new(store.as_context_mut()); - let start_func = Box::new(move |store: &mut dyn VMStore| -> Result<()> { - let old_thread = store.concurrent_state_mut().guest_thread.replace(thread); - log::trace!("thread start: replaced {old_thread:?} with {thread:?} as current thread"); - - store.maybe_push_call_context(thread.task)?; - - let mut store = token.as_context_mut(store); - let mut params = [ValRaw::i32(context)]; - // Use call_unchecked rather than call or call_async, as we don't want to run the function - // on a separate fiber if we're running in an async store. - unsafe { callee.call_unchecked(store.as_context_mut(), &mut params)? }; - - store.0.maybe_pop_call_context(thread.task)?; - - self.cleanup_thread(store.0, thread, runtime_instance)?; - log::trace!("explicit thread {thread:?} completed"); - let state = store.0.concurrent_state_mut(); - let task = state.get_mut(thread.task)?; - if task.threads.is_empty() && !task.returned_or_cancelled() { - bail!(Trap::NoAsyncResult); - } - state.guest_thread = old_thread; - old_thread.map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); - if state.get_mut(thread.task)?.ready_to_delete() { - Waitable::Guest(thread.task).delete_from(state)?; - } - log::trace!("thread start: restored {old_thread:?} as current thread"); - - Ok(()) - }); - let guest_call = WorkItem::GuestCall(GuestCall { - thread, - kind: GuestCallKind::StartExplicit(start_func), - }); - if high_priority { - store - .0 - .concurrent_state_mut() - .push_high_priority(guest_call); - } else { - store.0.concurrent_state_mut().push_low_priority(guest_call); - } - Ok(()) + Ok(guest_id) } pub(crate) fn resume_suspended_thread( @@ -3038,34 +3024,25 @@ impl Instance { let thread = state.get_mut(guest_thread.thread)?; match mem::replace(&mut thread.state, GuestThreadState::Running) { - GuestThreadState::NotStartedExplicit { - start_func_table_idx, - start_func_idx, - context, - } => { - self.start_thread( - &mut store, - guest_thread, - runtime_instance, - start_func_table_idx, - start_func_idx, - context, - high_priority, - )?; + GuestThreadState::NotStartedExplicit(start_func) => { + log::trace!("starting thread {guest_thread:?}"); + let guest_call = WorkItem::GuestCall(GuestCall { + thread: guest_thread, + kind: GuestCallKind::StartExplicit(Box::new(move |store| { + start_func(store, guest_thread) + })), + }); + store + .0 + .concurrent_state_mut() + .push_work_item(guest_call, high_priority); } GuestThreadState::Suspended(fiber) => { log::trace!("resuming thread {thread_id:?} that was suspended"); - if high_priority { - store - .0 - .concurrent_state_mut() - .push_high_priority(WorkItem::ResumeFiber(fiber)); - } else { - store - .0 - .concurrent_state_mut() - .push_low_priority(WorkItem::ResumeFiber(fiber)); - } + store + .0 + .concurrent_state_mut() + .push_work_item(WorkItem::ResumeFiber(fiber), high_priority); } _ => { bail!("cannot resume thread which is not suspended"); @@ -3083,10 +3060,10 @@ impl Instance { cancellable: bool, yielding: bool, to_thread: Option, - ) -> Result { + ) -> Result { // There could be a pending cancellation from a previous uncancellable wait if cancellable && store.0.concurrent_state_mut().take_pending_cancellation() { - return Ok(true); + return Ok(WaitResult::Cancelled); } let to_thread = to_thread @@ -3112,7 +3089,11 @@ impl Instance { store.0.suspend(reason)?; - Ok(cancellable && store.0.concurrent_state_mut().take_pending_cancellation()) + if cancellable && store.0.concurrent_state_mut().take_pending_cancellation() { + Ok(WaitResult::Cancelled) + } else { + Ok(WaitResult::Completed) + } } /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. @@ -3516,6 +3497,17 @@ pub trait VMComponentAsyncStore { debug_msg_address: u32, ) -> Result<()>; + /// The `thread.new_indirect` intrinsic + fn thread_new_indirect( + &mut self, + instance: Instance, + caller: RuntimeComponentInstanceIndex, + func_ty_idx: TypeFuncIndex, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Result; + /// The `thread.yield`, `thread.yield-to`, `thread.suspend`, and `thread.switch-to` intrinsics. fn suspension_intrinsic( &mut self, @@ -3524,7 +3516,7 @@ pub trait VMComponentAsyncStore { cancellable: bool, yielding: bool, to_thread: Option, - ) -> Result; + ) -> Result; /// The `thread.resume-later` intrinsic. fn thread_resume_later( @@ -3829,6 +3821,25 @@ impl VMComponentAsyncStore for StoreInner { ) } + fn thread_new_indirect( + &mut self, + instance: Instance, + caller: RuntimeComponentInstanceIndex, + func_ty_idx: TypeFuncIndex, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Result { + instance.thread_new_indirect( + StoreContextMut(self), + caller, + func_ty_idx, + start_func_table_idx, + start_func_idx, + context, + ) + } + fn suspension_intrinsic( &mut self, instance: Instance, @@ -3836,7 +3847,7 @@ impl VMComponentAsyncStore for StoreInner { cancellable: bool, yielding: bool, to_thread: Option, - ) -> Result { + ) -> Result { instance.suspension_intrinsic( StoreContextMut(self), caller, @@ -3966,11 +3977,9 @@ impl fmt::Debug for QualifiedThreadId { enum GuestThreadState { NotStartedImplicit, - NotStartedExplicit { - start_func_table_idx: RuntimeTableIndex, - start_func_idx: u32, - context: i32, - }, + NotStartedExplicit( + Box Result<()> + Send + Sync>, + ), Running, Suspended(StoreFiber<'static>), Pending, @@ -4016,19 +4025,15 @@ impl GuestThread { fn new_explicit( parent_task: TableId, - start_func_table_idx: RuntimeTableIndex, - start_func_idx: u32, - context: i32, + start_func: Box< + dyn FnOnce(&mut dyn VMStore, QualifiedThreadId) -> Result<()> + Send + Sync, + >, ) -> Self { Self { context: [0; 2], parent_task, wake_on_cancel: None, - state: GuestThreadState::NotStartedExplicit { - start_func_table_idx, - start_func_idx, - context, - }, + state: GuestThreadState::NotStartedExplicit(start_func), instance_rep: None, } } @@ -4730,6 +4735,14 @@ impl ConcurrentState { self.low_priority.push(item); } + fn push_work_item(&mut self, item: WorkItem, high_priority: bool) { + if high_priority { + self.push_high_priority(item); + } else { + self.push_low_priority(item); + } + } + /// Determine whether the instance associated with the specified guest task /// may be entered (i.e. is not already on the async call stack). /// diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 3687d0601c10..268e4250f1d1 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::WaitResult; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; @@ -811,13 +812,16 @@ fn thread_yield( caller_instance: u32, cancellable: u8, ) -> Result { - store.component_async_store().suspension_intrinsic( - instance, - RuntimeComponentInstanceIndex::from_u32(caller_instance), - cancellable != 0, - true, - None, - ) + store + .component_async_store() + .suspension_intrinsic( + instance, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + cancellable != 0, + true, + None, + ) + .map(|r| r == WaitResult::Cancelled) } #[cfg(feature = "component-model-async")] @@ -1385,8 +1389,8 @@ fn thread_new_indirect( func_idx: u32, context: u32, ) -> Result { - instance.thread_new_indirect( - store, + store.component_async_store().thread_new_indirect( + instance, RuntimeComponentInstanceIndex::from_u32(caller), TypeFuncIndex::from_u32(func_ty_id), RuntimeTableIndex::from_u32(func_table_idx), @@ -1403,13 +1407,16 @@ fn thread_switch_to( cancellable: u8, thread_idx: u32, ) -> Result { - store.component_async_store().suspension_intrinsic( - instance, - RuntimeComponentInstanceIndex::from_u32(caller), - cancellable != 0, - false, - Some(thread_idx), - ) + store + .component_async_store() + .suspension_intrinsic( + instance, + RuntimeComponentInstanceIndex::from_u32(caller), + cancellable != 0, + false, + Some(thread_idx), + ) + .map(|r| r == WaitResult::Cancelled) } #[cfg(feature = "component-model-async")] @@ -1419,13 +1426,16 @@ fn thread_suspend( caller: u32, cancellable: u8, ) -> Result { - store.component_async_store().suspension_intrinsic( - instance, - RuntimeComponentInstanceIndex::from_u32(caller), - cancellable != 0, - false, - None, - ) + store + .component_async_store() + .suspension_intrinsic( + instance, + RuntimeComponentInstanceIndex::from_u32(caller), + cancellable != 0, + false, + None, + ) + .map(|r| r == WaitResult::Cancelled) } #[cfg(feature = "component-model-async")] @@ -1450,11 +1460,14 @@ fn thread_yield_to( cancellable: u8, thread_idx: u32, ) -> Result { - store.component_async_store().suspension_intrinsic( - instance, - RuntimeComponentInstanceIndex::from_u32(caller_instance), - cancellable != 0, - true, - Some(thread_idx), - ) + store + .component_async_store() + .suspension_intrinsic( + instance, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + cancellable != 0, + true, + Some(thread_idx), + ) + .map(|r| r == WaitResult::Cancelled) } From 25a711fba6449d4d5ccb316be060eb370e97aa35 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 11:35:40 +0100 Subject: [PATCH 55/63] Review feedback --- .../wasmtime/src/runtime/component/concurrent.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 0e5d598409aa..0ed1ce43a3cd 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -3238,19 +3238,13 @@ impl Instance { // Not yet started; cancel and remove from pending let callee_instance = task.instance; - if !concurrent_state - .instance_state(callee_instance) - .pending - .iter() - .any(|(thread, _)| thread.task == guest_task) - { + let pending = &mut concurrent_state.instance_state(callee_instance).pending; + let pending_count = pending.len(); + pending.retain(|thread, _| thread.task != guest_task); + // If there were no pending threads for this task, we're in an error state + if pending.len() == pending_count { 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.returned_or_cancelled() { // Started, but not yet returned or cancelled; send the From c576d1c26c7d1e6a591c4ab34f1f487e92cdd045 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 12:16:59 +0100 Subject: [PATCH 56/63] Review feedback --- .../src/runtime/component/concurrent.rs | 126 ++++++------------ .../src/runtime/vm/component/libcalls.rs | 25 ++-- 2 files changed, 51 insertions(+), 100 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 0ed1ce43a3cd..69f7ce664581 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1833,14 +1833,11 @@ impl Instance { assert!(async_); Box::new(move |store: &mut dyn VMStore| { - let guest_id = self.id().get_mut(store).guest_tables().0[callee_instance] - .guest_thread_insert(guest_thread.thread.rep())?; - - store - .concurrent_state_mut() - .get_mut(guest_thread.thread)? - .instance_rep = Some(guest_id); - + self.add_guest_thread_to_instance_table( + guest_thread.thread, + store, + callee_instance, + )?; let old_thread = store .concurrent_state_mut() .guest_thread @@ -1884,13 +1881,11 @@ impl Instance { } else { let token = StoreToken::new(store.as_context_mut()); Box::new(move |store: &mut dyn VMStore| { - let guest_id = self.id().get_mut(store).guest_tables().0[callee_instance] - .guest_thread_insert(guest_thread.thread.rep())?; - - store - .concurrent_state_mut() - .get_mut(guest_thread.thread)? - .instance_rep = Some(guest_id); + self.add_guest_thread_to_instance_table( + guest_thread.thread, + store, + callee_instance, + )?; let old_thread = store .concurrent_state_mut() .guest_thread @@ -3001,25 +2996,19 @@ impl Instance { log::trace!("new thread with id {thread_id:?} created"); - let guest_id = self.id().get_mut(store.0).guest_tables().0[runtime_instance] - .guest_thread_insert(thread_id.rep())?; - store - .0 - .concurrent_state_mut() - .get_mut(thread_id)? - .instance_rep = Some(guest_id); - - Ok(guest_id) + self.add_guest_thread_to_instance_table(thread_id, store.0, runtime_instance) } - pub(crate) fn resume_suspended_thread( + pub(crate) fn resume_suspended_thread( self, - mut store: StoreContextMut, + store: &mut StoreOpaque, runtime_instance: RuntimeComponentInstanceIndex, - thread_id: TableId, + thread_idx: u32, high_priority: bool, ) -> Result<()> { - let state = store.0.concurrent_state_mut(); + let thread_id = + GuestThread::from_instance(self.id().get_mut(store), runtime_instance, thread_idx)?; + let state = store.concurrent_state_mut(); let guest_thread = QualifiedThreadId::qualify(state, thread_id)?; let thread = state.get_mut(guest_thread.thread)?; @@ -3033,14 +3022,12 @@ impl Instance { })), }); store - .0 .concurrent_state_mut() .push_work_item(guest_call, high_priority); } GuestThreadState::Suspended(fiber) => { log::trace!("resuming thread {thread_id:?} that was suspended"); store - .0 .concurrent_state_mut() .push_work_item(WorkItem::ResumeFiber(fiber), high_priority); } @@ -3051,31 +3038,43 @@ impl Instance { Ok(()) } + fn add_guest_thread_to_instance_table( + self, + thread_id: TableId, + store: &mut StoreOpaque, + runtime_instance: RuntimeComponentInstanceIndex, + ) -> Result { + let guest_id = self.id().get_mut(store).guest_tables().0[runtime_instance] + .guest_thread_insert(thread_id.rep())?; + store + .concurrent_state_mut() + .get_mut(thread_id)? + .instance_rep = Some(guest_id); + Ok(guest_id) + } + /// Helper function for the `thread.yield`, `thread.yield-to`, `thread.suspend`, /// and `thread.switch-to` intrinsics. - pub(crate) fn suspension_intrinsic( + pub(crate) fn suspension_intrinsic( self, - mut store: StoreContextMut, + store: &mut StoreOpaque, caller: RuntimeComponentInstanceIndex, cancellable: bool, yielding: bool, to_thread: Option, ) -> Result { // There could be a pending cancellation from a previous uncancellable wait - if cancellable && store.0.concurrent_state_mut().take_pending_cancellation() { + if cancellable && store.concurrent_state_mut().take_pending_cancellation() { return Ok(WaitResult::Cancelled); } - let to_thread = to_thread - .and_then(|id| GuestThread::from_instance(self.id().get_mut(store.0), caller, id).ok()); - - self.id().get(store.0).check_may_leave(caller)?; + self.id().get(store).check_may_leave(caller)?; if let Some(thread) = to_thread { - self.resume_suspended_thread(store.as_context_mut(), caller, thread, true)?; + self.resume_suspended_thread(store, caller, thread, true)?; } - let state = store.0.concurrent_state_mut(); + let state = store.concurrent_state_mut(); let guest_thread = state.guest_thread.unwrap(); let reason = if yielding { SuspendReason::Yielding { @@ -3087,9 +3086,9 @@ impl Instance { } }; - store.0.suspend(reason)?; + store.suspend(reason)?; - if cancellable && store.0.concurrent_state_mut().take_pending_cancellation() { + if cancellable && store.concurrent_state_mut().take_pending_cancellation() { Ok(WaitResult::Cancelled) } else { Ok(WaitResult::Completed) @@ -3501,24 +3500,6 @@ pub trait VMComponentAsyncStore { start_func_idx: u32, context: i32, ) -> Result; - - /// The `thread.yield`, `thread.yield-to`, `thread.suspend`, and `thread.switch-to` intrinsics. - fn suspension_intrinsic( - &mut self, - instance: Instance, - caller: RuntimeComponentInstanceIndex, - cancellable: bool, - yielding: bool, - to_thread: Option, - ) -> Result; - - /// The `thread.resume-later` intrinsic. - fn thread_resume_later( - &mut self, - instance: Instance, - caller: RuntimeComponentInstanceIndex, - thread: u32, - ) -> Result<()>; } /// SAFETY: See trait docs. @@ -3833,33 +3814,6 @@ impl VMComponentAsyncStore for StoreInner { context, ) } - - fn suspension_intrinsic( - &mut self, - instance: Instance, - caller: RuntimeComponentInstanceIndex, - cancellable: bool, - yielding: bool, - to_thread: Option, - ) -> Result { - instance.suspension_intrinsic( - StoreContextMut(self), - caller, - cancellable, - yielding, - to_thread, - ) - } - - fn thread_resume_later( - &mut self, - instance: Instance, - caller: RuntimeComponentInstanceIndex, - thread: u32, - ) -> Result<()> { - let thread = GuestThread::from_instance(instance.id().get_mut(self), caller, thread)?; - instance.resume_suspended_thread(StoreContextMut(self), caller, thread, false) - } } type HostTaskFuture = Pin> + Send + 'static>>; diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 268e4250f1d1..6a9d81756d6c 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -812,10 +812,9 @@ fn thread_yield( caller_instance: u32, cancellable: u8, ) -> Result { - store - .component_async_store() + instance .suspension_intrinsic( - instance, + store, RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, true, @@ -1407,10 +1406,9 @@ fn thread_switch_to( cancellable: u8, thread_idx: u32, ) -> Result { - store - .component_async_store() + instance .suspension_intrinsic( - instance, + store, RuntimeComponentInstanceIndex::from_u32(caller), cancellable != 0, false, @@ -1426,10 +1424,9 @@ fn thread_suspend( caller: u32, cancellable: u8, ) -> Result { - store - .component_async_store() + instance .suspension_intrinsic( - instance, + store, RuntimeComponentInstanceIndex::from_u32(caller), cancellable != 0, false, @@ -1445,10 +1442,11 @@ fn thread_resume_later( caller_instance: u32, thread_idx: u32, ) -> Result<()> { - store.component_async_store().thread_resume_later( - instance, + instance.resume_suspended_thread( + store, RuntimeComponentInstanceIndex::from_u32(caller_instance), thread_idx, + false, ) } @@ -1460,10 +1458,9 @@ fn thread_yield_to( cancellable: u8, thread_idx: u32, ) -> Result { - store - .component_async_store() + instance .suspension_intrinsic( - instance, + store, RuntimeComponentInstanceIndex::from_u32(caller_instance), cancellable != 0, true, From 00615018608e3c16a72fa94ff5fc139c0a9bf5e8 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Wed, 22 Oct 2025 12:19:51 +0100 Subject: [PATCH 57/63] Correct feature for import --- crates/wasmtime/src/runtime/vm/component/libcalls.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 6a9d81756d6c..5df911dfb6f8 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; +#[cfg(feature = "component-model-async")] use crate::component::concurrent::WaitResult; use crate::prelude::*; #[cfg(feature = "component-model-async")] From 773baad23a3b83a8307aca3a90179f3f862fefdf Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 23 Oct 2025 09:53:11 +0100 Subject: [PATCH 58/63] Review feedback --- .../src/runtime/component/concurrent.rs | 8 +++-- crates/wasmtime/src/runtime/vm/component.rs | 33 +++++++++---------- crates/wasmtime/src/runtime/vm/table.rs | 3 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 69f7ce664581..efbe5c5969e9 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -2938,8 +2938,11 @@ impl Instance { let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); let instance = self.id().get_mut(store.0); - let table = instance.runtime_table(start_func_table_idx); - let callee = instance.index_runtime_func_table(&table, start_func_idx as u64)?; + let callee = instance + .index_runtime_func_table(start_func_table_idx, start_func_idx as u64)? + .ok_or_else(|| { + anyhow!("the start function index points to an uninitialized function") + })?; if callee.type_index(store.0) != start_func_ty.type_index() { bail!( "start function does not match expected type (currently only `(i32) -> ()` is supported)" @@ -4337,6 +4340,7 @@ impl Waitable { if let Some(set) = self.common(state)?.set { state.get_mut(set)?.ready.remove(self); } + Ok(event) } diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index d0d479880a76..70a8eee279bd 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -19,7 +19,7 @@ use crate::store::InstanceId; use crate::{Func, vm}; use alloc::alloc::Layout; use alloc::sync::Arc; -use anyhow::{Result, anyhow}; +use anyhow::Result; use core::mem; use core::mem::offset_of; use core::pin::Pin; @@ -359,30 +359,27 @@ impl ComponentInstance { } } - /// Returns the `Func` at index `func_idx` in the funcref table `table`. - pub fn index_runtime_func_table(&self, table: &VMTableImport, func_idx: u64) -> Result { + /// Returns the `Func` at index `func_idx` in the funcref table at `table_idx`. + pub fn index_runtime_func_table( + &self, + table_idx: RuntimeTableIndex, + func_idx: u64, + ) -> Result> { unsafe { let store = self.store.0.as_ref(); + let table = self.runtime_table(table_idx); let vmctx = table.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 = vmctx - .byte_sub(mem::size_of::()) - .cast::(); + // SAFETY: it's a contract of this function that `vmctx` is a valid + // allocation which can go backwards to a `ComponentInstance`. + let mut instance_ptr = vm::Instance::from_vmctx(vmctx); // 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 = Pin::new_unchecked(instance_ptr.as_mut()); let table = instance.get_defined_table_with_lazy_init(table.index, [func_idx]); - let funcref = 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`. - 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}")), - }?; - - Ok(Func::from_vm_func_ref(store.id(), funcref)) + let func = table + .get_func(func_idx)? + .map(|funcref| Func::from_vm_func_ref(store.id(), funcref)); + Ok(func) } } diff --git a/crates/wasmtime/src/runtime/vm/table.rs b/crates/wasmtime/src/runtime/vm/table.rs index 8b3f84f4692c..e94029f475dc 100644 --- a/crates/wasmtime/src/runtime/vm/table.rs +++ b/crates/wasmtime/src/runtime/vm/table.rs @@ -750,7 +750,8 @@ impl Table { /// Get reference to the specified element. /// - /// Returns `None` if the index is out of bounds. + /// Returns `Ok(None)` if the element is null or uninitialized. + /// Returns `Err` if the index is out of bounds. /// /// Panics if this is a table of GC references and `gc_store` is `None`. pub fn get_func(&self, index: u64) -> Result>, Trap> { From 1c76856f948ab79257b420a98ba7669cbc86c268 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 23 Oct 2025 09:59:50 +0100 Subject: [PATCH 59/63] Disable failing tests --- crates/test-util/src/wast.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index 429ad4f184cd..ad72d11de3d6 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -669,6 +669,16 @@ impl WastTest { let failing_component_model_tests = [ // FIXME(#11683) "component-model/test/values/trap-in-post-return.wast", + // Awaiting https://github.com/WebAssembly/component-model/pull/570 + "component-model/test/resources/multiple-resources.wast", + "component-model/test/async/empty-wait.wast", + "component-model/test/async/drop-stream.wast", + "component-model/test/async/passing-resources.wast", + "component-model/test/async/async-calls-sync.wast", + "component-model/test/async/partial-stream-copies.wast", + "component-model/test/async/futures-must-write.wast", + "component-model/test/async/cancel-stream.wast", + "component-model/test/async/drop-waitable-set.wast", ]; if failing_component_model_tests .iter() From 4c4aa63da6a9ac1fcab40bc2f859947aa04b2783 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 27 Oct 2025 10:08:35 +0000 Subject: [PATCH 60/63] Enable fixed tests --- crates/test-util/src/wast.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index ad72d11de3d6..429ad4f184cd 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -669,16 +669,6 @@ impl WastTest { let failing_component_model_tests = [ // FIXME(#11683) "component-model/test/values/trap-in-post-return.wast", - // Awaiting https://github.com/WebAssembly/component-model/pull/570 - "component-model/test/resources/multiple-resources.wast", - "component-model/test/async/empty-wait.wast", - "component-model/test/async/drop-stream.wast", - "component-model/test/async/passing-resources.wast", - "component-model/test/async/async-calls-sync.wast", - "component-model/test/async/partial-stream-copies.wast", - "component-model/test/async/futures-must-write.wast", - "component-model/test/async/cancel-stream.wast", - "component-model/test/async/drop-waitable-set.wast", ]; if failing_component_model_tests .iter() From 9f057926cad0f7f097f485efdcc41741b3190f88 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 27 Oct 2025 10:10:01 +0000 Subject: [PATCH 61/63] Review feedback --- crates/wasmtime/src/runtime/component/concurrent.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index efbe5c5969e9..4030459479e2 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1404,7 +1404,9 @@ impl StoreOpaque { let state = self.concurrent_state_mut(); state.guest_thread = old_thread; - old_thread.map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); + if let Some(ref ot) = old_thread { + state.get_mut(ot.thread)?.state = GuestThreadState::Running; + } log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { From 77b0789703a203f6f77644a93b5576b28eb2311d Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 27 Oct 2025 10:13:48 +0000 Subject: [PATCH 62/63] Readd tests --- crates/test-util/src/wast.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index 429ad4f184cd..ad72d11de3d6 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -669,6 +669,16 @@ impl WastTest { let failing_component_model_tests = [ // FIXME(#11683) "component-model/test/values/trap-in-post-return.wast", + // Awaiting https://github.com/WebAssembly/component-model/pull/570 + "component-model/test/resources/multiple-resources.wast", + "component-model/test/async/empty-wait.wast", + "component-model/test/async/drop-stream.wast", + "component-model/test/async/passing-resources.wast", + "component-model/test/async/async-calls-sync.wast", + "component-model/test/async/partial-stream-copies.wast", + "component-model/test/async/futures-must-write.wast", + "component-model/test/async/cancel-stream.wast", + "component-model/test/async/drop-waitable-set.wast", ]; if failing_component_model_tests .iter() From 1de6f968c6da838c5f621f7b79cb0daff818326c Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 27 Oct 2025 10:22:58 +0000 Subject: [PATCH 63/63] Ignore task deletion test with Miri --- tests/all/component_model/async.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index 80601194dade..ec7e6b606703 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -320,6 +320,7 @@ async fn drop_resource_async() -> Result<()> { /// 2. An explicit thread suspends indefinitely /// 3. An explicit thread yield loops indefinitely #[tokio::test] +#[cfg_attr(miri, ignore)] async fn task_deletion() -> Result<()> { let mut config = Config::new(); config.async_support(true);