Skip to content

Commit 24cef8f

Browse files
fix: allow for use of both manual & fetchEvent based HTTP (#247)
* fix: allow for use of both manual & fetchEvent based HTTP This commit updates the componentize-js code to support both fetchEvent and manual based HTTP. To make this possible we needed to re-introduce a few things: - A feature that represents fetch-event - Allow controlling removal of the the built-in fetch event - Allow skipping checks for manual wasi:http/incoming-handler impls - Add oxc-parser to detect manual impls and toggle fetch-event feature The docs are also updated to note the changes to API/usage. * test: add test case for fetch-event integration * fix: tests, arg order for splice bindings * refactor: use sets for features & detected exports * refactor: remove comment * chore: fix clippy * refactor: simplify feature creation
1 parent a2622a2 commit 24cef8f

File tree

13 files changed

+485
-116
lines changed

13 files changed

+485
-116
lines changed

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,29 @@ The default set of features includes:
184184
* `'random'`: Support for cryptographic random, depends on `wasi:random`. **When disabled, random numbers will still be generated but will not be random and instead fully deterministic.**
185185
* `'clocks'`: Support for clocks and duration polls, depends on `wasi:clocks` and `wasi:io`. **When disabled, using any timer functions like setTimeout or setInterval will panic.**
186186
* `'http'`: Support for outbound HTTP via the `fetch` global in JS.
187+
* `'fetch-event'`: Support for `fetch` based incoming request handling (i.e. `addEventListener('fetch', ...)`)
187188

188-
Setting `disableFeatures: ['random', 'stdio', 'clocks', 'http']` will disable all features creating a minimal "pure component", that does not depend on any WASI APIs at all and just the target world.
189+
Setting `disableFeatures: ['random', 'stdio', 'clocks', 'http', 'fetch-event']` will disable all features creating a minimal "pure component", that does not depend on any WASI APIs at all and just the target world.
189190

190191
Note that pure components **will not report errors and will instead trap**, so that this should only be enabled after very careful testing.
191192

192193
Note that features explicitly imported by the target world cannot be disabled - if you target a component to a world that imports `wasi:clocks`, then `disableFeatures: ['clocks']` will not be supported.
193194

195+
Note that depending on your component implementation, some features may be automatically disabled. For example, if using
196+
`wasi:http/incoming-handler` manually, the `fetch-event` cannot be used.
197+
194198
## Using StarlingMonkey's `fetch-event`
195199

196-
The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/[email protected]#handle`. When targeting worlds that export `wasi:http/[email protected]` the fetch event will automatically be attached. Alternatively, to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/[email protected]'` object. Using the `fetchEvent` requires enabling the `http` feature.
200+
The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/[email protected]#handle`.
201+
202+
When targeting worlds that export `wasi:http/[email protected]` the fetch event will automatically be attached. Alternatively,
203+
to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/[email protected]'`
204+
object. Using the `fetchEvent` requires enabling the `http` feature.
205+
206+
> [!WARNING]
207+
> If using `fetch-event`, ensure that you *do not* manually import (i.e. exporting `incomingHandler` from your ES module).
208+
>
209+
> Modules that export `incomingHandler` and have the `http` feature enabled are assumed to be using `wasi:http` manually.
197210
198211
## API
199212

@@ -206,7 +219,7 @@ export function componentize(opts: {
206219
debugBuild?: bool,
207220
engine?: string,
208221
preview2Adapter?: string,
209-
disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http')[],
222+
disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http', 'fetch-event')[],
210223
}): {
211224
component: Uint8Array,
212225
imports: string[]

crates/spidermonkey-embedding-splicer/src/bin/splicer.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ enum Commands {
5050
#[arg(short, long)]
5151
out_dir: PathBuf,
5252

53+
/// Features to enable (multiple allowed)
54+
#[arg(short, long)]
55+
features: Vec<String>,
56+
5357
/// Path to WIT file or directory
5458
#[arg(long)]
5559
wit_path: Option<PathBuf>,
@@ -99,6 +103,7 @@ fn main() -> Result<()> {
99103
Commands::SpliceBindings {
100104
input,
101105
out_dir,
106+
features,
102107
wit_path,
103108
world_name,
104109
debug,
@@ -113,8 +118,15 @@ fn main() -> Result<()> {
113118

114119
let wit_path_str = wit_path.as_ref().map(|p| p.to_string_lossy().to_string());
115120

116-
let result = splice::splice_bindings(engine, world_name, wit_path_str, None, debug)
117-
.map_err(|e| anyhow::anyhow!(e))?;
121+
let features = features
122+
.iter()
123+
.map(|v| Features::from_str(v))
124+
.collect::<Result<Vec<_>>>()?;
125+
126+
let result =
127+
splice::splice_bindings(engine, features, None, wit_path_str, world_name, debug)
128+
.map_err(|e| anyhow::anyhow!(e))?;
129+
118130
fs::write(out_dir.join("component.wasm"), result.wasm).with_context(|| {
119131
format!(
120132
"Failed to write output file: {}",

crates/spidermonkey-embedding-splicer/src/bindgen.rs

Lines changed: 20 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use wit_component::StringEncoding;
1919
use wit_parser::abi::WasmType;
2020
use wit_parser::abi::{AbiVariant, WasmSignature};
2121

22+
use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::Features;
23+
2224
use crate::{uwrite, uwriteln};
2325

2426
#[derive(Debug)]
@@ -104,6 +106,9 @@ struct JsBindgen<'a> {
104106
resource_directions: HashMap<TypeId, AbiVariant>,
105107

106108
imported_resources: BTreeSet<TypeId>,
109+
110+
/// Features that were enabled at the time of generation
111+
features: &'a Vec<Features>,
107112
}
108113

109114
#[derive(Debug)]
@@ -131,7 +136,11 @@ pub struct Componentization {
131136
pub resource_imports: Vec<(String, String, u32)>,
132137
}
133138

134-
pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result<Componentization> {
139+
pub fn componentize_bindgen(
140+
resolve: &Resolve,
141+
wid: WorldId,
142+
features: &Vec<Features>,
143+
) -> Result<Componentization> {
135144
let mut bindgen = JsBindgen {
136145
src: Source::default(),
137146
esm_bindgen: EsmBindgen::default(),
@@ -146,6 +155,7 @@ pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result<Component
146155
imports: Vec::new(),
147156
resource_directions: HashMap::new(),
148157
imported_resources: BTreeSet::new(),
158+
features,
149159
};
150160

151161
bindgen.sizes.fill(resolve);
@@ -390,57 +400,17 @@ impl JsBindgen<'_> {
390400
intrinsic.name().to_string()
391401
}
392402

393-
fn exports_bindgen(
394-
&mut self,
395-
// guest_exports: &Option<Vec<String>>,
396-
// features: Vec<Features>,
397-
) -> Result<()> {
403+
fn exports_bindgen(&mut self) -> Result<()> {
398404
for (key, export) in &self.resolve.worlds[self.world].exports {
399405
let name = self.resolve.name_world_key(key);
400-
// Do not generate exports when the guest export is not implemented.
401-
// We check both the full interface name - "ns:pkg@v/my-interface" and the
402-
// aliased interface name "myInterface". All other names are always
403-
// camel-case in the check.
404-
// match key {
405-
// WorldKey::Interface(iface) => {
406-
// if !guest_exports.contains(&name) {
407-
// let iface = &self.resolve.interfaces[*iface];
408-
// if let Some(iface_name) = iface.name.as_ref() {
409-
// let camel_case_name = iface_name.to_lower_camel_case();
410-
// if !guest_exports.contains(&camel_case_name) {
411-
// // For wasi:http/incoming-handler, we treat it
412-
// // as a special case as the engine already
413-
// // provides the export using fetchEvent and that
414-
// // can be used when an explicit export is not
415-
// // defined by the guest content.
416-
// if iface_name == "incoming-handler"
417-
// || name.starts_with("wasi:http/[email protected].")
418-
// {
419-
// if !features.contains(&Features::Http) {
420-
// bail!(
421-
// "JS export definition for '{}' not found. Cannot use fetchEvent because the http feature is not enabled.",
422-
// camel_case_name
423-
// )
424-
// }
425-
// continue;
426-
// }
427-
// bail!("Expected a JS export definition for '{}'", camel_case_name);
428-
// }
429-
// // TODO: move populate_export_aliases to a preprocessing
430-
// // step that doesn't require esm_bindgen, so that we can
431-
// // do alias deduping here as well.
432-
// } else {
433-
// continue;
434-
// }
435-
// }
436-
// }
437-
// WorldKey::Name(export_name) => {
438-
// let camel_case_name = export_name.to_lower_camel_case();
439-
// if !guest_exports.contains(&camel_case_name) {
440-
// bail!("Expected a JS export definition for '{}'", camel_case_name);
441-
// }
442-
// }
443-
// }
406+
407+
// Skip bindings generation for wasi:http/incoming-handler if the fetch-event
408+
// feature was enabled. We expect that the built-in engine implementation will be used
409+
if name.starts_with("wasi:http/[email protected].")
410+
&& self.features.contains(&Features::FetchEvent)
411+
{
412+
continue;
413+
}
444414

445415
match export {
446416
WorldItem::Function(func) => {

crates/spidermonkey-embedding-splicer/src/splice.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use wit_parser::Resolve;
1818

1919
use crate::bindgen::BindingItem;
2020
use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::{
21-
CoreFn, CoreTy, SpliceResult,
21+
CoreFn, CoreTy, Features, SpliceResult,
2222
};
2323
use crate::{bindgen, map_core_fn, parse_wit, splice};
2424

@@ -32,9 +32,10 @@ use crate::{bindgen, map_core_fn, parse_wit, splice};
3232
// }
3333
pub fn splice_bindings(
3434
engine: Vec<u8>,
35-
world_name: Option<String>,
36-
wit_path: Option<String>,
35+
features: Vec<Features>,
3736
wit_source: Option<String>,
37+
wit_path: Option<String>,
38+
world_name: Option<String>,
3839
debug: bool,
3940
) -> Result<SpliceResult, String> {
4041
let (mut resolve, id) = match (wit_source, wit_path) {
@@ -60,6 +61,7 @@ pub fn splice_bindings(
6061
wit_component::dummy_module(&resolve, world, wit_parser::ManglingAndAbi::Standard32);
6162

6263
// merge the engine world with the target world, retaining the engine producers
64+
6365
let (engine_world, producers) = if let Ok((
6466
_,
6567
Bindgen {
@@ -113,7 +115,7 @@ pub fn splice_bindings(
113115
};
114116

115117
let componentized =
116-
bindgen::componentize_bindgen(&resolve, world).map_err(|err| err.to_string())?;
118+
bindgen::componentize_bindgen(&resolve, world, &features).map_err(|err| err.to_string())?;
117119

118120
resolve
119121
.merge_worlds(engine_world, world)
@@ -255,7 +257,8 @@ pub fn splice_bindings(
255257
));
256258
}
257259

258-
let mut wasm = splice::splice(engine, imports, exports, debug).map_err(|e| format!("{e:?}"))?;
260+
let mut wasm =
261+
splice::splice(engine, imports, exports, features, debug).map_err(|e| format!("{e:?}"))?;
259262

260263
// add the world section to the spliced wasm
261264
wasm.push(section.id());
@@ -337,19 +340,25 @@ pub fn splice(
337340
engine: Vec<u8>,
338341
imports: Vec<(String, String, CoreFn, Option<i32>)>,
339342
exports: Vec<(String, CoreFn)>,
343+
features: Vec<Features>,
340344
debug: bool,
341345
) -> Result<Vec<u8>> {
342346
let mut module = Module::parse(&engine, false).unwrap();
343347

344348
// since StarlingMonkey implements CLI Run and incoming handler,
345349
// we override them only if the guest content exports those functions
346350
remove_if_exported_by_js(&mut module, &exports, "wasi:cli/[email protected].", "#run");
347-
remove_if_exported_by_js(
348-
&mut module,
349-
&exports,
350-
"wasi:http/[email protected].",
351-
"#handle",
352-
);
351+
352+
// if 'fetch-event' feature is disabled (default being default-enabled),
353+
// remove the built-in incoming-handler which is built around it's use.
354+
if !features.contains(&Features::FetchEvent) {
355+
remove_if_exported_by_js(
356+
&mut module,
357+
&exports,
358+
"wasi:http/[email protected].",
359+
"#handle",
360+
);
361+
}
353362

354363
// we reencode the WASI world component data, so strip it out from the
355364
// custom section

crates/spidermonkey-embedding-splicer/src/stub_wasi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub fn stub_wasi(
139139
stub_stdio(&mut module)?;
140140
}
141141

142-
if !features.contains(&Features::Http) {
142+
if !features.contains(&Features::Http) && !features.contains(&Features::FetchEvent) {
143143
stub_http(&mut module)?;
144144
}
145145

crates/spidermonkey-embedding-splicer/src/wit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ impl std::str::FromStr for Features {
1616
"clocks" => Ok(Features::Clocks),
1717
"random" => Ok(Features::Random),
1818
"http" => Ok(Features::Http),
19+
"fetch-event" => Ok(Features::FetchEvent),
1920
_ => bail!("unrecognized feature string [{s}]"),
2021
}
2122
}

crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface splicer {
1313
clocks,
1414
random,
1515
http,
16+
fetch-event,
1617
}
1718

1819
record core-fn {

crates/splicer-component/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use spidermonkey_embedding_splicer::stub_wasi::stub_wasi;
22
use spidermonkey_embedding_splicer::wit::{self, export};
3-
use spidermonkey_embedding_splicer::splice;
43
use spidermonkey_embedding_splicer::wit::exports::local::spidermonkey_embedding_splicer::splicer::{Features, Guest, SpliceResult};
4+
use spidermonkey_embedding_splicer::splice;
55

66
struct SpidermonkeyEmbeddingSplicerComponent;
77

@@ -18,13 +18,13 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {
1818

1919
fn splice_bindings(
2020
engine: Vec<u8>,
21-
_features: Vec<Features>,
22-
world_name: Option<String>,
23-
wit_path: Option<String>,
21+
features: Vec<Features>,
2422
wit_source: Option<String>,
23+
wit_path: Option<String>,
24+
world_name: Option<String>,
2525
debug: bool,
2626
) -> Result<SpliceResult, String> {
27-
splice::splice_bindings(engine, wit_source, wit_path, world_name, debug)
27+
splice::splice_bindings(engine, features, wit_source, wit_path, world_name, debug)
2828
}
2929
}
3030

0 commit comments

Comments
 (0)