Skip to content

Commit 732bc3f

Browse files
fix(bindgen): preserve Rust API compatibility for 2.17
1 parent 07fdbd6 commit 732bc3f

9 files changed

Lines changed: 434 additions & 133 deletions

File tree

‎crates/js-component-bindgen-component/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ wit-bindgen = { workspace = true, features = ["macros"] }
1616
wit-component = { workspace = true }
1717
wat = { workspace = true, features = [ "component-model" ]}
1818

19-
js-component-bindgen = { path = "../js-component-bindgen" }
19+
js-component-bindgen = { path = "../js-component-bindgen", features = ["external-runtime"] }

‎crates/js-component-bindgen-component/src/lib.rs‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{collections::BTreeMap, path::PathBuf};
33
use anyhow::{Context as _, Result};
44

55
use js_component_bindgen::source::wit_parser::{Resolve, UnresolvedPackageGroup};
6-
use js_component_bindgen::transpile;
6+
use js_component_bindgen::{TranspileConfig, transpile_with_config};
77
use wit_component::{DecodedWasm, WitPrinter};
88

99
/// Calls [`write!`] with the passed arguments and unwraps the result.
@@ -86,7 +86,6 @@ impl bindings::Guest for JsComponentBindgenComponent {
8686
.perf_strings_skip_copy_utf16_validation
8787
.unwrap_or(false),
8888
)
89-
.maybe_cm_runtime_es_module(options.cm_runtime_es_module)
9089
.flags_as_bigint(options.flags_as_bigint.unwrap_or(false))
9190
.variants_inline_cases(options.variants_inline_cases.unwrap_or(false))
9291
.use_namespace_objects(options.use_namespace_objects.unwrap_or(false))
@@ -103,9 +102,15 @@ impl bindings::Guest for JsComponentBindgenComponent {
103102
files,
104103
imports,
105104
mut exports,
106-
} = transpile(&component, opts)
107-
.map_err(|e| format!("{e:?}"))
108-
.map_err(|e| e.to_string())?;
105+
} = transpile_with_config(
106+
&component,
107+
TranspileConfig::builder()
108+
.opts(opts)
109+
.maybe_cm_runtime_es_module(options.cm_runtime_es_module)
110+
.build(),
111+
)
112+
.map_err(|e| format!("{e:?}"))
113+
.map_err(|e| e.to_string())?;
109114

110115
Ok(Transpiled {
111116
files,

‎crates/js-component-bindgen/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ crate-type = ["lib"]
2020
[features]
2121
default = ["transpile-bindgen"]
2222
transpile-bindgen = []
23+
# Expose cooperative-thread names in the public intrinsic API.
24+
coop-threads = []
25+
# Enable selection of an external Component Model runtime provider.
26+
external-runtime = []
2327

2428
[package.metadata.docs.rs]
2529
all-features = true

‎crates/js-component-bindgen/README.md‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,39 @@ $ cargo add js-component-bindgen
3131
```
3232

3333
Generated JavaScript includes its runtime intrinsics inline by default. To
34-
select an external runtime provider, set `TranspileOpts::cm_runtime_es_module` to
34+
select an external runtime provider, enable the opt-in `external-runtime` Cargo
35+
feature, call `transpile_with_config`, and set `TranspileConfig::cm_runtime_es_module` to
3536
`@bytecodealliance/jco-cm-runtime` or another module specifier. Make the selected
3637
module resolvable from the generated output; it must export a `runtime` provider.
3738
Currently, external providers implement `resource.tableGet`, while the remaining
3839
intrinsics are generated inline.
3940

41+
```toml
42+
[dependencies]
43+
js-component-bindgen = { version = "2.17", features = ["external-runtime"] }
44+
```
45+
46+
```rust
47+
use js_component_bindgen::{TranspileConfig, TranspileOpts, transpile_with_config};
48+
49+
let config = TranspileConfig::builder()
50+
.opts(TranspileOpts::builder().name("component".into()).build())
51+
.cm_runtime_es_module("@bytecodealliance/jco-cm-runtime".into())
52+
.build();
53+
let transpiled = transpile_with_config(component, config)?;
54+
```
55+
56+
The existing `transpile(component, opts)` API and `TranspileOpts` struct literals
57+
remain supported. `TranspileConfig` is non-exhaustive so future configuration
58+
fields can be added without breaking callers; construct it with its builder or
59+
`Default`.
60+
61+
The separate opt-in `coop-threads` Cargo feature exposes the thread-named
62+
`Intrinsic` aliases, such as `Intrinsic::GlobalCurrentThreadMeta`. The published
63+
task-named variants remain available for exhaustive matches. This feature is
64+
independent of `external-runtime`; it controls the public Rust names and does
65+
not change the generated JavaScript.
66+
4067
# License
4168

4269
This project is licensed under the Apache 2.0 license with the LLVM exception.

‎crates/js-component-bindgen/src/intrinsics/js_helper.rs‎

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
//! Intrinsics that represent helpers that should be used from JS code
22
3-
use crate::{intrinsics::RenderIntrinsicsArgs, source::Source};
3+
use std::fmt::Write;
4+
5+
use crate::intrinsics::p3::async_task::AsyncTaskIntrinsic;
6+
use crate::{
7+
intrinsics::{Intrinsic, RenderIntrinsicsArgs},
8+
source::Source,
9+
uwriteln,
10+
};
411

512
/// This enum contains intrinsics that function as JS helpers
613
///
@@ -13,24 +20,37 @@ pub enum JsHelperIntrinsic {
1320

1421
/// A helper function that creates a JS `DataView` from a given memory index
1522
DataView,
23+
24+
/// Run a generator function synchronously until it yields a promise.
25+
MaybeSyncGeneratorFn,
26+
27+
/// Bypass the JSPI boundary when a host import's caller cannot block.
28+
ConditionalSuspendingHostImportFn,
1629
}
1730

1831
impl JsHelperIntrinsic {
1932
/// Retrieve global names for
2033
pub fn get_global_names() -> impl IntoIterator<Item = &'static str> {
21-
["emptyFunc", "dataView"]
34+
[
35+
"emptyFunc",
36+
"dataView",
37+
"_maybeSyncGenerator",
38+
"_conditionalSuspendingHostImport",
39+
]
2240
}
2341

2442
/// Get the name for the intrinsic
2543
pub fn name(&self) -> &'static str {
2644
match self {
2745
Self::EmptyFunc => "emptyFunc",
2846
Self::DataView => "dataView",
47+
Self::MaybeSyncGeneratorFn => "_maybeSyncGenerator",
48+
Self::ConditionalSuspendingHostImportFn => "_conditionalSuspendingHostImport",
2949
}
3050
}
3151

3252
/// Render an intrinsic to a string
33-
pub fn render(&self, output: &mut Source, _args: &RenderIntrinsicsArgs) {
53+
pub fn render(&self, output: &mut Source, args: &RenderIntrinsicsArgs) {
3454
match self {
3555
Self::EmptyFunc => output.push_str("
3656
const emptyFunc = () => {};
@@ -39,6 +59,66 @@ impl JsHelperIntrinsic {
3959
let dv = new DataView(new ArrayBuffer());
4060
const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer);
4161
"),
62+
// Wrap a generator function, which yields where an async function would await,
63+
// so that it runs synchronously until it yields a promise. A host import that
64+
// answers synchronously then returns synchronously, which a task that cannot
65+
// block (e.g. a resource destructor) requires.
66+
Self::MaybeSyncGeneratorFn => {
67+
let maybe_sync_generator_fn = args.require_intrinsic(Self::MaybeSyncGeneratorFn);
68+
69+
output.push_str(&format!(
70+
r#"
71+
function {maybe_sync_generator_fn}(genFn) {{
72+
return function (...args) {{
73+
const gen = genFn.apply(this, args);
74+
const step = (method, value) => {{
75+
while (true) {{
76+
const {{ value: yielded, done }} = gen[method](value);
77+
if (done) {{ return yielded; }}
78+
if (yielded === null ||
79+
(typeof yielded !== 'object' && typeof yielded !== 'function') ||
80+
typeof yielded.then !== 'function') {{
81+
method = 'next';
82+
value = yielded;
83+
continue;
84+
}}
85+
return Promise.resolve(yielded).then(
86+
res => step('next', res),
87+
err => step('throw', err),
88+
);
89+
}}
90+
}};
91+
return step('next', undefined);
92+
}};
93+
}}
94+
"#,
95+
));
96+
}
97+
98+
Self::ConditionalSuspendingHostImportFn => {
99+
let wrapper = args.require_intrinsic(*self);
100+
let current_meta = args.require_intrinsic(Intrinsic::GlobalCurrentThreadMeta);
101+
let get_thread = args.require_intrinsic(AsyncTaskIntrinsic::GetCurrentThread);
102+
uwriteln!(
103+
output,
104+
r#"
105+
function {wrapper}(module, componentIdx, fn) {{
106+
return new WebAssembly.Instance(module, {{ '': {{
107+
mayBlock: () => {{
108+
const saved = {current_meta}[componentIdx];
109+
const thread = saved
110+
? {get_thread}(saved.componentIdx, saved.threadID)
111+
: null;
112+
return thread?.task.mayBlock() ?? false;
113+
}},
114+
fast: fn,
115+
slow: new WebAssembly.Suspending(fn),
116+
}} }}).exports.run;
117+
}}
118+
"#,
119+
);
120+
}
121+
42122
}
43123
}
44124
}

0 commit comments

Comments
 (0)