diff --git a/crates/spidermonkey-embedding-splicer/src/bindgen.rs b/crates/spidermonkey-embedding-splicer/src/bindgen.rs index 872c1b57..250431b7 100644 --- a/crates/spidermonkey-embedding-splicer/src/bindgen.rs +++ b/crates/spidermonkey-embedding-splicer/src/bindgen.rs @@ -29,6 +29,7 @@ pub enum Resource { Constructor(String), Static(String), Method(String), + Destructor(String), } impl Resource { @@ -38,6 +39,7 @@ impl Resource { Resource::Constructor(name) => format!("[constructor]{name}"), Resource::Static(name) => format!("[static]{name}.{fn_name}"), Resource::Method(name) => format!("[method]{name}.{fn_name}"), + Resource::Destructor(name) => format!("[dtor]{name}"), } } @@ -65,6 +67,9 @@ impl Resource { fn_name.to_lower_camel_case() ) } + Resource::Destructor(name) => { + format!("{}$dtor", name.to_lower_camel_case()) + } } } } @@ -190,7 +195,8 @@ pub fn componentize_bindgen( Resource::None => None, Resource::Method(name) | Resource::Static(name) - | Resource::Constructor(name) => Some(name), + | Resource::Constructor(name) + | Resource::Destructor(name) => Some(name), }) .or_default() .push(item); @@ -426,10 +432,12 @@ impl JsBindgen<'_> { } WorldItem::Interface { id, stability: _ } => { let iface = &self.resolve.interfaces[*id]; - for id in iface.types.values() { - if let TypeDefKind::Resource = &self.resolve.types[*id].kind { + let iface_name = interface_name(self.resolve, *id); + for ty_id in iface.types.values() { + if let TypeDefKind::Resource = &self.resolve.types[*ty_id].kind { self.resource_directions - .insert(*id, AbiVariant::GuestExport); + .insert(*ty_id, AbiVariant::GuestExport); + self.resource_dtor_bindgen(name.clone(), iface_name.clone(), *ty_id); } } for (func_name, func) in &iface.functions { @@ -991,6 +999,59 @@ impl JsBindgen<'_> { )); } + fn resource_dtor_bindgen( + &mut self, + export_name: String, + iface_name: Option, + resource: TypeId, + ) { + let resource_name = self.resolve.types[resource].name.as_ref().unwrap(); + let resource_name_camel = resource_name.to_lower_camel_case(); + let prefix = iface_name + .as_deref() + .map(|name| format!("{name}$")) + .unwrap_or_default(); + let symbol_dispose = self.intrinsic(Intrinsic::SymbolDispose); + let symbol_resource_handle = self.intrinsic(Intrinsic::SymbolResourceHandle); + let binding_name = format!( + "export_{}", + binding_name(&format!("{resource_name_camel}$dtor"), &iface_name) + ); + + uwriteln!( + self.src, + " + async function {binding_name}(rep) {{ + const entry = repTable.get(rep); + if (!entry) return; + repTable.delete(rep); + const resource = entry.rep; + delete resource[{symbol_resource_handle}]; + finalizationRegistry_export${prefix}{resource_name_camel}.unregister(resource); + if (resource[{symbol_dispose}]) resource[{symbol_dispose}](); + }} + " + ); + + self.exports.push(( + export_name, + BindingItem { + iface: true, + iface_name, + binding_name, + resource: Resource::Destructor(resource_name.clone()), + name: resource_name.clone(), + func: CoreFn { + params: vec![CoreTy::I32], + ret: None, + retptr: false, + retsize: 0, + paramptr: false, + }, + }, + )); + } + fn core_fn(&self, func: &Function, sig: &WasmSignature) -> CoreFn { CoreFn { retsize: if sig.retptr { diff --git a/crates/spidermonkey-embedding-splicer/src/splice.rs b/crates/spidermonkey-embedding-splicer/src/splice.rs index 0c70bf5c..df74450d 100644 --- a/crates/spidermonkey-embedding-splicer/src/splice.rs +++ b/crates/spidermonkey-embedding-splicer/src/splice.rs @@ -199,7 +199,11 @@ pub fn splice_bindings( } else { export_name.clone() }; - exports.push((expt, map_core_fn(func))); + exports.push(( + expt, + map_core_fn(func), + matches!(resource, bindgen::Resource::Destructor(_)), + )); } let mut imports = Vec::new(); @@ -340,7 +344,7 @@ pub fn splice_bindings( pub fn splice( engine: Vec, imports: Vec<(String, String, CoreFn, Option)>, - exports: Vec<(String, CoreFn)>, + exports: Vec<(String, CoreFn, bool)>, features: Vec, debug: bool, ) -> Result> { @@ -383,13 +387,13 @@ pub fn splice( fn remove_if_exported_by_js( module: &mut Module, - content_exports: &[(String, CoreFn)], + content_exports: &[(String, CoreFn, bool)], name_start: &str, name_end: &str, ) { let content_exports_run = content_exports .iter() - .any(|(name, _)| name.starts_with(name_start) && name.ends_with(name_end)); + .any(|(name, _, _)| name.starts_with(name_start) && name.ends_with(name_end)); if content_exports_run { let exported_run_fn = module .exports @@ -846,7 +850,10 @@ fn synthesize_import_functions( Ok(()) } -fn synthesize_export_functions(module: &mut Module, exports: &[(String, CoreFn)]) -> Result<()> { +fn synthesize_export_functions( + module: &mut Module, + exports: &[(String, CoreFn, bool)], +) -> Result<()> { let cabi_realloc = get_export_fid( module, &module @@ -867,7 +874,7 @@ fn synthesize_export_functions(module: &mut Module, exports: &[(String, CoreFn)] let memory = 0; // (2) Export call function synthesis - for (export_num, (expt_name, expt_sig)) in exports.iter().enumerate() { + for (export_num, (expt_name, expt_sig, inline_post_call)) in exports.iter().enumerate() { // Export function synthesis { // add the function type @@ -1031,10 +1038,19 @@ fn synthesize_export_functions(module: &mut Module, exports: &[(String, CoreFn)] } } + if *inline_post_call { + func.i32_const(export_num as i32); + func.call(post_call); + } + let fid = func.finish_module(module); module.exports.add_export_func((*expt_name).clone(), *fid); } + if *inline_post_call { + continue; + } + // Post export function synthesis // We always define a post-export since we use a bulk deallocation strategy // add the function type diff --git a/test/cases/resource-disposal/source.js b/test/cases/resource-disposal/source.js new file mode 100644 index 00000000..09a58c5d --- /dev/null +++ b/test/cases/resource-disposal/source.js @@ -0,0 +1,23 @@ +let disposeCount = 0; +const disposeSymbol = Symbol.dispose || Symbol.for('dispose'); + +class Example { + constructor(id) { + this.id = id; + } + + getId() { + return this.id; + } + + [disposeSymbol]() { + disposeCount += 1; + } +} + +export const resources = { + Example, + disposeCount() { + return disposeCount; + }, +}; diff --git a/test/cases/resource-disposal/test.js b/test/cases/resource-disposal/test.js new file mode 100644 index 00000000..03ff7ce3 --- /dev/null +++ b/test/cases/resource-disposal/test.js @@ -0,0 +1,14 @@ +import { strictEqual } from 'node:assert'; + +const disposeSymbol = Symbol.dispose || Symbol.for('dispose'); + +export function test(instance) { + const resource = new instance.resources.Example(42); + + strictEqual(resource.getId(), 42); + resource[disposeSymbol](); + strictEqual(instance.resources.disposeCount(), 1); + + resource[disposeSymbol](); + strictEqual(instance.resources.disposeCount(), 1); +} diff --git a/test/cases/resource-disposal/world.wit b/test/cases/resource-disposal/world.wit new file mode 100644 index 00000000..c2748fcd --- /dev/null +++ b/test/cases/resource-disposal/world.wit @@ -0,0 +1,14 @@ +package test:test; + +interface resources { + resource example { + constructor(id: u32); + get-id: func() -> u32; + } + + dispose-count: func() -> u32; +} + +world test { + export resources; +}