diff --git a/rust/private/rust_wasm_component_bindgen.bzl b/rust/private/rust_wasm_component_bindgen.bzl index 8527a051..e3570c6c 100644 --- a/rust/private/rust_wasm_component_bindgen.bzl +++ b/rust/private/rust_wasm_component_bindgen.bzl @@ -477,6 +477,7 @@ def rust_wasm_component_bindgen( symmetric = False, invert_direction = False, bitflags_dep = "@crates//:bitflags", + async_interfaces = None, **kwargs): """Builds a Rust WebAssembly component with automatic WIT binding generation. @@ -507,12 +508,38 @@ def rust_wasm_component_bindgen( bitflags = "2" And configure crate_universe in MODULE.bazel. + async_interfaces: Optional wit-bindgen `--async` filters for the guest + (WASM) bindings, overriding the default. By default p3 lifts every + export async (`["all"]`) and p2 is fully sync (`[]`). Under the p3 + default a plain-sync WIT function still becomes an async-lift export, + which a call-return consumer cannot invoke (#526). Override to mix + sync and async exports. Each entry is a wit-bindgen `--async` value; + **filter names must include the package version**, e.g. + `pkg:iface/i@0.1.0#fn`: + + [] # no --async: each export + # follows its WIT signature + # (async-typed -> async, + # sync -> sync). Solves #526 + # when the WIT already + # distinguishes them. + ["-all"] # force every export sync + ["-export:pkg:iface/i@0.1.0#fn"] # `fn` sync, others WIT-default + ["pkg:iface/i@0.1.0#stream-fn"] # `stream-fn` async, others default + + Note: wit-bindgen (0.54) rejects combining the `all` blanket with a + per-export `-export:` exclude ("unused async option") — use the + WIT-default (`[]`) or allowlist forms above instead. Only affects the + guest bindings; native-guest bindings are always sync (the host + runtime has no async_support). **kwargs: Additional arguments passed to rust_wasm_component """ - # Determine P3 async settings from wasi_version + # Determine P3 async settings from wasi_version, unless the caller supplied + # an explicit override. Default: p3 lifts all exports async, p2 is sync. wasi_version = kwargs.get("wasi_version", "p2") - p3_async_interfaces = ["all"] if wasi_version == "p3" else [] + if async_interfaces == None: + async_interfaces = ["all"] if wasi_version == "p3" else [] # Generate WIT bindings based on symmetric flag if symmetric: @@ -541,7 +568,7 @@ def rust_wasm_component_bindgen( wit = wit, language = "rust", generation_mode = "guest", - async_interfaces = p3_async_interfaces, + async_interfaces = async_interfaces, visibility = ["//visibility:private"], ) diff --git a/test/p3/BUILD.bazel b/test/p3/BUILD.bazel index a890ef2d..6c101dd7 100644 --- a/test/p3/BUILD.bazel +++ b/test/p3/BUILD.bazel @@ -70,6 +70,27 @@ build_test( targets = [":hello_p3"], ) +# Per-function async override (#526): a plain-sync WIT export kept sync under p3 +# while the component is still p3. Without the override this fails to compile +# ("method should be `async` ... but it is synchronous"); the override forces a +# call-return lift so a sync consumer can invoke it. +rust_wasm_component_bindgen( + name = "hello_p3_sync", + srcs = ["src_p3_sync/lib.rs"], + # Force the `greet` export sync (call-return) even though the component is + # p3. The filter name requires the package version. Other exports (none + # here) would follow their WIT default. + async_interfaces = ["-export:hello:interfaces/greeting@0.1.0#greet"], + profiles = ["release"], + wasi_version = "p3", + wit = ":hello_interfaces", +) + +build_test( + name = "p3_sync_export_build_test", + targets = [":hello_p3_sync"], +) + # ============================================================================ # WASI 0.3 interface WIT (stable v0.3.0) — covers wasi_p3_deps.bzl # ============================================================================ @@ -102,6 +123,7 @@ test_suite( name = "p3", tests = [ ":p3_build_test", + ":p3_sync_export_build_test", ":wasi_p3_wit_build_test", ], ) @@ -111,6 +133,7 @@ test_suite( tests = [ ":p2_build_test", ":p3_build_test", + ":p3_sync_export_build_test", ":wasi_p3_wit_build_test", ], ) diff --git a/test/p3/src_p3_sync/lib.rs b/test/p3/src_p3_sync/lib.rs new file mode 100644 index 00000000..88db652c --- /dev/null +++ b/test/p3/src_p3_sync/lib.rs @@ -0,0 +1,19 @@ +// P3 component exposing a SYNC export via the async_interfaces override (#526). +// +// `greet` has a plain-sync WIT signature. Under the p3 default (--async all) it +// becomes an async-lift export, and this sync `fn greet` would fail to compile +// with "method should be `async` or return a future, but it is synchronous" — +// the exact friction reported in #526. The bindgen target forces it sync with +// async_interfaces = ["all", "-export:hello:interfaces/greeting#greet"] +// so a call-return consumer (e.g. a witness MC/DC harness) can invoke it. +use hello_p3_sync_bindings::exports::hello::interfaces::greeting::Guest; + +struct Component; + +impl Guest for Component { + fn greet(name: String) -> String { + format!("Hello, {}! (P3 sync export)", name) + } +} + +hello_p3_sync_bindings::export!(Component with_types_in hello_p3_sync_bindings); diff --git a/wit/private/wit_bindgen.bzl b/wit/private/wit_bindgen.bzl index eb7b25f5..36bd24cb 100644 --- a/wit/private/wit_bindgen.bzl +++ b/wit/private/wit_bindgen.bzl @@ -22,10 +22,15 @@ def _build_derive_args(additional_derives): return args def _build_async_args(async_interfaces): - """Build --async arguments for async interface configuration""" + """Build --async arguments for async interface configuration. + + Uses the `--async=VALUE` form rather than `--async VALUE` so that filter + values beginning with `-` (e.g. `-all`, `-export:pkg:iface/i#fn`, which force + a method sync) are not misparsed by clap as separate flags. + """ args = [] for async_interface in async_interfaces: - args.extend(["--async", async_interface]) + args.append("--async=" + async_interface) return args def _to_snake_case(name):