diff --git a/crates/backend/src/typegen.rs b/crates/backend/src/typegen.rs index f7eeb174..2f0efdf7 100644 --- a/crates/backend/src/typegen.rs +++ b/crates/backend/src/typegen.rs @@ -218,6 +218,12 @@ fn is_ts_union_type(rust_ty: &str) -> bool { .unwrap_or(false) } +const TSFN_RUST_TY: &str = "ThreadsafeFunction"; + +fn should_convert_tuple_to_variadic(rust_ty: &str) -> bool { + rust_ty == TSFN_RUST_TY +} + fn is_ts_function_type_notation(ty: &Type) -> bool { match ty { Type::Path(syn::TypePath { qself: None, path }) => { @@ -235,12 +241,32 @@ fn is_ts_function_type_notation(ty: &Type) -> bool { } } -pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (String, bool) { +pub fn ty_to_ts_type( + ty: &Type, + is_return_ty: bool, + is_struct_field: bool, + convert_tuple_to_variadic: bool, +) -> (String, bool, bool) { match ty { - Type::Reference(r) => ty_to_ts_type(&r.elem, is_return_ty, is_struct_field), + Type::Reference(r) => ty_to_ts_type(&r.elem, is_return_ty, is_struct_field, false), Type::Tuple(tuple) => { if tuple.elems.is_empty() { - ("undefined".to_owned(), false) + ("undefined".to_owned(), false, false) + } else if convert_tuple_to_variadic { + let variadic = &tuple + .elems + .iter() + .enumerate() + .map(|(i, arg)| { + let (ts_type, is_optional, _) = ty_to_ts_type(arg, false, false, false); + r#fn::FnArg { + arg: format!("arg{}", i), + ts_type, + is_optional, + } + }) + .collect::(); + (format!("{}", variadic), false, true) } else { ( format!( @@ -248,11 +274,12 @@ pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (S tuple .elems .iter() - .map(|elem| ty_to_ts_type(elem, false, false).0) + .map(|elem| ty_to_ts_type(elem, false, false, false).0) .collect::>() .join(", ") ), false, + false, ) } } @@ -267,14 +294,18 @@ pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (S .args .iter() .filter_map(|arg| match arg { - syn::GenericArgument::Type(generic_ty) => { - Some(ty_to_ts_type(generic_ty, false, false)).map(|(mut ty, is_struct_field)| { - if is_ts_union_type && is_ts_function_type_notation(generic_ty) { - ty = format!("({})", ty); - } - (ty, is_struct_field) - }) - } + syn::GenericArgument::Type(generic_ty) => Some(ty_to_ts_type( + generic_ty, + false, + false, + should_convert_tuple_to_variadic(&rust_ty), + )) + .map(|(mut ty, is_optional, is_variadic)| { + if is_ts_union_type && is_ts_function_type_notation(generic_ty) { + ty = format!("({})", ty); + } + (ty, is_optional, is_variadic) + }), _ => None, }) .collect::>() @@ -285,7 +316,7 @@ pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (S if rust_ty == "Result" && is_return_ty { ts_ty = Some(args.first().unwrap().to_owned()); } else if rust_ty == "Option" { - ts_ty = args.first().map(|(arg, _)| { + ts_ty = args.first().map(|(arg, _, _)| { ( if is_struct_field { arg.to_string() @@ -295,56 +326,78 @@ pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (S format!("{} | undefined | null", arg) }, true, + false, ) }); } else if rust_ty == "AsyncTask" { ts_ty = r#struct::TASK_STRUCTS.with(|t| { - let (output_type, _) = args.first().unwrap().to_owned(); + let (output_type, _, _) = args.first().unwrap().to_owned(); if let Some(o) = t.borrow().get(&output_type) { - Some((format!("Promise<{}>", o), false)) + Some((format!("Promise<{}>", o), false, false)) } else { - Some(("Promise".to_owned(), false)) + Some(("Promise".to_owned(), false, false)) } }); } else if rust_ty == "Reference" || rust_ty == "WeakReference" { ts_ty = r#struct::TASK_STRUCTS.with(|t| { // Reference => T if let Some(arg) = args.first() { - let (output_type, _) = arg.to_owned(); + let (output_type, _, _) = arg.to_owned(); if let Some(o) = t.borrow().get(&output_type) { - Some((o.to_owned(), false)) + Some((o.to_owned(), false, false)) } else { - Some((output_type, false)) + Some((output_type, false, false)) } } else { // Not NAPI-RS `Reference` - Some((rust_ty, false)) + Some((rust_ty, false, false)) } }); } else if let Some(&(known_ty, _, _)) = KNOWN_TYPES.get(rust_ty.as_str()) { if known_ty.contains("{}") { ts_ty = Some(( - fill_ty(known_ty, args.into_iter().map(|(arg, _)| arg).collect()), + fill_ty(known_ty, args.into_iter().map(|(arg, _, _)| arg).collect()), + false, false, )); } else { - ts_ty = Some((known_ty.to_owned(), false)); + ts_ty = Some((known_ty.to_owned(), false, false)); } } else if let Some(t) = crate::typegen::r#struct::CLASS_STRUCTS .with(|c| c.borrow_mut().get(rust_ty.as_str()).cloned()) { - ts_ty = Some((t, false)); - } else if rust_ty == "ThreadsafeFunction" { + ts_ty = Some((t, false, false)); + } else if rust_ty == TSFN_RUST_TY { let fatal_tsfn = match args.get(1) { - Some((arg, _)) => arg == "Fatal", + Some((arg, _, _)) => arg == "Fatal", _ => false, }; - let first_arg = args.first().map(|(arg, _)| arg).unwrap(); + let (args, is_variadic) = args + .first() + .map(|(arg, _, is_variadic)| (arg, is_variadic)) + .unwrap(); ts_ty = if fatal_tsfn { - Some((format!("(value: {}) => any", first_arg), false)) + Some(( + { + if *is_variadic { + format!("({}) => any", args) + } else { + format!("(value: {}) => any", args) + } + }, + false, + false, + )) } else { Some(( - format!("(err: Error | null, value: {}) => any", first_arg), + { + if *is_variadic { + format!("(err: Error | null, {}) => any", args) + } else { + format!("(err: Error | null, value: {}) => any", args) + } + }, + false, false, )) }; @@ -354,19 +407,25 @@ pub fn ty_to_ts_type(ty: &Type, is_return_ty: bool, is_struct_field: bool) -> (S aliases .borrow() .get(rust_ty.as_str()) - .map(|a| (a.to_owned(), false)) + .map(|a| (a.to_owned(), false, false)) }); - ts_ty = type_alias.or(Some((rust_ty, false))); + ts_ty = type_alias.or(Some((rust_ty, false, false))); } } - ts_ty.unwrap_or_else(|| ("any".to_owned(), false)) + ts_ty.unwrap_or_else(|| ("any".to_owned(), false, false)) } - Type::Group(g) => ty_to_ts_type(&g.elem, is_return_ty, is_struct_field), + Type::Group(g) => ty_to_ts_type(&g.elem, is_return_ty, is_struct_field, false), Type::Array(a) => { - let (element_type, is_optional) = ty_to_ts_type(&a.elem, is_return_ty, is_struct_field); - (format!("{}[]", element_type), is_optional) + let (element_type, is_optional, _) = + ty_to_ts_type(&a.elem, is_return_ty, is_struct_field, false); + (format!("{}[]", element_type), is_optional, false) } - _ => ("any".to_owned(), false), + Type::Paren(p) => { + let (element_type, is_optional, _) = + ty_to_ts_type(&p.elem, is_return_ty, is_struct_field, false); + (element_type, is_optional, false) + } + _ => ("any".to_owned(), false, false), } } diff --git a/crates/backend/src/typegen/const.rs b/crates/backend/src/typegen/const.rs index e926608d..e3933d4e 100644 --- a/crates/backend/src/typegen/const.rs +++ b/crates/backend/src/typegen/const.rs @@ -17,7 +17,7 @@ impl ToTypeDef for NapiConst { def: format!( "export const {}: {}", &self.js_name, - ty_to_ts_type(&self.type_name, false, false).0 + ty_to_ts_type(&self.type_name, false, false, false).0 ), js_mod: self.js_mod.to_owned(), js_doc: js_doc_from_comments(&self.comments), diff --git a/crates/backend/src/typegen/fn.rs b/crates/backend/src/typegen/fn.rs index be135729..8832650c 100644 --- a/crates/backend/src/typegen/fn.rs +++ b/crates/backend/src/typegen/fn.rs @@ -6,13 +6,13 @@ use syn::{Pat, PathArguments, PathSegment}; use super::{ty_to_ts_type, ToTypeDef, TypeDef}; use crate::{js_doc_from_comments, CallbackArg, FnKind, NapiFn}; -struct FnArg { - arg: String, - ts_type: String, - is_optional: bool, +pub(crate) struct FnArg { + pub(crate) arg: String, + pub(crate) ts_type: String, + pub(crate) is_optional: bool, } -struct FnArgList { +pub(crate) struct FnArgList { this: Option, args: Vec, last_required: Option, @@ -110,7 +110,7 @@ fn gen_callback_type(callback: &CallbackArg) -> String { .iter() .enumerate() .map(|(i, arg)| { - let (ts_type, is_optional) = ty_to_ts_type(arg, false, false); + let (ts_type, is_optional, _) = ty_to_ts_type(arg, false, false, false); FnArg { arg: format!("arg{}", i), ts_type, @@ -119,7 +119,7 @@ fn gen_callback_type(callback: &CallbackArg) -> String { }) .collect::(), ret = match &callback.ret { - Some(ty) => ty_to_ts_type(ty, true, false).0, + Some(ty) => ty_to_ts_type(ty, true, false, false).0, None => "void".to_owned(), } ) @@ -153,7 +153,7 @@ impl NapiFn { }) = arguments { if let Some(syn::GenericArgument::Type(ty)) = angle_bracketed_args.first() { - let (ts_type, _) = ty_to_ts_type(ty, false, false); + let (ts_type, _, _) = ty_to_ts_type(ty, false, false, false); return Some(FnArg { arg: "this".to_owned(), ts_type, @@ -178,7 +178,7 @@ impl NapiFn { i.mutability = None; } - let (ts_type, is_optional) = ty_to_ts_type(&path.ty, false, false); + let (ts_type, is_optional, _) = ty_to_ts_type(&path.ty, false, false, false); let ts_type = arg.use_overridden_type_or(|| ts_type); let arg = path.pat.to_token_stream().to_string().to_case(Case::Camel); @@ -230,7 +230,7 @@ impl NapiFn { .unwrap_or_else(|| "".to_owned()), _ => { let ret = if let Some(ret) = &self.ret { - let (ts_type, _) = ty_to_ts_type(ret, true, false); + let (ts_type, _, _) = ty_to_ts_type(ret, true, false, false); if ts_type == "undefined" { "void".to_owned() } else if ts_type == "Self" { diff --git a/crates/backend/src/typegen/struct.rs b/crates/backend/src/typegen/struct.rs index b67c0211..971edfd2 100644 --- a/crates/backend/src/typegen/struct.rs +++ b/crates/backend/src/typegen/struct.rs @@ -38,19 +38,19 @@ impl ToTypeDef for NapiImpl { TASK_STRUCTS.with(|t| { t.borrow_mut().insert( self.name.to_string(), - ty_to_ts_type(output_type, false, true).0, + ty_to_ts_type(output_type, false, true, false).0, ); }); } if let Some(output_type) = &self.iterator_yield_type { let next_type = if let Some(ref ty) = self.iterator_next_type { - ty_to_ts_type(ty, false, false).0 + ty_to_ts_type(ty, false, false, false).0 } else { "void".to_owned() }; let return_type = if let Some(ref ty) = self.iterator_return_type { - ty_to_ts_type(ty, false, false).0 + ty_to_ts_type(ty, false, false, false).0 } else { "void".to_owned() }; @@ -60,7 +60,7 @@ impl ToTypeDef for NapiImpl { original_name: None, def: format!( "[Symbol.iterator](): Iterator<{}, {}, {}>", - ty_to_ts_type(output_type, false, true).0, + ty_to_ts_type(output_type, false, true, false).0, return_type, next_type, ), @@ -118,7 +118,7 @@ impl NapiStruct { field_str.push_str("readonly ") } - let (arg, is_optional) = ty_to_ts_type(&f.ty, false, true); + let (arg, is_optional, _) = ty_to_ts_type(&f.ty, false, true, false); let arg = f.ts_type.as_ref().map(|ty| ty.to_string()).unwrap_or(arg); let sep = if is_optional { "?" } else { "" }; diff --git a/crates/napi/src/threadsafe_function.rs b/crates/napi/src/threadsafe_function.rs index 931ea898..736b5c58 100644 --- a/crates/napi/src/threadsafe_function.rs +++ b/crates/napi/src/threadsafe_function.rs @@ -208,9 +208,72 @@ impl Clone for ThreadsafeFunction { } } -impl FromNapiValue for ThreadsafeFunction { +pub trait JsValuesTupleIntoVec { + fn into_vec(self, env: &Env) -> Result>; +} + +impl JsValuesTupleIntoVec for T { + fn into_vec(self, env: &Env) -> Result> { + Ok(vec![JsUnknown(crate::Value { + env: env.0, + value: unsafe { ::to_napi_value(env.0, self)? }, + value_type: crate::ValueType::Unknown, + })]) + } +} + +macro_rules! impl_js_value_tuple_to_vec { + ($($ident:ident),*) => { + impl<$($ident: ToNapiValue),*> JsValuesTupleIntoVec for ($($ident,)*) { + fn into_vec(self, env: &Env) -> Result> { + #[allow(non_snake_case)] + let ($($ident,)*) = self; + Ok(vec![$(JsUnknown($crate::Value { + env: env.0, + value: unsafe { <$ident as ToNapiValue>::to_napi_value(env.0, $ident)? }, + value_type: $crate::ValueType::Unknown, + })),*]) + } + } + }; +} + +impl_js_value_tuple_to_vec!(A); +impl_js_value_tuple_to_vec!(A, B); +impl_js_value_tuple_to_vec!(A, B, C); +impl_js_value_tuple_to_vec!(A, B, C, D); +impl_js_value_tuple_to_vec!(A, B, C, D, E); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W); +impl_js_value_tuple_to_vec!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X); +impl_js_value_tuple_to_vec!( + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y +); +impl_js_value_tuple_to_vec!( + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z +); + +impl FromNapiValue + for ThreadsafeFunction +{ unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { - Self::create(env, napi_val, 0, |ctx| Ok(vec![ctx.value])) + Self::create(env, napi_val, 0, |ctx| ctx.value.into_vec(&ctx.env)) } } diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index cf98e6a6..7c627270 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -209,6 +209,7 @@ Generated by [AVA](https://avajs.dev). export function tsfnAsyncCall(func: (...args: any[]) => any): Promise␊ export function acceptThreadsafeFunction(func: (err: Error | null, value: number) => any): void␊ export function acceptThreadsafeFunctionFatal(func: (value: number) => any): void␊ + export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void␊ export function getBuffer(): Buffer␊ export function appendBuffer(buf: Buffer): Buffer␊ export function getEmptyBuffer(): Buffer␊ diff --git a/examples/napi/__test__/typegen.spec.ts.snap b/examples/napi/__test__/typegen.spec.ts.snap index 269788d6..c299f346 100644 Binary files a/examples/napi/__test__/typegen.spec.ts.snap and b/examples/napi/__test__/typegen.spec.ts.snap differ diff --git a/examples/napi/__test__/values.spec.ts b/examples/napi/__test__/values.spec.ts index 698e62dd..53624a31 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -119,6 +119,7 @@ import { bigintFromI64, acceptThreadsafeFunction, acceptThreadsafeFunctionFatal, + acceptThreadsafeFunctionTupleArgs, promiseInEither, runScript, } from '../' @@ -827,6 +828,20 @@ Napi4Test('accept ThreadsafeFunction Fatal', async (t) => { }) }) +Napi4Test('accept ThreadsafeFunction tuple args', async (t) => { + await new Promise((resolve, reject) => { + acceptThreadsafeFunctionTupleArgs((err, num, bool, str) => { + if (err) { + return reject(err) + } + t.is(num, 1) + t.is(bool, false) + t.is(str, 'NAPI-RS') + resolve() + }) + }) +}) + Napi4Test('object only from js', (t) => { return new Promise((resolve, reject) => { receiveObjectOnlyFromJs({ diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index cea2101e..b76c0778 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -199,6 +199,7 @@ export function tsfnCallWithCallback(func: (...args: any[]) => any): void export function tsfnAsyncCall(func: (...args: any[]) => any): Promise export function acceptThreadsafeFunction(func: (err: Error | null, value: number) => any): void export function acceptThreadsafeFunctionFatal(func: (value: number) => any): void +export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void export function getBuffer(): Buffer export function appendBuffer(buf: Buffer): Buffer export function getEmptyBuffer(): Buffer diff --git a/examples/napi/src/threadsafe_function.rs b/examples/napi/src/threadsafe_function.rs index fe88ab64..a6bfea0f 100644 --- a/examples/napi/src/threadsafe_function.rs +++ b/examples/napi/src/threadsafe_function.rs @@ -116,3 +116,13 @@ pub fn accept_threadsafe_function_fatal(func: ThreadsafeFunction) { + thread::spawn(move || { + func.call( + Ok((1, false, "NAPI-RS".into())), + ThreadsafeFunctionCallMode::NonBlocking, + ); + }); +}