From 56e41d2c7ba6df461272411a2802af574e39ff8c Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Fri, 18 Aug 2023 10:43:35 -0400 Subject: [PATCH] feat(neon): Extractors --- crates/neon/src/context/mod.rs | 56 ++++ crates/neon/src/extract/mod.rs | 473 ++++++++++++++++++++++++++++ crates/neon/src/lib.rs | 1 + crates/neon/src/types_impl/boxed.rs | 8 + crates/neon/src/types_impl/date.rs | 2 +- 5 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 crates/neon/src/extract/mod.rs diff --git a/crates/neon/src/context/mod.rs b/crates/neon/src/context/mod.rs index a3c9b8e92..67eb69a6a 100644 --- a/crates/neon/src/context/mod.rs +++ b/crates/neon/src/context/mod.rs @@ -147,6 +147,7 @@ pub use crate::types::buffer::lock::Lock; use crate::{ event::TaskBuilder, + extract::FromArgs, handle::Handle, object::Object, result::{JsResult, NeonResult, Throw}, @@ -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`. @@ -691,6 +722,27 @@ impl<'a> FunctionContext<'a> { } } + /// Extract Rust data from the JavaScript arguments. + /// + /// This is frequently more efficient and ergonomic than getting arguments + /// individually. See the [`extract`](crate::extract) module documentation + /// for more examples. + /// + /// ``` + /// # use neon::prelude::*; + /// 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) + } + /// Produces a handle to the `this`-binding and attempts to downcast as a specific type. /// Equivalent to calling `cx.this_value().downcast_or_throw(&mut cx)`. /// @@ -703,6 +755,10 @@ impl<'a> FunctionContext<'a> { pub fn this_value(&mut self) -> Handle<'a, JsValue> { JsValue::new_internal(self.info.this(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/extract/mod.rs b/crates/neon/src/extract/mod.rs new file mode 100644 index 000000000..0a58455c7 --- /dev/null +++ b/crates/neon/src/extract/mod.rs @@ -0,0 +1,473 @@ +//! 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::*; +//! 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::*; +//! 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::*; +//! 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::*; +//! # #[cfg(feature = "napi-5")] +//! # use neon::types::JsDate; +//! # #[cfg(feature = "napi-5")] +//! use neon::extract::Date; +//! +//! # #[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 +//! custom extractors for each field, but it can also be done by attempting extraction +//! multiple times with [`cx.try_catch(..)`](Context::try_catch). +//! +//! ``` +//! # use neon::prelude::*; +//! +//! 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 Ok((a, b)) = cx.try_catch(|cx| cx.args()) { +//! 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 std::ptr; + +use crate::{ + context::{Context, FunctionContext}, + handle::{Handle, Root}, + object::Object, + result::{NeonResult, ResultExt, Throw}, + sys, + types::{ + buffer::{Binary, TypedArray}, + private::ValueInternal, + Finalize, JsArrayBuffer, JsBox, JsTypedArray, JsValue, Value, + }, +}; + +#[cfg(feature = "napi-6")] +use crate::types::{bigint::Sign, JsBigInt}; + +/// Extract Rust data from a JavaScript value +pub trait TryFromJs<'cx>: Sized { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>; +} + +impl<'cx, V> TryFromJs<'cx> for Handle<'cx, V> +where + V: Value, +{ + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw(cx) + } +} + +impl<'cx, T> TryFromJs<'cx> for Option +where + T: TryFromJs<'cx>, +{ + 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(None); + } + + T::try_from_js(cx, v).map(Some) + } +} + +impl<'cx> TryFromJs<'cx> for f64 { + 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 cx.throw_type_error("number expected"), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(n) + } +} + +impl<'cx> TryFromJs<'cx> for bool { + 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::NumberExpected => return cx.throw_type_error("bool expected"), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(b) + } +} + +impl<'cx> TryFromJs<'cx> for String { + 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 cx.throw_type_error("string expected"), + 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(String::from_utf8_unchecked(buf)) + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] +#[cfg(feature = "napi-5")] +/// Extract an [`f64`] from a [`Date`](crate::types::JsDate) +pub struct Date(pub f64); + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] +#[cfg(feature = "napi-5")] +impl<'cx> TryFromJs<'cx> for Date { + 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 cx.throw_type_error("Date expected"), + sys::Status::PendingException => return Err(Throw::new()), + status => assert_eq!(status, sys::Status::Ok), + } + } + + Ok(Date(d)) + } +} + +impl<'cx, T> TryFromJs<'cx> for Vec +where + JsTypedArray: Value, + T: Binary, +{ + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw::, _>(cx) + .map(|v| v.as_slice(cx).to_vec()) + } +} + +/// Extract a [`Vec`] from an [`ArrayBuffer`](JsArrayBuffer) +pub struct ArrayBuffer(pub Vec); + +impl<'cx> TryFromJs<'cx> for ArrayBuffer { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + let buf = v + .downcast_or_throw::(cx)? + .as_slice(cx) + .to_vec(); + + Ok(ArrayBuffer(buf)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[cfg(feature = "napi-6")] +impl<'cx> TryFromJs<'cx> for u64 { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw::(cx)? + .to_u64(cx) + .or_throw(cx) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[cfg(feature = "napi-6")] +impl<'cx> TryFromJs<'cx> for i64 { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw::(cx)? + .to_i64(cx) + .or_throw(cx) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[cfg(feature = "napi-6")] +impl<'cx> TryFromJs<'cx> for u128 { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw::(cx)? + .to_u128(cx) + .or_throw(cx) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[cfg(feature = "napi-6")] +impl<'cx> TryFromJs<'cx> for i128 { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + v.downcast_or_throw::(cx)? + .to_i128(cx) + .or_throw(cx) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[cfg(feature = "napi-6")] +/// Extract the [`Sign`] and [`u64`] words from a [`BigInt`](JsBigInt) +pub struct BigInt(pub Sign, pub Vec); + +#[cfg(feature = "napi-6")] +impl<'cx> TryFromJs<'cx> for BigInt { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + let (sign, d) = v.downcast_or_throw::(cx)?.to_digits_le(cx); + + Ok(BigInt(sign, d)) + } +} + +impl<'cx, T> TryFromJs<'cx> for &'cx T +where + T: Finalize + 'static, +{ + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + Ok(v.downcast_or_throw::, _>(cx)?.as_ref()) + } +} + +impl<'cx> TryFromJs<'cx> for () { + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + if !is_null_or_undefined(cx, v)? { + return cx.throw_type_error("expected null or undefined"); + } + + Ok(()) + } +} + +impl<'cx, V> TryFromJs<'cx> for Root +where + V: Object, +{ + fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult + where + C: Context<'cx>, + { + Ok(v.downcast_or_throw::(cx)?.root(cx)) + } +} + +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, + )) +} + +/// 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> {} + +macro_rules! impl_arguments { + ($(#[$attrs:meta])? [$($head:ident),*], []) => {}; + + ($(#[$attrs:meta])? [$($head:ident),*], [$cur:ident $(, $tail:ident)*]) => { + $(#[$attrs])? + impl<'cx, $($head,)* $cur> FromArgs<'cx> for ($($head,)* $cur,) + where + $($head: TryFromJs<'cx>,)* + $cur: TryFromJs<'cx>, + {} + + impl<'cx, $($head,)* $cur> private::FromArgsInternal<'cx> for ($($head,)* $cur,) + where + $($head: TryFromJs<'cx>,)* + $cur: TryFromJs<'cx>, + { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult { + #[allow(non_snake_case)] + let [$($head,)* $cur] = cx.argv(); + + Ok(( + $($head::try_from_js(cx, $head)?,)* + $cur::try_from_js(cx, $cur)?, + )) + } + } + + impl_arguments!($(#[$attrs])? [$($head,)* $cur], [$($tail),*]); + }; +} + +impl_arguments!([], [T1, T2, T3, T4, T5, T6, T7, T8]); +impl_arguments!( + #[doc(hidden)] + [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 FromArgsInternal<'cx>: Sized { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult; + } +} diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 818c078bc..8c3db2240 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -80,6 +80,7 @@ pub mod context; pub mod event; +pub mod extract; pub mod handle; pub mod meta; pub mod object; diff --git a/crates/neon/src/types_impl/boxed.rs b/crates/neon/src/types_impl/boxed.rs index 4306bbed5..388a1b1ad 100644 --- a/crates/neon/src/types_impl/boxed.rs +++ b/crates/neon/src/types_impl/boxed.rs @@ -259,6 +259,14 @@ impl JsBox { } } +impl<'cx, T: Finalize + 'static> Handle<'cx, JsBox> { + pub fn as_ref(&self) -> &'cx T { + // Safety: `JsBox` is only a reference into the JS heap. This value + // will live at least as long as the _handle_ and not the `JsBox`. + unsafe { &*self.0.raw_data } + } +} + impl Deref for JsBox { type Target = T; diff --git a/crates/neon/src/types_impl/date.rs b/crates/neon/src/types_impl/date.rs index 6f9706a74..a3fab6b83 100644 --- a/crates/neon/src/types_impl/date.rs +++ b/crates/neon/src/types_impl/date.rs @@ -145,7 +145,7 @@ impl JsDate { Handle::new_internal(JsDate(local)) } - /// Gets the `Date`'s value. An invalid `Date` will return [`std::f64::NAN`]. + /// Gets the `Date`'s value. An invalid `Date` will return [`f64::NAN`]. pub fn value<'a, C: Context<'a>>(&self, cx: &mut C) -> f64 { let env = cx.env().to_raw(); unsafe { sys::date::value(env, self.to_local()) }