Skip to content

fix: allow for use of both manual & fetchEvent based HTTP #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,29 @@ The default set of features includes:
* `'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.**
* `'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.**
* `'http'`: Support for outbound HTTP via the `fetch` global in JS.
* `'fetch-event'`: Support for `fetch` based incoming request handling (i.e. `addEventListener('fetch', ...)`)

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.
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.

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

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.

Note that depending on your component implementation, some features may be automatically disabled. For example, if using
`wasi:http/incoming-handler` manually, the `fetch-event` cannot be used.

## Using StarlingMonkey's `fetch-event`

The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/incoming-handler@0.2.0#handle`. When targeting worlds that export `wasi:http/incoming-handler@0.2.0` the fetch event will automatically be attached. Alternatively, to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/incoming-handler@0.2.0'` object. Using the `fetchEvent` requires enabling the `http` feature.
The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/incoming-handler@0.2.0#handle`.

When targeting worlds that export `wasi:http/incoming-handler@0.2.0` the fetch event will automatically be attached. Alternatively,
to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/incoming-handler@0.2.0'`
object. Using the `fetchEvent` requires enabling the `http` feature.

> [!WARNING]
> If using `fetch-event`, ensure that you *do not* manually import (i.e. exporting `incomingHandler` from your ES module).
>
> Modules that export `incomingHandler` and have the `http` feature enabled are assumed to be using `wasi:http` manually.

## API

Expand All @@ -206,7 +219,7 @@ export function componentize(opts: {
debugBuild?: bool,
engine?: string,
preview2Adapter?: string,
disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http')[],
disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http', 'fetch-event')[],
}): {
component: Uint8Array,
imports: string[]
Expand Down
16 changes: 14 additions & 2 deletions crates/spidermonkey-embedding-splicer/src/bin/splicer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ enum Commands {
#[arg(short, long)]
out_dir: PathBuf,

/// Features to enable (multiple allowed)
#[arg(short, long)]
features: Vec<String>,

/// Path to WIT file or directory
#[arg(long)]
wit_path: Option<PathBuf>,
Expand Down Expand Up @@ -99,6 +103,7 @@ fn main() -> Result<()> {
Commands::SpliceBindings {
input,
out_dir,
features,
wit_path,
world_name,
debug,
Expand All @@ -113,8 +118,15 @@ fn main() -> Result<()> {

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

let result = splice::splice_bindings(engine, world_name, wit_path_str, None, debug)
.map_err(|e| anyhow::anyhow!(e))?;
let features = features
.iter()
.map(|v| Features::from_str(v))
.collect::<Result<Vec<_>>>()?;

let result =
splice::splice_bindings(engine, features, None, wit_path_str, world_name, debug)
.map_err(|e| anyhow::anyhow!(e))?;

fs::write(out_dir.join("component.wasm"), result.wasm).with_context(|| {
format!(
"Failed to write output file: {}",
Expand Down
70 changes: 20 additions & 50 deletions crates/spidermonkey-embedding-splicer/src/bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use wit_component::StringEncoding;
use wit_parser::abi::WasmType;
use wit_parser::abi::{AbiVariant, WasmSignature};

use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::Features;

use crate::{uwrite, uwriteln};

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

imported_resources: BTreeSet<TypeId>,

/// Features that were enabled at the time of generation
features: &'a Vec<Features>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -131,7 +136,11 @@ pub struct Componentization {
pub resource_imports: Vec<(String, String, u32)>,
}

pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result<Componentization> {
pub fn componentize_bindgen(
resolve: &Resolve,
wid: WorldId,
features: &Vec<Features>,
) -> Result<Componentization> {
let mut bindgen = JsBindgen {
src: Source::default(),
esm_bindgen: EsmBindgen::default(),
Expand All @@ -146,6 +155,7 @@ pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result<Component
imports: Vec::new(),
resource_directions: HashMap::new(),
imported_resources: BTreeSet::new(),
features,
};

bindgen.sizes.fill(resolve);
Expand Down Expand Up @@ -390,57 +400,17 @@ impl JsBindgen<'_> {
intrinsic.name().to_string()
}

fn exports_bindgen(
&mut self,
// guest_exports: &Option<Vec<String>>,
// features: Vec<Features>,
) -> Result<()> {
fn exports_bindgen(&mut self) -> Result<()> {
for (key, export) in &self.resolve.worlds[self.world].exports {
let name = self.resolve.name_world_key(key);
// Do not generate exports when the guest export is not implemented.
// We check both the full interface name - "ns:pkg@v/my-interface" and the
// aliased interface name "myInterface". All other names are always
// camel-case in the check.
// match key {
// WorldKey::Interface(iface) => {
// if !guest_exports.contains(&name) {
// let iface = &self.resolve.interfaces[*iface];
// if let Some(iface_name) = iface.name.as_ref() {
// let camel_case_name = iface_name.to_lower_camel_case();
// if !guest_exports.contains(&camel_case_name) {
// // For wasi:http/incoming-handler, we treat it
// // as a special case as the engine already
// // provides the export using fetchEvent and that
// // can be used when an explicit export is not
// // defined by the guest content.
// if iface_name == "incoming-handler"
// || name.starts_with("wasi:http/incoming-handler@0.2.")
// {
// if !features.contains(&Features::Http) {
// bail!(
// "JS export definition for '{}' not found. Cannot use fetchEvent because the http feature is not enabled.",
// camel_case_name
// )
// }
// continue;
// }
// bail!("Expected a JS export definition for '{}'", camel_case_name);
// }
// // TODO: move populate_export_aliases to a preprocessing
// // step that doesn't require esm_bindgen, so that we can
// // do alias deduping here as well.
// } else {
// continue;
// }
// }
// }
// WorldKey::Name(export_name) => {
// let camel_case_name = export_name.to_lower_camel_case();
// if !guest_exports.contains(&camel_case_name) {
// bail!("Expected a JS export definition for '{}'", camel_case_name);
// }
// }
// }

// Skip bindings generation for wasi:http/incoming-handler if the fetch-event
// feature was enabled. We expect that the built-in engine implementation will be used
if name.starts_with("wasi:http/incoming-handler@0.2.")
&& self.features.contains(&Features::FetchEvent)
{
continue;
}

match export {
WorldItem::Function(func) => {
Expand Down
31 changes: 20 additions & 11 deletions crates/spidermonkey-embedding-splicer/src/splice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use wit_parser::Resolve;

use crate::bindgen::BindingItem;
use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::{
CoreFn, CoreTy, SpliceResult,
CoreFn, CoreTy, Features, SpliceResult,
};
use crate::{bindgen, map_core_fn, parse_wit, splice};

Expand All @@ -32,9 +32,10 @@ use crate::{bindgen, map_core_fn, parse_wit, splice};
// }
pub fn splice_bindings(
engine: Vec<u8>,
world_name: Option<String>,
wit_path: Option<String>,
features: Vec<Features>,
wit_source: Option<String>,
wit_path: Option<String>,
world_name: Option<String>,
debug: bool,
) -> Result<SpliceResult, String> {
let (mut resolve, id) = match (wit_source, wit_path) {
Expand All @@ -60,6 +61,7 @@ pub fn splice_bindings(
wit_component::dummy_module(&resolve, world, wit_parser::ManglingAndAbi::Standard32);

// merge the engine world with the target world, retaining the engine producers

let (engine_world, producers) = if let Ok((
_,
Bindgen {
Expand Down Expand Up @@ -113,7 +115,7 @@ pub fn splice_bindings(
};

let componentized =
bindgen::componentize_bindgen(&resolve, world).map_err(|err| err.to_string())?;
bindgen::componentize_bindgen(&resolve, world, &features).map_err(|err| err.to_string())?;

resolve
.merge_worlds(engine_world, world)
Expand Down Expand Up @@ -255,7 +257,8 @@ pub fn splice_bindings(
));
}

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

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

// since StarlingMonkey implements CLI Run and incoming handler,
// we override them only if the guest content exports those functions
remove_if_exported_by_js(&mut module, &exports, "wasi:cli/run@0.2.", "#run");
remove_if_exported_by_js(
&mut module,
&exports,
"wasi:http/incoming-handler@0.2.",
"#handle",
);

// if 'fetch-event' feature is disabled (default being default-enabled),
// remove the built-in incoming-handler which is built around it's use.
if !features.contains(&Features::FetchEvent) {
remove_if_exported_by_js(
&mut module,
&exports,
"wasi:http/incoming-handler@0.2.",
"#handle",
);
}

// we reencode the WASI world component data, so strip it out from the
// custom section
Expand Down
2 changes: 1 addition & 1 deletion crates/spidermonkey-embedding-splicer/src/stub_wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ pub fn stub_wasi(
stub_stdio(&mut module)?;
}

if !features.contains(&Features::Http) {
if !features.contains(&Features::Http) && !features.contains(&Features::FetchEvent) {
stub_http(&mut module)?;
}

Expand Down
1 change: 1 addition & 0 deletions crates/spidermonkey-embedding-splicer/src/wit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ impl std::str::FromStr for Features {
"clocks" => Ok(Features::Clocks),
"random" => Ok(Features::Random),
"http" => Ok(Features::Http),
"fetch-event" => Ok(Features::FetchEvent),
_ => bail!("unrecognized feature string [{s}]"),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface splicer {
clocks,
random,
http,
fetch-event,
}

record core-fn {
Expand Down
10 changes: 5 additions & 5 deletions crates/splicer-component/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use spidermonkey_embedding_splicer::stub_wasi::stub_wasi;
use spidermonkey_embedding_splicer::wit::{self, export};
use spidermonkey_embedding_splicer::splice;
use spidermonkey_embedding_splicer::wit::exports::local::spidermonkey_embedding_splicer::splicer::{Features, Guest, SpliceResult};
use spidermonkey_embedding_splicer::splice;

struct SpidermonkeyEmbeddingSplicerComponent;

Expand All @@ -18,13 +18,13 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent {

fn splice_bindings(
engine: Vec<u8>,
_features: Vec<Features>,
world_name: Option<String>,
wit_path: Option<String>,
features: Vec<Features>,
wit_source: Option<String>,
wit_path: Option<String>,
world_name: Option<String>,
debug: bool,
) -> Result<SpliceResult, String> {
splice::splice_bindings(engine, wit_source, wit_path, world_name, debug)
splice::splice_bindings(engine, features, wit_source, wit_path, world_name, debug)
}
}

Expand Down
Loading
Loading