Skip to content

Commit d8cdb30

Browse files
avrabeclaude
andauthored
feat(bindgen): per-function async override for p3 (async_interfaces) (#526) (#527)
`rust_wasm_component_bindgen(wasi_version="p3")` hardcoded `--async all`, so every export — including plain-sync WIT functions — was lifted async. A call-return consumer (e.g. a witness MC/DC harness) cannot invoke an async-lift export, forcing a separate p2 sibling component just to expose one sync entry point (#526). Add an `async_interfaces` parameter exposing wit-bindgen's `--async` filters on the guest bindings. Default is unchanged (p3 -> ["all"], p2 -> []). Callers can now pass: [] # no --async: each export follows its WIT # signature (async-typed async, sync sync) ["-all"] # force every export sync ["-export:pkg:iface/i@0.1.0#fn"] # that export sync, others WIT-default ["pkg:iface/i@0.1.0#stream-fn"] # that export async, others WIT-default Also fix `_build_async_args` to emit `--async=VALUE` (not `--async VALUE`) so filter values beginning with `-` (`-all`, `-export:...`) aren't misparsed by clap as separate flags. Verified end-to-end: new //test/p3:hello_p3_sync builds a p3 component whose `greet` export is a plain sync `fn` (no async, no cfg dance) via async_interfaces=["-export:hello:interfaces/greeting@0.1.0#greet"]. Under the p3 default this fails to compile ("method should be `async` or return a future, but it is synchronous"); generated bindings drop from 3 async-lift exports to 0. Default p3 path (hello_p3, --async=all) still builds — no regression. Findings captured for the wit-bindgen 0.54 contract: filter names require the package version; combining `all` with a per-export `-export:` exclude is rejected ("unused async option") — use [] (WIT defaults) or the allowlist form. Refs: pulseengine/relay#202, pulseengine/witness#107 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dedae09 commit d8cdb30

4 files changed

Lines changed: 79 additions & 5 deletions

File tree

rust/private/rust_wasm_component_bindgen.bzl

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ def rust_wasm_component_bindgen(
477477
symmetric = False,
478478
invert_direction = False,
479479
bitflags_dep = "@crates//:bitflags",
480+
async_interfaces = None,
480481
**kwargs):
481482
"""Builds a Rust WebAssembly component with automatic WIT binding generation.
482483
@@ -507,12 +508,38 @@ def rust_wasm_component_bindgen(
507508
bitflags = "2"
508509
509510
And configure crate_universe in MODULE.bazel.
511+
async_interfaces: Optional wit-bindgen `--async` filters for the guest
512+
(WASM) bindings, overriding the default. By default p3 lifts every
513+
export async (`["all"]`) and p2 is fully sync (`[]`). Under the p3
514+
default a plain-sync WIT function still becomes an async-lift export,
515+
which a call-return consumer cannot invoke (#526). Override to mix
516+
sync and async exports. Each entry is a wit-bindgen `--async` value;
517+
**filter names must include the package version**, e.g.
518+
`pkg:iface/i@0.1.0#fn`:
519+
520+
[] # no --async: each export
521+
# follows its WIT signature
522+
# (async-typed -> async,
523+
# sync -> sync). Solves #526
524+
# when the WIT already
525+
# distinguishes them.
526+
["-all"] # force every export sync
527+
["-export:pkg:iface/i@0.1.0#fn"] # `fn` sync, others WIT-default
528+
["pkg:iface/i@0.1.0#stream-fn"] # `stream-fn` async, others default
529+
530+
Note: wit-bindgen (0.54) rejects combining the `all` blanket with a
531+
per-export `-export:` exclude ("unused async option") — use the
532+
WIT-default (`[]`) or allowlist forms above instead. Only affects the
533+
guest bindings; native-guest bindings are always sync (the host
534+
runtime has no async_support).
510535
**kwargs: Additional arguments passed to rust_wasm_component
511536
"""
512537

513-
# Determine P3 async settings from wasi_version
538+
# Determine P3 async settings from wasi_version, unless the caller supplied
539+
# an explicit override. Default: p3 lifts all exports async, p2 is sync.
514540
wasi_version = kwargs.get("wasi_version", "p2")
515-
p3_async_interfaces = ["all"] if wasi_version == "p3" else []
541+
if async_interfaces == None:
542+
async_interfaces = ["all"] if wasi_version == "p3" else []
516543

517544
# Generate WIT bindings based on symmetric flag
518545
if symmetric:
@@ -541,7 +568,7 @@ def rust_wasm_component_bindgen(
541568
wit = wit,
542569
language = "rust",
543570
generation_mode = "guest",
544-
async_interfaces = p3_async_interfaces,
571+
async_interfaces = async_interfaces,
545572
visibility = ["//visibility:private"],
546573
)
547574

test/p3/BUILD.bazel

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ build_test(
7070
targets = [":hello_p3"],
7171
)
7272

73+
# Per-function async override (#526): a plain-sync WIT export kept sync under p3
74+
# while the component is still p3. Without the override this fails to compile
75+
# ("method should be `async` ... but it is synchronous"); the override forces a
76+
# call-return lift so a sync consumer can invoke it.
77+
rust_wasm_component_bindgen(
78+
name = "hello_p3_sync",
79+
srcs = ["src_p3_sync/lib.rs"],
80+
# Force the `greet` export sync (call-return) even though the component is
81+
# p3. The filter name requires the package version. Other exports (none
82+
# here) would follow their WIT default.
83+
async_interfaces = ["-export:hello:interfaces/greeting@0.1.0#greet"],
84+
profiles = ["release"],
85+
wasi_version = "p3",
86+
wit = ":hello_interfaces",
87+
)
88+
89+
build_test(
90+
name = "p3_sync_export_build_test",
91+
targets = [":hello_p3_sync"],
92+
)
93+
7394
# ============================================================================
7495
# WASI 0.3 interface WIT (stable v0.3.0) — covers wasi_p3_deps.bzl
7596
# ============================================================================
@@ -102,6 +123,7 @@ test_suite(
102123
name = "p3",
103124
tests = [
104125
":p3_build_test",
126+
":p3_sync_export_build_test",
105127
":wasi_p3_wit_build_test",
106128
],
107129
)
@@ -111,6 +133,7 @@ test_suite(
111133
tests = [
112134
":p2_build_test",
113135
":p3_build_test",
136+
":p3_sync_export_build_test",
114137
":wasi_p3_wit_build_test",
115138
],
116139
)

test/p3/src_p3_sync/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// P3 component exposing a SYNC export via the async_interfaces override (#526).
2+
//
3+
// `greet` has a plain-sync WIT signature. Under the p3 default (--async all) it
4+
// becomes an async-lift export, and this sync `fn greet` would fail to compile
5+
// with "method should be `async` or return a future, but it is synchronous" —
6+
// the exact friction reported in #526. The bindgen target forces it sync with
7+
// async_interfaces = ["all", "-export:hello:interfaces/greeting#greet"]
8+
// so a call-return consumer (e.g. a witness MC/DC harness) can invoke it.
9+
use hello_p3_sync_bindings::exports::hello::interfaces::greeting::Guest;
10+
11+
struct Component;
12+
13+
impl Guest for Component {
14+
fn greet(name: String) -> String {
15+
format!("Hello, {}! (P3 sync export)", name)
16+
}
17+
}
18+
19+
hello_p3_sync_bindings::export!(Component with_types_in hello_p3_sync_bindings);

wit/private/wit_bindgen.bzl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ def _build_derive_args(additional_derives):
2222
return args
2323

2424
def _build_async_args(async_interfaces):
25-
"""Build --async arguments for async interface configuration"""
25+
"""Build --async arguments for async interface configuration.
26+
27+
Uses the `--async=VALUE` form rather than `--async VALUE` so that filter
28+
values beginning with `-` (e.g. `-all`, `-export:pkg:iface/i#fn`, which force
29+
a method sync) are not misparsed by clap as separate flags.
30+
"""
2631
args = []
2732
for async_interface in async_interfaces:
28-
args.extend(["--async", async_interface])
33+
args.append("--async=" + async_interface)
2934
return args
3035

3136
def _to_snake_case(name):

0 commit comments

Comments
 (0)