diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..51a8b622 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +target +build +napi +napi-derive +napi-derive-example +test_module +.yarnrc +Cargo.lock +rustfmt.toml diff --git a/Cargo.toml b/Cargo.toml index 1e3888c3..9ff9fe0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [workspace] members = [ "./build", - "./napi" + "./napi", + "./napi-derive", + "./napi-derive-example" ] exclude = ["./test_module"] diff --git a/napi-derive-example/Cargo.toml b/napi-derive-example/Cargo.toml new file mode 100644 index 00000000..4fcaf238 --- /dev/null +++ b/napi-derive-example/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "napi-derive-example" +version = "0.1.0" +authors = ["LongYinan "] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi-rs = { path = "../napi" } +napi-derive = { path = "../napi-derive" } + +[build-dependencies] +napi-build = { path = "../build" } diff --git a/napi-derive-example/build.rs b/napi-derive-example/build.rs new file mode 100644 index 00000000..985e8b9b --- /dev/null +++ b/napi-derive-example/build.rs @@ -0,0 +1,7 @@ +extern crate napi_build; + +fn main() { + use napi_build::setup; + + setup(); +} diff --git a/napi-derive-example/src/lib.rs b/napi-derive-example/src/lib.rs new file mode 100644 index 00000000..c1696b0d --- /dev/null +++ b/napi-derive-example/src/lib.rs @@ -0,0 +1,46 @@ +#[macro_use] +extern crate napi_rs as napi; +#[macro_use] +extern crate napi_derive; + +use napi::{Any, Env, Error, Object, Result, Status, Value, CallContext, Number}; +use std::convert::TryInto; + +register_module!(test_module, init); + +fn init<'env>( + env: &'env Env, + exports: &'env mut Value<'env, Object>, +) -> Result>> { + exports.set_named_property( + "testThrow", + env.create_function("testThrow", test_throw)?, + )?; + + exports.set_named_property( + "fibonacci", + env.create_function("fibonacci", fibonacci)?, + )?; + Ok(None) +} + +#[js_function] +fn test_throw<'a>( + _ctx: CallContext, +) -> Result> { + Err(Error::new(Status::GenericFailure)) +} + +#[js_function] +fn fibonacci<'env>(ctx: CallContext<'env>) -> Result> { + let n = ctx.get::(0)?.try_into()?; + ctx.env.create_int64(fibonacci_native(n)) +} + +#[inline] +fn fibonacci_native(n: i64) -> i64 { + match n { + 1 | 2 => 1, + _ => fibonacci_native(n - 1) + fibonacci_native(n - 2) + } +} diff --git a/napi-derive/Cargo.toml b/napi-derive/Cargo.toml new file mode 100644 index 00000000..8055d1dc --- /dev/null +++ b/napi-derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "napi-derive" +version = "0.1.0" +authors = ["LongYinan "] +edition = "2018" + +[dependencies] +syn = { version = "1.0", features = ["fold", "full"] } +quote = "1.0" +proc-macro2 = "1.0" + +[lib] +proc-macro = true diff --git a/napi-derive/README.md b/napi-derive/README.md new file mode 100644 index 00000000..8c04b96d --- /dev/null +++ b/napi-derive/README.md @@ -0,0 +1,23 @@ +# napi-derive + +## js_function + +```rust +use napi_rs::{Result, Value, CallContext, Number}; +use napi_derive::js_function; +use std::convert::TryInto; + +#[js_function] +fn fibonacci<'env>(ctx: CallContext<'env>) -> Result> { + let n = ctx.get::(0)?.try_into()?; + ctx.env.create_int64(fibonacci_native(n)) +} + +#[inline] +fn fibonacci_native(n: i64) -> i64 { + match n { + 1 | 2 => 1, + _ => fibonacci_native(n - 1) + fibonacci_native(n - 2) + } +} +``` diff --git a/napi-derive/src/lib.rs b/napi-derive/src/lib.rs new file mode 100644 index 00000000..329555c1 --- /dev/null +++ b/napi-derive/src/lib.rs @@ -0,0 +1,128 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::{Ident, Literal}; +use quote::{quote, format_ident}; +use syn::fold::{fold_signature, fold_fn_arg, Fold}; +use syn::parse::{Parse, ParseStream, Result}; +use syn::punctuated::Punctuated; +use syn::{parse_macro_input, FnArg, ItemFn, Signature, Block, Token}; + +struct ArgLength { + length: Option, +} + +impl Parse for ArgLength { + fn parse(input: ParseStream) -> Result { + let vars = Punctuated::::parse_terminated(input)?; + Ok(ArgLength { + length: vars.first().map(|i| i.clone()), + }) + } +} + +struct JsFunction { + args: Vec, + name: Option, + signature: Option, + block: Vec, +} + +impl JsFunction { + pub fn new() -> Self { + JsFunction { + args: vec![], + name: None, + signature: None, + block: vec![], + } + } +} + +impl Fold for JsFunction { + fn fold_fn_arg(&mut self, arg: FnArg) -> FnArg { + self.args.push(arg.clone()); + fold_fn_arg(self, arg) + } + + fn fold_signature(&mut self, signature: Signature) -> Signature { + self.name = Some(format_ident!("{}", signature.ident)); + let mut new_signature = signature.clone(); + new_signature.ident = format_ident!("_{}", signature.ident); + self.signature = Some(new_signature); + fold_signature(self, signature) + } + + fn fold_block(&mut self, node: Block) -> Block { + self.block.push(node.clone()); + node + } +} + +#[proc_macro_attribute] +pub fn js_function(attr: TokenStream, input: TokenStream) -> TokenStream { + let arg_len = parse_macro_input!(attr as ArgLength); + let arg_len_span = arg_len.length.unwrap_or(Literal::usize_unsuffixed(0)); + let input = parse_macro_input!(input as ItemFn); + let mut js_fn = JsFunction::new(); + js_fn.fold_item_fn(input); + let fn_name = js_fn.name.unwrap(); + let fn_block = js_fn.block; + let signature = js_fn.signature.unwrap(); + let new_fn_name = signature.ident.clone(); + let expanded = quote! { + #signature #(#fn_block)* + + extern "C" fn #fn_name( + raw_env: napi_rs::sys::napi_env, + cb_info: napi_rs::sys::napi_callback_info, + ) -> napi_rs::sys::napi_value { + use std::io::Write; + use std::mem; + use std::os::raw::c_char; + use std::ptr; + use napi_rs::{Any, Env, Status, Value, CallContext}; + let mut argc = #arg_len_span as usize; + let mut raw_args = + unsafe { mem::MaybeUninit::<[napi_rs::sys::napi_value; 8]>::uninit().assume_init() }; + let mut raw_this = ptr::null_mut(); + + let mut has_error = false; + + unsafe { + let status = napi_rs::sys::napi_get_cb_info( + raw_env, + cb_info, + &mut argc as *mut usize as *mut u64, + &mut raw_args[0], + &mut raw_this, + ptr::null_mut(), + ); + has_error = has_error && (Status::from(status) == Status::Ok); + } + + let env = Env::from_raw(raw_env); + let call_ctx = CallContext::new(&env, raw_this, raw_args, #arg_len_span); + let result = call_ctx.and_then(|ctx| #new_fn_name(ctx)); + has_error = has_error && result.is_err(); + + match result { + Ok(result) => result.into_raw(), + Err(e) => { + if !cfg!(windows) { + let _ = writeln!(::std::io::stderr(), "Error calling function: {:?}", e); + } + let message = format!("{:?}", e); + unsafe { + napi_rs::sys::napi_throw_error(raw_env, ptr::null(), message.as_ptr() as *const c_char); + } + let mut undefined = ptr::null_mut(); + unsafe { napi_rs::sys::napi_get_undefined(raw_env, &mut undefined) }; + undefined + } + } + } + }; + // Hand the output tokens back to the compiler + TokenStream::from(expanded) +} diff --git a/napi/src/call_context.rs b/napi/src/call_context.rs new file mode 100644 index 00000000..40aa417b --- /dev/null +++ b/napi/src/call_context.rs @@ -0,0 +1,22 @@ +use crate::{Any, Value, Env, ValueType, Error, Status, Result, sys}; + +pub struct CallContext<'env, T: ValueType = Any> { + pub env: &'env Env, + pub this: Value<'env, T>, + args: [sys::napi_value; 8], + arg_len: usize, +} + +impl <'env, T: ValueType> CallContext <'env, T> { + pub fn new(env: &'env Env, this: sys::napi_value, args: [sys::napi_value; 8], arg_len: usize) -> Result { + Ok(Self { env, this: Value::<'env, T>::from_raw(env, this)?, args, arg_len }) + } + + pub fn get(&'env self, index: usize) -> Result> { + if index + 1 > self.arg_len { + Err(Error::new(Status::GenericFailure)) + } else { + Value::<'env, ArgType>::from_raw(&self.env, self.args[index]) + } + } +} diff --git a/napi/src/lib.rs b/napi/src/lib.rs index e0f7045f..8768c72e 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -13,7 +13,9 @@ use std::string::String as RustString; mod executor; pub mod sys; +mod call_context; +pub use call_context::CallContext; pub use sys::{napi_valuetype, Status}; pub type Result = std::result::Result; @@ -157,68 +159,6 @@ macro_rules! register_module { }; } -#[macro_export] -macro_rules! callback { - ($callback_expr:expr) => {{ - use std::io::Write; - use std::mem; - use std::os::raw::c_char; - use std::ptr; - use $crate::sys; - use $crate::{Any, Env, Status, Value}; - - extern "C" fn raw_callback( - raw_env: sys::napi_env, - cb_info: sys::napi_callback_info, - ) -> sys::napi_value { - const MAX_ARGC: usize = 8; - let mut argc = MAX_ARGC; - let mut raw_args = - unsafe { mem::MaybeUninit::<[$crate::sys::napi_value; MAX_ARGC]>::uninit().assume_init() }; - let mut raw_this = ptr::null_mut(); - - unsafe { - let status = sys::napi_get_cb_info( - raw_env, - cb_info, - &mut argc as *mut usize as *mut u64, - &mut raw_args[0], - &mut raw_this, - ptr::null_mut(), - ); - debug_assert!(Status::from(status) == Status::Ok); - } - - let env = Env::from_raw(raw_env); - let this = Value::from_raw(&env, raw_this).unwrap(); - let mut args = unsafe { mem::MaybeUninit::<[Value; 8]>::uninit().assume_init() }; - for (i, raw_arg) in raw_args.iter().enumerate() { - args[i] = Value::from_raw(&env, *raw_arg).unwrap() - } - - let callback = $callback_expr; - let result = callback(&env, this, &args[0..argc]); - - match result { - Ok(Some(result)) => result.into_raw(), - Ok(None) => env.get_undefined().unwrap().into_raw(), - Err(e) => { - if !cfg!(windows) { - let _ = writeln!(::std::io::stderr(), "Error calling function: {:?}", e); - } - let message = format!("{:?}", e); - unsafe { - $crate::sys::napi_throw_error(raw_env, ptr::null(), message.as_ptr() as *const c_char); - } - env.get_undefined().unwrap().into_raw() - } - } - } - - raw_callback - }}; -} - impl Error { pub fn new(status: Status) -> Self { Error { status: status } diff --git a/test_module/Cargo.toml b/test_module/Cargo.toml index c15e95d2..5e0d2f99 100644 --- a/test_module/Cargo.toml +++ b/test_module/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] futures = "0.3" napi-rs = {path = "../napi"} +napi-derive = {path = "../napi-derive"} [build-dependencies] napi-build = { path = "../build" } diff --git a/test_module/src/lib.rs b/test_module/src/lib.rs index 5198bcf6..6cc7ca94 100644 --- a/test_module/src/lib.rs +++ b/test_module/src/lib.rs @@ -3,7 +3,8 @@ extern crate napi_rs as napi; extern crate futures; -use napi::{Any, Env, Error, Object, Result, Status, Value}; +use napi::{Any, Env, Error, Object, Result, Status, Value, CallContext}; +use napi_derive::js_function; register_module!(test_module, init); @@ -13,23 +14,22 @@ fn init<'env>( ) -> Result>> { exports.set_named_property( "testSpawn", - env.create_function("testSpawn", callback!(test_spawn))?, + env.create_function("testSpawn", test_spawn)?, )?; exports.set_named_property( "testThrow", - env.create_function("testThrow", callback!(test_throw))?, + env.create_function("testThrow", test_throw)?, )?; Ok(None) } +#[js_function] fn test_spawn<'a>( - env: &'a Env, - _this: Value<'a, Any>, - _args: &[Value<'a, Any>], -) -> Result>> { + ctx: CallContext<'a> +) -> Result> { use futures::executor::ThreadPool; use futures::StreamExt; - + let env = ctx.env; let async_context = env.async_init(None, "test_spawn")?; let pool = ThreadPool::new().expect("Failed to build pool"); let (promise, deferred) = env.create_promise()?; @@ -55,13 +55,12 @@ fn test_spawn<'a>( env.create_executor().execute(fut_values); - Ok(Some(promise.into_any())) + Ok(promise) } +#[js_function] fn test_throw<'a>( - _env: &'a Env, - _this: Value<'a, Any>, - _args: &[Value<'a, Any>], -) -> Result>> { + _ctx: CallContext<'a>, +) -> Result> { Err(Error::new(Status::GenericFailure)) } diff --git a/test_module/yarn.lock b/test_module/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/test_module/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +