diff --git a/README.md b/README.md index 52234a8..be54728 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,36 @@ export async function greet(name) { } ``` +Exported resource methods and static methods may also be async. Implement them +as `async` members on the exported JavaScript class; resource constructors +remain synchronous: + +```wit +package example:counter; + +interface counter-api { + resource counter { + constructor(initial: u32); + add: async func(value: u32) -> u32; + create: static async func(initial: u32) -> counter; + } +} + +world counter { + export counter-api; +} +``` + +```js +class Counter { + constructor(initial) { this.value = initial; } + async add(value) { this.value += value; return this.value; } + static async create(initial) { return new this(initial); } +} + +export const counterApi = { Counter }; +``` + ### Streams Streams transfer a sequence of values between components. Lifted WIT streams diff --git a/crates/runtime/src/bindings.rs b/crates/runtime/src/bindings.rs index b50efe8..fbfb575 100644 --- a/crates/runtime/src/bindings.rs +++ b/crates/runtime/src/bindings.rs @@ -12,7 +12,7 @@ use crate::futures::{make_future, register_future_classes}; use crate::result::ResultBoundary; use crate::streams::{make_stream, register_stream_classes}; use crate::task::Pending; -use crate::trivia::iface_lookup; +use crate::trivia::{fn_lookup, iface_lookup}; use crate::wit_imports::{FuncKind, WitInterface, classify, find_resource}; use crate::{DetHashSet, DetIndexMap, QjsCallContext, coerce_fn}; @@ -243,8 +243,8 @@ fn call_import<'js>( /// Build the `asyncExports` object for the `__cqjs` namespace. /// -/// Each wrapper calls the user's export function, then chains `.then()` to -/// signal `task_return` back to the host. +/// Each wrapper calls the user's export function or resource member, then +/// chains `.then()` to signal `task_return` back to the host. fn build_async_exports<'js>( ctx: &rquickjs::Ctx<'js>, wit_def: Wit, @@ -255,30 +255,72 @@ fn build_async_exports<'js>( let mut iface_objs: DetIndexMap> = DetIndexMap::default(); for (func_index, func) in wit_def.iter_export_funcs().enumerate() { - let func_name = func.name().to_lower_camel_case(); + let wrapper_name = func.name().to_lower_camel_case(); + let func_name = func.name(); + let kind = classify(func_name); let iface_name = func .interface() .map(|interface| iface_lookup(ctx, interface).to_string()); - let fn_name = func_name.clone(); let iface = iface_name.clone(); let wrapper = Function::new( ctx.clone(), coerce_fn(move |ctx: Ctx<'_>, args: Rest>| { let exports = ctx.user_module().exports(&ctx)?; + let export_scope = || { + if let Some(ref iface) = iface { + exports.get(iface.as_str()) + } else { + Ok(exports.clone()) + } + }; - let user_fn: Function = if let Some(ref iface) = iface { - let iface_obj: rquickjs::Object = exports.get(iface.as_str())?; - iface_obj.get(fn_name.as_str())? - } else { - exports.get(fn_name.as_str())? + let mut values = args.0; + let (user_fn, this): (Function<'_>, Option>) = match kind { + FuncKind::Freestanding => { + let scope: Object = export_scope()?; + (scope.get(fn_lookup(&ctx, func_name))?, None) + } + FuncKind::Constructor { .. } => { + return Err(rquickjs::Exception::throw_type( + &ctx, + "resource constructors cannot be async", + )); + } + FuncKind::Method { method, .. } => { + if values.is_empty() { + return Err(rquickjs::Exception::throw_type( + &ctx, + "async resource method receiver is missing", + )); + } + let receiver = values.remove(0); + let receiver_obj = receiver.as_object().ok_or_else(|| { + rquickjs::Exception::throw_type( + &ctx, + "async resource method receiver is not an object", + ) + })?; + let method: Function = receiver_obj.get(fn_lookup(&ctx, method))?; + (method, Some(receiver)) + } + FuncKind::Static { resource, method } => { + let scope: Object = export_scope()?; + let class_name = resource.to_upper_camel_case(); + let class: Object = scope.get(class_name.as_str())?; + let method: Function = class.get(fn_lookup(&ctx, method))?; + (method, Some(class.into_value())) + } }; - let mut js_args = function::Args::new(ctx.clone(), args.0.len()); - for arg in args.0 { + let mut js_args = function::Args::new(ctx.clone(), values.len()); + for arg in values { js_args.push_arg(arg)?; } + if let Some(this) = this { + js_args.this(this)?; + } let result = user_fn.call_arg::(js_args)?; let promise_obj = result @@ -350,7 +392,7 @@ fn build_async_exports<'js>( .or_insert_with(|| rquickjs::Object::new(ctx.clone()).unwrap()), None => &exports, }; - target.set(func_name.as_str(), wrapper)?; + target.set(wrapper_name.as_str(), wrapper)?; } for (name, obj) in iface_objs { diff --git a/crates/runtime/src/interpreter.rs b/crates/runtime/src/interpreter.rs index 801f29a..7a9d3a3 100644 --- a/crates/runtime/src/interpreter.rs +++ b/crates/runtime/src/interpreter.rs @@ -121,7 +121,8 @@ impl Interpreter for QjsInterpreter { let js_func: Function = class.get(method_name).unwrap_or_else(|err| { panic!("static method '{method_name}' not found: {err:?}") }); - let args = cx.stack_into_args(ctx); + let mut args = cx.stack_into_args(ctx); + args.this(class).expect("failed to set static this"); call_export(ctx, func, method_name, js_func, args, cx); } FuncKind::Freestanding => { diff --git a/tests/async_types.rs b/tests/async_types.rs index 562553b..37ee6ac 100644 --- a/tests/async_types.rs +++ b/tests/async_types.rs @@ -134,6 +134,247 @@ async fn test_async_with_await() { assert_eq!(result, Val::U32(100)); } +#[tokio::test] +async fn test_async_exported_resource_members() { + let mut instance = TestCase::new() + .wit( + r#" + package test:async-resource; + + interface counter-api { + resource counter { + constructor(initial: u32); + add: async func(value: u32) -> u32; + get-value: func() -> u32; + create: static async func(initial: u32) -> counter; + try-fail: async func() -> result; + } + } + + world async-resource-test { + export counter-api; + } + "#, + ) + .script( + r#" + class Counter { + constructor(initial) { + this.value = initial; + } + + async add(value) { + await Promise.resolve(); + this.value += value; + return this.value; + } + + getValue() { + return this.value; + } + + static async create(initial) { + await Promise.resolve(); + return new this(initial); + } + + async tryFail() { + await Promise.resolve(); + throw "expected failure"; + } + } + + export const counterApi = { Counter }; + "#, + ) + .build_async() + .await + .unwrap(); + + let (component, store) = instance.parts(); + let iface_idx = component + .get_export_index(&mut *store, None, "test:async-resource/counter-api") + .expect("interface export not found"); + + let ctor_idx = component + .get_export_index(&mut *store, Some(&iface_idx), "[constructor]counter") + .expect("[constructor]counter not found"); + + let ctor = component.get_func(&mut *store, ctor_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + ctor.call_async(&mut *store, &[Val::U32(40)], &mut results) + .await + .unwrap(); + + let counter = results[0].clone(); + + let add_idx = component + .get_export_index(&mut *store, Some(&iface_idx), "[method]counter.add") + .expect("[method]counter.add not found"); + + let add = component.get_func(&mut *store, add_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + add.call_async(&mut *store, &[counter.clone(), Val::U32(2)], &mut results) + .await + .unwrap(); + + assert_eq!(results[0], Val::U32(42)); + + let get_value_idx = component + .get_export_index(&mut *store, Some(&iface_idx), "[method]counter.get-value") + .expect("[method]counter.get-value not found"); + + let get_value = component.get_func(&mut *store, get_value_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + get_value + .call_async(&mut *store, std::slice::from_ref(&counter), &mut results) + .await + .unwrap(); + + assert_eq!(results[0], Val::U32(42)); + + let create_idx = component + .get_export_index(&mut *store, Some(&iface_idx), "[static]counter.create") + .expect("[static]counter.create not found"); + + let create = component.get_func(&mut *store, create_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + create + .call_async(&mut *store, &[Val::U32(7)], &mut results) + .await + .unwrap(); + let created = results[0].clone(); + + let mut results = [Val::Bool(false)]; + get_value + .call_async(&mut *store, &[created], &mut results) + .await + .unwrap(); + + assert_eq!(results[0], Val::U32(7)); + + let try_fail_idx = component + .get_export_index(&mut *store, Some(&iface_idx), "[method]counter.try-fail") + .expect("[method]counter.try-fail not found"); + + let try_fail = component.get_func(&mut *store, try_fail_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + try_fail + .call_async(&mut *store, std::slice::from_ref(&counter), &mut results) + .await + .unwrap(); + + assert_eq!( + results[0], + Val::Result(Err(Some(Box::new(Val::String("expected failure".into()))))) + ); +} + +#[tokio::test] +async fn test_async_method_only_resource_interface() { + let mut instance = TestCase::new() + .wit( + r#" + package test:method-only-resource; + + interface types { + resource blob { + peek: func() -> u32; + read: async func() -> u32; + } + } + + interface factory { + use types.{blob}; + make: func(value: u32) -> blob; + } + + world method-only-resource { + export types; + export factory; + } + "#, + ) + .script( + r#" + class Blob { + constructor(value) { + this.value = value; + } + + peek() { + return this.value; + } + + async read() { + await Promise.resolve(); + return this.value; + } + } + + export const factory = { + make(value) { + return new Blob(value); + }, + }; + "#, + ) + .build_async() + .await + .unwrap(); + + let (component, store) = instance.parts(); + let factory_idx = component + .get_export_index(&mut *store, None, "test:method-only-resource/factory") + .expect("factory interface export not found"); + + let make_idx = component + .get_export_index(&mut *store, Some(&factory_idx), "make") + .expect("factory.make not found"); + + let make = component.get_func(&mut *store, make_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + make.call_async(&mut *store, &[Val::U32(9)], &mut results) + .await + .unwrap(); + + let blob = results[0].clone(); + + let types_idx = component + .get_export_index(&mut *store, None, "test:method-only-resource/types") + .expect("types interface export not found"); + + let peek_idx = component + .get_export_index(&mut *store, Some(&types_idx), "[method]blob.peek") + .expect("[method]blob.peek not found"); + + let peek = component.get_func(&mut *store, peek_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + peek.call_async(&mut *store, std::slice::from_ref(&blob), &mut results) + .await + .unwrap(); + assert_eq!(results[0], Val::U32(9)); + + let read_idx = component + .get_export_index(&mut *store, Some(&types_idx), "[method]blob.read") + .expect("[method]blob.read not found"); + + let read = component.get_func(&mut *store, read_idx).unwrap(); + let mut results = [Val::Bool(false)]; + + read.call_async(&mut *store, &[blob], &mut results) + .await + .unwrap(); + assert_eq!(results[0], Val::U32(9)); +} + #[tokio::test] async fn test_async_echo_record() { let mut instance = TestCase::new() diff --git a/tests/wit_types.rs b/tests/wit_types.rs index 05cd402..23f6d65 100644 --- a/tests/wit_types.rs +++ b/tests/wit_types.rs @@ -1889,7 +1889,7 @@ fn test_static_resource_method_in_interface() { class Widget { constructor(name) { this.name = name; } getName() { return this.name; } - static createDefault() { return new Widget("default"); } + static createDefault() { return new this("default"); } } export const widgetApi = { Widget }; "#,