From cf1b6b721b5a9fdf5a47e85f67db89d0a127a874 Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Fri, 23 Feb 2024 16:59:58 -0500 Subject: [PATCH] feat(neon): Extractors Extractors allow quickly getting JavaScript arguments as Rust values. In addition to immediate ergonomics and performance improvements, extractors set a foundation for future proc macros. --- .cargo/config.toml | 8 +- Cargo.lock | 57 +++- crates/neon/Cargo.toml | 5 + crates/neon/src/context/internal.rs | 2 +- crates/neon/src/context/mod.rs | 80 +++++ crates/neon/src/lib.rs | 3 + crates/neon/src/types_impl/bigint.rs | 4 +- crates/neon/src/types_impl/boxed.rs | 4 +- crates/neon/src/types_impl/buffer/types.rs | 12 +- crates/neon/src/types_impl/date.rs | 4 +- crates/neon/src/types_impl/error.rs | 4 +- crates/neon/src/types_impl/extract/json.rs | 76 +++++ crates/neon/src/types_impl/extract/mod.rs | 229 +++++++++++++ crates/neon/src/types_impl/extract/types.rs | 356 ++++++++++++++++++++ crates/neon/src/types_impl/mod.rs | 37 +- crates/neon/src/types_impl/private.rs | 2 +- crates/neon/src/types_impl/promise.rs | 4 +- test/napi/Cargo.toml | 2 +- test/napi/lib/extract.js | 68 ++++ test/napi/src/js/extract.rs | 120 +++++++ test/napi/src/lib.rs | 10 + 21 files changed, 1030 insertions(+), 57 deletions(-) create mode 100644 crates/neon/src/types_impl/extract/json.rs create mode 100644 crates/neon/src/types_impl/extract/mod.rs create mode 100644 crates/neon/src/types_impl/extract/types.rs create mode 100644 test/napi/lib/extract.js create mode 100644 test/napi/src/js/extract.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 3b847a149..e508732f2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ [alias] # Neon defines mutually exclusive feature flags which prevents using `cargo clippy --all-features` # The following aliases simplify linting the entire workspace -neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers" -neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers -- -A clippy::missing_safety_doc" -neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,futures,external-buffers" -neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers,sys -- --cfg docsrs" +neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers,serde" +neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers,serde -- -A clippy::missing_safety_doc" +neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,futures,external-buffers,serde" +neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers,sys,serde -- --cfg docsrs" diff --git a/Cargo.lock b/Cargo.lock index e9293a627..7350a58f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.55", "which", ] @@ -244,6 +244,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "lazy_static" version = "1.4.0" @@ -360,6 +366,8 @@ dependencies = [ "psd", "semver", "send_wrapper", + "serde", + "serde_json", "smallvec", "tokio", "widestring", @@ -370,7 +378,7 @@ name = "neon-macros" version = "1.0.0" dependencies = [ "quote", - "syn 2.0.39", + "syn 2.0.55", "syn-mid", ] @@ -490,7 +498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.55", ] [[package]] @@ -519,9 +527,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -537,9 +545,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -628,6 +636,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + [[package]] name = "semver" version = "1.0.20" @@ -642,22 +656,33 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.55", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", ] [[package]] @@ -691,9 +716,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -708,7 +733,7 @@ checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.55", ] [[package]] @@ -728,7 +753,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.55", ] [[package]] diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index 9c7045642..1f81a3573 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -32,6 +32,8 @@ aquamarine = { version = "0.3.2", optional = true } easy-cast = { version = "0.5.2", optional = true } doc-comment = { version = "0.3.3", optional = true } send_wrapper = "0.6.0" +serde = { version = "1.0.197", optional = true } +serde_json = { version = "1.0.114", optional = true } [dependencies.tokio] version = "1.34.0" @@ -42,6 +44,9 @@ optional = true [features] default = ["napi-8"] +# Enable extracting values by serializing to JSON +serde = ["dep:serde", "dep:serde_json"] + # Enable the creation of external binary buffers. This is disabled by default # since these APIs fail at runtime in environments that enable the V8 memory # cage (such as Electron: https://www.electronjs.org/blog/v8-memory-cage). diff --git a/crates/neon/src/context/internal.rs b/crates/neon/src/context/internal.rs index 0386afff5..12747fa8d 100644 --- a/crates/neon/src/context/internal.rs +++ b/crates/neon/src/context/internal.rs @@ -20,7 +20,7 @@ impl From for Env { thread_local! { #[allow(unused)] - pub(crate) static IS_RUNNING: RefCell = RefCell::new(false); + pub(crate) static IS_RUNNING: RefCell = const { RefCell::new(false) }; } impl Env { diff --git a/crates/neon/src/context/mod.rs b/crates/neon/src/context/mod.rs index 33517db5a..c66c55c6c 100644 --- a/crates/neon/src/context/mod.rs +++ b/crates/neon/src/context/mod.rs @@ -157,6 +157,7 @@ use crate::{ types::{ boxed::{Finalize, JsBox}, error::JsError, + extract::FromArgs, private::ValueInternal, Deferred, JsArray, JsArrayBuffer, JsBoolean, JsBuffer, JsFunction, JsNull, JsNumber, JsObject, JsPromise, JsString, JsUndefined, JsValue, StringResult, Value, @@ -212,6 +213,36 @@ impl CallbackInfo<'_> { local } } + + pub(crate) fn argv_exact<'b, C: Context<'b>, const N: usize>( + &self, + cx: &mut C, + ) -> [Handle<'b, JsValue>; N] { + use std::ptr; + + let mut argv = [JsValue::new_internal(ptr::null_mut()); N]; + let mut argc = argv.len(); + + // # Safety + // * Node-API fills empty slots with `undefined + // * `Handle` and `JsValue` are transparent wrappers around a raw pointer + unsafe { + assert_eq!( + sys::get_cb_info( + cx.env().to_raw(), + self.info, + &mut argc, + argv.as_mut_ptr().cast(), + ptr::null_mut(), + ptr::null_mut(), + ), + sys::Status::Ok, + ); + } + + // Empty values will be filled with `undefined` + argv + } } /// Indicates whether a function was called with `new`. @@ -683,6 +714,55 @@ impl<'a> FunctionContext<'a> { pub fn this_value(&mut self) -> Handle<'a, JsValue> { JsValue::new_internal(self.info.this(self)) } + + /// Extract Rust data from the JavaScript arguments. + /// + /// This is frequently more efficient and ergonomic than getting arguments + /// individually. See the [`extract`](crate::types::extract) module documentation + /// for more examples. + /// + /// ``` + /// # use neon::{prelude::*, types::extract::*}; + /// fn add(mut cx: FunctionContext) -> JsResult { + /// let (a, b): (f64, f64) = cx.args()?; + /// + /// Ok(cx.number(a + b)) + /// } + /// ``` + pub fn args(&mut self) -> NeonResult + where + T: FromArgs<'a>, + { + T::from_args(self) + } + + /// Extract Rust data from the JavaScript arguments. + /// + /// Similar to [`FunctionContext::args`], but does not throw a JavaScript exception on errors. Useful + /// for function overloading. + /// + /// ``` + /// # use neon::{prelude::*, types::extract::*}; + /// fn combine(mut cx: FunctionContext) -> JsResult { + /// if let Some((a, b)) = cx.args_opt::<(f64, f64)>()? { + /// return Ok(cx.number(a + b).upcast()); + /// } + /// + /// let (a, b): (String, String) = cx.args()?; + /// + /// Ok(cx.string(a + &b).upcast()) + /// } + /// ``` + pub fn args_opt(&mut self) -> NeonResult> + where + T: FromArgs<'a>, + { + T::from_args_opt(self) + } + + pub(crate) fn argv(&mut self) -> [Handle<'a, JsValue>; N] { + self.info.argv_exact(self) + } } impl<'a> ContextInternal<'a> for FunctionContext<'a> { diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 818c078bc..c11281685 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -78,6 +78,8 @@ //! [supported]: https://github.com/neon-bindings/neon#platform-support #![cfg_attr(docsrs, feature(doc_cfg))] +extern crate core; + pub mod context; pub mod event; pub mod handle; @@ -88,6 +90,7 @@ pub mod reflect; pub mod result; #[cfg(not(feature = "sys"))] mod sys; +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] #[cfg(feature = "napi-6")] pub mod thread; // To use the #[aquamarine] attribute on the top-level neon::types module docs, we have to diff --git a/crates/neon/src/types_impl/bigint.rs b/crates/neon/src/types_impl/bigint.rs index dd8db5b3b..805f8931a 100644 --- a/crates/neon/src/types_impl/bigint.rs +++ b/crates/neon/src/types_impl/bigint.rs @@ -428,8 +428,8 @@ unsafe impl TransparentNoCopyWrapper for JsBigInt { } impl private::ValueInternal for JsBigInt { - fn name() -> String { - "BigInt".to_string() + fn name() -> &'static str { + "BigInt" } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/boxed.rs b/crates/neon/src/types_impl/boxed.rs index 990ce83cf..e54330757 100644 --- a/crates/neon/src/types_impl/boxed.rs +++ b/crates/neon/src/types_impl/boxed.rs @@ -183,8 +183,8 @@ unsafe impl TransparentNoCopyWrapper for JsBox { } impl ValueInternal for JsBox { - fn name() -> String { - any::type_name::().to_string() + fn name() -> &'static str { + any::type_name::() } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/buffer/types.rs b/crates/neon/src/types_impl/buffer/types.rs index 652dc7a7d..6b499d66c 100644 --- a/crates/neon/src/types_impl/buffer/types.rs +++ b/crates/neon/src/types_impl/buffer/types.rs @@ -124,8 +124,8 @@ unsafe impl TransparentNoCopyWrapper for JsBuffer { } impl ValueInternal for JsBuffer { - fn name() -> String { - "Buffer".to_string() + fn name() -> &'static str { + "Buffer" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -341,8 +341,8 @@ unsafe impl TransparentNoCopyWrapper for JsArrayBuffer { } impl ValueInternal for JsArrayBuffer { - fn name() -> String { - "JsArrayBuffer".to_string() + fn name() -> &'static str { + "JsArrayBuffer" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -791,8 +791,8 @@ macro_rules! impl_typed_array { impl Object for JsTypedArray<$etyp> {} impl ValueInternal for JsTypedArray<$etyp> { - fn name() -> String { - stringify!($typ).to_string() + fn name() -> &'static str { + stringify!($typ) } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/date.rs b/crates/neon/src/types_impl/date.rs index 6f9706a74..54ffcd44a 100644 --- a/crates/neon/src/types_impl/date.rs +++ b/crates/neon/src/types_impl/date.rs @@ -160,8 +160,8 @@ impl JsDate { } impl ValueInternal for JsDate { - fn name() -> String { - "object".to_string() + fn name() -> &'static str { + "object" } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/error.rs b/crates/neon/src/types_impl/error.rs index c34d6de7f..bfc98b317 100644 --- a/crates/neon/src/types_impl/error.rs +++ b/crates/neon/src/types_impl/error.rs @@ -48,8 +48,8 @@ unsafe impl TransparentNoCopyWrapper for JsError { } impl ValueInternal for JsError { - fn name() -> String { - "Error".to_string() + fn name() -> &'static str { + "Error" } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/extract/json.rs b/crates/neon/src/types_impl/extract/json.rs new file mode 100644 index 000000000..ccb3b1de1 --- /dev/null +++ b/crates/neon/src/types_impl/extract/json.rs @@ -0,0 +1,76 @@ +use crate::{ + context::Context, + handle::Handle, + object::Object, + result::{JsResult, NeonResult}, + types::{ + extract::{private, TryFromJs}, + JsFunction, JsObject, JsString, JsValue, + }, +}; + +#[cfg(feature = "napi-6")] +use crate::{handle::Root, thread::LocalKey}; + +fn global_json_stringify<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + cx.global::("JSON")?.get(cx, "stringify") +} + +#[cfg(not(feature = "napi-6"))] +fn json_stringify<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + global_json_stringify(cx) +} + +#[cfg(feature = "napi-6")] +fn json_stringify<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + static STRINGIFY: LocalKey> = LocalKey::new(); + + STRINGIFY + .get_or_try_init(cx, |cx| global_json_stringify(cx).map(|f| f.root(cx))) + .map(|f| f.to_inner(cx)) +} + +fn stringify<'cx, C>(cx: &mut C, v: Handle) -> NeonResult +where + C: Context<'cx>, +{ + json_stringify(cx)? + .call(cx, v, [v])? + .downcast_or_throw::(cx) + .map(|s| s.value(cx)) +} + +/// Extract a value by serializing to JSON +pub struct Json(pub T); + +impl<'cx, T> TryFromJs<'cx> for Json +where + for<'de> T: serde::de::Deserialize<'de>, +{ + type Error = serde_json::Error; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + Ok(serde_json::from_str(&stringify(cx, v)?).map(Json)) + } + + fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + Self::try_from_js(cx, v)?.or_else(|err| cx.throw_error(err.to_string())) + } +} + +impl private::Sealed for Json {} diff --git a/crates/neon/src/types_impl/extract/mod.rs b/crates/neon/src/types_impl/extract/mod.rs new file mode 100644 index 000000000..d5cec2480 --- /dev/null +++ b/crates/neon/src/types_impl/extract/mod.rs @@ -0,0 +1,229 @@ +//! Traits and utilities for extract Rust data from JavaScript values +//! +//! The full list of included extractors can be found on [`TryFromJs`]. +//! +//! ## Extracting Handles +//! +//! JavaScript arguments may be extracted into a Rust tuple. +//! +//! ``` +//! # use neon::{prelude::*, types::extract::*}; +//! fn greet(mut cx: FunctionContext) -> JsResult { +//! let (greeting, name): (Handle, Handle) = cx.args()?; +//! let message = format!("{}, {}!", greeting.value(&mut cx), name.value(&mut cx)); +//! +//! Ok(cx.string(message)) +//! } +//! ``` +//! +//! ## Extracting Native Types +//! +//! It's also possible to extract directly into native Rust types instead of a [`Handle`]. +//! +//! ``` +//! # use neon::{prelude::*, types::extract::*}; +//! fn add(mut cx: FunctionContext) -> JsResult { +//! let (a, b): (f64, f64) = cx.args()?; +//! +//! Ok(cx.number(a + b)) +//! } +//! ``` +//! +//! ## Extracting [`Option`] +//! +//! It's also possible to mix [`Handle`], Rust types, and even [`Option`] for +//! handling `null` and `undefined`. +//! +//! ``` +//! # use neon::{prelude::*, types::extract::*}; +//! fn get_or_default(mut cx: FunctionContext) -> JsResult { +//! let (n, default_value): (Option, Handle) = cx.args()?; +//! +//! if let Some(n) = n { +//! return Ok(cx.number(n).upcast()); +//! } +//! +//! Ok(default_value) +//! } +//! ``` +//! +//! ## Additional Extractors +//! +//! In some cases, the expected JavaScript type is ambiguous. For example, when +//! trying to extract an [`f64`], the argument may be a `Date` instead of a `number`. +//! Newtype extractors are provided to help. +//! +//! ``` +//! # use neon::{prelude::*, types::extract::*}; +//! # #[cfg(feature = "napi-5")] +//! # use neon::types::JsDate; +//! +//! # #[cfg(feature = "napi-5")] +//! fn add_hours(mut cx: FunctionContext) -> JsResult { +//! const MS_PER_HOUR: f64 = 60.0 * 60.0 * 1000.0; +//! +//! let (Date(date), hours): (Date, f64) = cx.args()?; +//! let date = date + hours * MS_PER_HOUR; +//! +//! cx.date(date).or_throw(&mut cx) +//! } +//! ``` +//! +//! ## Overloaded Functions +//! +//! It's common in JavaScript to overload function signatures. This can be implemented with +//! [`FunctionContext::args_opt`] or [`Context::try_catch`]. +//! +//! ``` +//! # use neon::{prelude::*, types::extract::*}; +//! +//! fn add(mut cx: FunctionContext, a: f64, b: f64) -> Handle { +//! cx.number(a + b) +//! } +//! +//! fn concat(mut cx: FunctionContext, a: String, b: String) -> Handle { +//! cx.string(a + &b) +//! } +//! +//! fn combine(mut cx: FunctionContext) -> JsResult { +//! if let Some((a, b)) = cx.args_opt()? { +//! return Ok(add(cx, a, b).upcast()); +//! } +//! +//! let (a, b) = cx.args()?; +//! +//! Ok(concat(cx, a, b).upcast()) +//! } +//! ``` +//! +//! Note well, in this example, type annotations are not required on the tuple because +//! Rust is able to infer it from the type arguments on `add` and `concat`. + +use crate::{ + context::{Context, FunctionContext}, + handle::Handle, + result::NeonResult, + types::JsValue, +}; + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +pub use self::json::*; +pub use self::types::*; + +#[cfg(feature = "serde")] +mod json; +mod types; + +/// Extract Rust data from a JavaScript value +pub trait TryFromJs<'cx> +where + Self: private::Sealed + Sized, +{ + type Error; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>; + + fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>; +} + +/// Trait specifying values that may be extracted from function arguments. +/// +/// **Note:** This trait is implemented for tuples of up to 32 values, but for +/// the sake of brevity, only tuples up to size 8 are shown in this documentation. +pub trait FromArgs<'cx>: private::FromArgsInternal<'cx> {} + +// Convenience implementation for single arguments instead of needing a single element tuple +impl<'cx, T> private::FromArgsInternal<'cx> for T +where + T: TryFromJs<'cx>, +{ + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult { + let (v,) = private::FromArgsInternal::from_args(cx)?; + + Ok(v) + } + + fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult> { + if let Some((v,)) = private::FromArgsInternal::from_args_opt(cx)? { + Ok(Some(v)) + } else { + Ok(None) + } + } +} + +impl<'cx, T> FromArgs<'cx> for T where T: TryFromJs<'cx> {} + +macro_rules! from_args_impl { + ($(#[$attrs:meta])? [$($ty:ident),*]) => { + $(#[$attrs])? + impl<'cx, $($ty,)*> FromArgs<'cx> for ($($ty,)*) + where + $($ty: TryFromJs<'cx>,)* + {} + + #[allow(non_snake_case)] + impl<'cx, $($ty,)*> private::FromArgsInternal<'cx> for ($($ty,)*) + where + $($ty: TryFromJs<'cx>,)* + { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult { + let [$($ty,)*] = cx.argv(); + + Ok(($($ty::from_js(cx, $ty)?,)*)) + } + + fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult> { + let [$($ty,)*] = cx.argv(); + + Ok(Some(( + $(match $ty::try_from_js(cx, $ty)? { + Ok(v) => v, + Err(_) => return Ok(None), + },)* + ))) + } + } + } +} + +macro_rules! from_args_expand { + ($(#[$attrs:meta])? [$($head:ident),*], []) => {}; + + ($(#[$attrs:meta])? [$($head:ident),*], [$cur:ident $(, $tail:ident)*]) => { + from_args_impl!($(#[$attrs])? [$($head,)* $cur]); + from_args_expand!($(#[$attrs])? [$($head,)* $cur], [$($tail),*]); + }; +} + +macro_rules! from_args { + ([$($show:ident),*], [$($hide:ident),*]) => { + from_args_expand!([], [$($show),*]); + from_args_expand!(#[doc(hidden)] [$($show),*], [$($hide),*]); + }; +} + +from_args!( + [T1, T2, T3, T4, T5, T6, T7, T8], + [ + T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, + T27, T28, T29, T30, T31, T32 + ] +); + +mod private { + use crate::{context::FunctionContext, result::NeonResult}; + + pub trait Sealed {} + + pub trait FromArgsInternal<'cx>: Sized { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult; + + fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult>; + } +} diff --git a/crates/neon/src/types_impl/extract/types.rs b/crates/neon/src/types_impl/extract/types.rs new file mode 100644 index 000000000..01d954a08 --- /dev/null +++ b/crates/neon/src/types_impl/extract/types.rs @@ -0,0 +1,356 @@ +use std::{convert::Infallible, error, fmt, marker::PhantomData, ptr}; + +use crate::{ + context::Context, + handle::Handle, + result::{NeonResult, ResultExt, Throw}, + sys, + types::{ + buffer::{Binary, TypedArray}, + extract::{private, TryFromJs}, + private::ValueInternal, + JsArrayBuffer, JsBoolean, JsBuffer, JsNumber, JsString, JsTypedArray, JsValue, Value, + }, +}; + +#[cfg(feature = "napi-5")] +use crate::types::JsDate; + +/// Error returned when a JavaScript value is not the type expected +pub struct TypeExpected(PhantomData); + +impl TypeExpected { + fn new() -> Self { + Self(PhantomData) + } +} + +impl fmt::Display for TypeExpected { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "expected {}", T::name()) + } +} + +impl fmt::Debug for TypeExpected { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("TypeExpected").field(&T::name()).finish() + } +} + +impl error::Error for TypeExpected {} + +impl ResultExt for Result> { + fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult { + match self { + Ok(v) => Ok(v), + Err(_) => cx.throw_type_error(format!("expected {}", U::name())), + } + } +} + +macro_rules! from_js { + () => { + fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + Self::try_from_js(cx, v)?.or_throw(cx) + } + }; +} + +impl<'cx, V> TryFromJs<'cx> for Handle<'cx, V> +where + V: Value, +{ + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + Ok(v.downcast(cx).map_err(|_| TypeExpected::new())) + } + + from_js!(); +} + +impl<'cx, V: Value> private::Sealed for Handle<'cx, V> {} + +impl<'cx, T> TryFromJs<'cx> for Option +where + T: TryFromJs<'cx>, +{ + type Error = T::Error; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + if is_null_or_undefined(cx, v)? { + return Ok(Ok(None)); + } + + T::try_from_js(cx, v).map(|v| v.map(Some)) + } + + fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + if is_null_or_undefined(cx, v)? { + return Ok(None); + } + + T::from_js(cx, v).map(Some) + } +} + +impl<'cx, T> private::Sealed for Option where T: TryFromJs<'cx> {} + +impl<'cx> TryFromJs<'cx> for f64 { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let mut n = 0f64; + + unsafe { + match sys::get_value_double(cx.env().to_raw(), v.to_local(), &mut n) { + sys::Status::NumberExpected => return Ok(Err(TypeExpected::new())), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(Ok(n)) + } + + from_js!(); +} + +impl private::Sealed for f64 {} + +impl<'cx> TryFromJs<'cx> for bool { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let mut b = false; + + unsafe { + match sys::get_value_bool(cx.env().to_raw(), v.to_local(), &mut b) { + sys::Status::BooleanExpected => return Ok(Err(TypeExpected::new())), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(Ok(b)) + } + + from_js!(); +} + +impl private::Sealed for bool {} + +impl<'cx> TryFromJs<'cx> for String { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let env = cx.env().to_raw(); + let v = v.to_local(); + let mut len = 0usize; + + unsafe { + match sys::get_value_string_utf8(env, v, ptr::null_mut(), 0, &mut len) { + sys::Status::StringExpected => return Ok(Err(TypeExpected::new())), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + // Make room for null terminator to avoid losing a character + let mut buf = Vec::::with_capacity(len + 1); + let mut written = 0usize; + + unsafe { + assert_eq!( + sys::get_value_string_utf8( + env, + v, + buf.as_mut_ptr().cast(), + buf.capacity(), + &mut written, + ), + sys::Status::Ok, + ); + + debug_assert_eq!(len, written); + buf.set_len(len); + + Ok(Ok(String::from_utf8_unchecked(buf))) + } + } + + from_js!(); +} + +impl private::Sealed for String {} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] +#[cfg(feature = "napi-5")] +/// Extract an [`f64`] from a [`JsDate`] +pub struct Date(pub f64); + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] +#[cfg(feature = "napi-5")] +impl<'cx> TryFromJs<'cx> for Date { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let mut d = 0f64; + + unsafe { + match sys::get_date_value(cx.env().to_raw(), v.to_local(), &mut d) { + sys::Status::DateExpected => return Ok(Err(TypeExpected::new())), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(Ok(Date(d))) + } + + from_js!(); +} + +impl private::Sealed for Date {} + +impl<'cx> TryFromJs<'cx> for () { + type Error = Infallible; + + fn try_from_js( + _cx: &mut C, + _v: Handle<'cx, JsValue>, + ) -> NeonResult> + where + C: Context<'cx>, + { + Ok(Ok(())) + } + + fn from_js(_cx: &mut C, _v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + Ok(()) + } +} + +impl private::Sealed for () {} + +impl<'cx, T> TryFromJs<'cx> for Vec +where + JsTypedArray: Value, + T: Binary, +{ + type Error = TypeExpected>; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let v = match v.downcast::, _>(cx) { + Ok(v) => v, + Err(_) => return Ok(Err(Self::Error::new())), + }; + + Ok(Ok(v.as_slice(cx).to_vec())) + } + + from_js!(); +} + +impl private::Sealed for Vec +where + JsTypedArray: Value, + T: Binary, +{ +} + +/// Extract a [`Vec`] from a [`JsBuffer`] +pub struct Buffer(pub Vec); + +impl<'cx> TryFromJs<'cx> for Buffer { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let v = match v.downcast::(cx) { + Ok(v) => v, + Err(_) => return Ok(Err(Self::Error::new())), + }; + + Ok(Ok(Buffer(v.as_slice(cx).to_vec()))) + } + + from_js!(); +} + +impl private::Sealed for Buffer {} + +/// Extract a [`Vec`] from a [`JsArrayBuffer`] +pub struct ArrayBuffer(pub Vec); + +impl<'cx> TryFromJs<'cx> for ArrayBuffer { + type Error = TypeExpected; + + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> + where + C: Context<'cx>, + { + let v = match v.downcast::(cx) { + Ok(v) => v, + Err(_) => return Ok(Err(Self::Error::new())), + }; + + Ok(Ok(ArrayBuffer(v.as_slice(cx).to_vec()))) + } + + from_js!(); +} + +impl private::Sealed for ArrayBuffer {} + +fn is_null_or_undefined<'cx, C, V>(cx: &mut C, v: Handle) -> NeonResult +where + C: Context<'cx>, + V: Value, +{ + let mut ty = sys::ValueType::Object; + + unsafe { + match sys::typeof_value(cx.env().to_raw(), v.to_local(), &mut ty) { + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(matches!( + ty, + sys::ValueType::Undefined | sys::ValueType::Null, + )) +} diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index 327df09d0..73df1d312 100644 --- a/crates/neon/src/types_impl/mod.rs +++ b/crates/neon/src/types_impl/mod.rs @@ -8,6 +8,7 @@ pub mod buffer; #[cfg(feature = "napi-5")] pub(crate) mod date; pub(crate) mod error; +pub mod extract; pub mod function; pub(crate) mod promise; @@ -167,8 +168,8 @@ unsafe impl TransparentNoCopyWrapper for JsValue { } impl ValueInternal for JsValue { - fn name() -> String { - "any".to_string() + fn name() -> &'static str { + "any" } fn is_typeof(_env: Env, _other: &Other) -> bool { @@ -245,8 +246,8 @@ unsafe impl TransparentNoCopyWrapper for JsUndefined { } impl ValueInternal for JsUndefined { - fn name() -> String { - "undefined".to_string() + fn name() -> &'static str { + "undefined" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -313,8 +314,8 @@ unsafe impl TransparentNoCopyWrapper for JsNull { } impl ValueInternal for JsNull { - fn name() -> String { - "null".to_string() + fn name() -> &'static str { + "null" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -390,8 +391,8 @@ unsafe impl TransparentNoCopyWrapper for JsBoolean { } impl ValueInternal for JsBoolean { - fn name() -> String { - "boolean".to_string() + fn name() -> &'static str { + "boolean" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -465,8 +466,8 @@ unsafe impl TransparentNoCopyWrapper for JsString { } impl ValueInternal for JsString { - fn name() -> String { - "string".to_string() + fn name() -> &'static str { + "string" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -743,8 +744,8 @@ unsafe impl TransparentNoCopyWrapper for JsNumber { } impl ValueInternal for JsNumber { - fn name() -> String { - "number".to_string() + fn name() -> &'static str { + "number" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -802,8 +803,8 @@ unsafe impl TransparentNoCopyWrapper for JsObject { } impl ValueInternal for JsObject { - fn name() -> String { - "object".to_string() + fn name() -> &'static str { + "object" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -944,8 +945,8 @@ unsafe impl TransparentNoCopyWrapper for JsArray { } impl ValueInternal for JsArray { - fn name() -> String { - "Array".to_string() + fn name() -> &'static str { + "Array" } fn is_typeof(env: Env, other: &Other) -> bool { @@ -1226,8 +1227,8 @@ unsafe impl TransparentNoCopyWrapper for JsFunction { } impl ValueInternal for JsFunction { - fn name() -> String { - "function".to_string() + fn name() -> &'static str { + "function" } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/crates/neon/src/types_impl/private.rs b/crates/neon/src/types_impl/private.rs index 04eee2213..774dd0bcb 100644 --- a/crates/neon/src/types_impl/private.rs +++ b/crates/neon/src/types_impl/private.rs @@ -6,7 +6,7 @@ use crate::{ }; pub trait ValueInternal: TransparentNoCopyWrapper + 'static { - fn name() -> String; + fn name() -> &'static str; fn is_typeof(env: Env, other: &Other) -> bool; diff --git a/crates/neon/src/types_impl/promise.rs b/crates/neon/src/types_impl/promise.rs index 625d247e3..913d788ab 100644 --- a/crates/neon/src/types_impl/promise.rs +++ b/crates/neon/src/types_impl/promise.rs @@ -249,8 +249,8 @@ unsafe impl TransparentNoCopyWrapper for JsPromise { } impl ValueInternal for JsPromise { - fn name() -> String { - "Promise".to_string() + fn name() -> &'static str { + "Promise" } fn is_typeof(env: Env, other: &Other) -> bool { diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index 36439d990..b671819d6 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -17,4 +17,4 @@ tokio = { version = "1.34.0", features = ["rt-multi-thread"] } [dependencies.neon] version = "1.0.0" path = "../../crates/neon" -features = ["futures", "napi-experimental", "external-buffers"] +features = ["futures", "napi-experimental", "external-buffers", "serde"] diff --git a/test/napi/lib/extract.js b/test/napi/lib/extract.js new file mode 100644 index 000000000..7da6defef --- /dev/null +++ b/test/napi/lib/extract.js @@ -0,0 +1,68 @@ +const assert = require("assert"); + +const addon = require(".."); + +describe("Extractors", () => { + it("Single Argument", () => { + assert.strictEqual(addon.extract_single_add_one(41), 42); + }); + + it("Kitchen Sink", () => { + const symbol = Symbol("Test"); + + assert.deepStrictEqual( + addon.extract_values( + true, + 42, + undefined, + "hello", + new Date(0), + symbol, + 100, + "exists" + ), + [true, 42, "hello", new Date(0), symbol, 100, "exists"] + ); + + // Pass `null` and `undefined` for `None` + assert.deepStrictEqual( + addon.extract_values( + true, + 42, + undefined, + "hello", + new Date(0), + symbol, + null + ), + [true, 42, "hello", new Date(0), symbol, undefined, undefined] + ); + }); + + it("Buffers", () => { + const test = (TypedArray) => { + const buf = new ArrayBuffer(24); + const view = new TypedArray(buf); + + view[0] = 8; + view[1] = 16; + view[2] = 18; + + assert.strictEqual(addon.extract_buffer_sum(view), 42); + }; + + test(Uint8Array); + test(Uint16Array); + test(Uint32Array); + test(Int8Array); + test(Int16Array); + test(Int32Array); + test(Float32Array); + test(Float64Array); + }); + + it("JSON", () => { + assert.strictEqual(addon.extract_json_sum([1, 2, 3, 4]), 10); + assert.strictEqual(addon.extract_json_sum([8, 16, 18]), 42); + }); +}); diff --git a/test/napi/src/js/extract.rs b/test/napi/src/js/extract.rs new file mode 100644 index 000000000..75759c7c2 --- /dev/null +++ b/test/napi/src/js/extract.rs @@ -0,0 +1,120 @@ +use neon::{prelude::*, types::extract::*}; + +pub fn extract_values(mut cx: FunctionContext) -> JsResult { + let (boolean, number, _unit, string, Date(date), value, opt_number, opt_string): ( + bool, + f64, + (), + String, + Date, + Handle, + Option, + Option, + ) = cx.args()?; + + let arr = cx.empty_array(); + let boolean = cx.boolean(boolean); + let number = cx.number(number); + let string = cx.string(string); + let date = cx.date(date).or_throw(&mut cx)?; + + let opt_number = opt_number + .map(|n| cx.number(n).upcast::()) + .unwrap_or_else(|| cx.undefined().upcast()); + + let opt_string = opt_string + .map(|n| cx.string(n).upcast::()) + .unwrap_or_else(|| cx.undefined().upcast()); + + arr.set(&mut cx, 0, boolean)?; + arr.set(&mut cx, 1, number)?; + arr.set(&mut cx, 2, string)?; + arr.set(&mut cx, 3, date)?; + arr.set(&mut cx, 4, value)?; + arr.set(&mut cx, 5, opt_number)?; + arr.set(&mut cx, 6, opt_string)?; + + Ok(arr) +} + +pub fn extract_buffer_sum(mut cx: FunctionContext) -> JsResult { + fn sum<'cx, T>( + cx: &mut FunctionContext<'cx>, + buf: Vec, + map: impl Fn(T) -> f64, + ) -> JsResult<'cx, JsNumber> { + Ok(cx.number(buf.into_iter().map(map).sum::())) + } + + // `Float32Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n.into()); + } + + // `Float32Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n); + } + + // `Buffer` + if let Some(Buffer(buf)) = cx.args_opt()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `ArrayBuffer` + if let Some(ArrayBuffer(buf)) = cx.args_opt()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Uint8Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Uint16Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Uint32Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Uint64Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Int8Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Int16Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Int32Array` + if let Some(buf) = cx.args_opt::>()? { + return sum(&mut cx, buf, |n| n as f64); + } + + // `Int64Array` + let buf: Vec = cx.args()?; + + sum(&mut cx, buf, |n| n as f64) +} + +pub fn extract_json_sum(mut cx: FunctionContext) -> JsResult { + let Json::>(nums) = cx.args()?; + + Ok(cx.number(nums.into_iter().sum::())) +} + +pub fn extract_single_add_one(mut cx: FunctionContext) -> JsResult { + let n: f64 = cx.args()?; + + Ok(cx.number(n + 1.0)) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 4bdbe1402..8131550f5 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -12,6 +12,7 @@ mod js { pub mod coercions; pub mod date; pub mod errors; + pub mod extract; pub mod functions; pub mod futures; pub mod numbers; @@ -398,5 +399,14 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { // JsBigInt test suite cx.export_function("bigint_suite", js::bigint::bigint_suite)?; + // Extractors + cx.export_function("extract_values", js::extract::extract_values)?; + cx.export_function("extract_buffer_sum", js::extract::extract_buffer_sum)?; + cx.export_function("extract_json_sum", js::extract::extract_json_sum)?; + cx.export_function( + "extract_single_add_one", + js::extract::extract_single_add_one, + )?; + Ok(()) }