diff --git a/crates/neon/src/object/mod.rs b/crates/neon/src/object/mod.rs index 319e2d650..796f03804 100644 --- a/crates/neon/src/object/mod.rs +++ b/crates/neon/src/object/mod.rs @@ -32,19 +32,28 @@ //! [hierarchy]: crate::types#the-javascript-type-hierarchy //! [symbol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol +use smallvec::smallvec; + use crate::{ - context::Context, + context::{internal::ContextInternal, Context, Cx}, handle::{Handle, Root}, result::{NeonResult, Throw}, sys::{self, raw}, - types::{build, function::CallOptions, utf8::Utf8, JsFunction, JsUndefined, JsValue, Value}, + types::{ + build, + extract::{TryFromJs, TryIntoJs}, + function::{BindOptions, CallOptions}, + private::ValueInternal, + utf8::Utf8, + JsFunction, JsObject, JsUndefined, JsValue, Value, + }, }; #[cfg(feature = "napi-6")] use crate::{result::JsResult, types::JsArray}; /// A property key in a JavaScript object. -pub trait PropertyKey { +pub trait PropertyKey: Copy { unsafe fn get_from<'c, C: Context<'c>>( self, cx: &mut C, @@ -134,8 +143,90 @@ impl<'a> PropertyKey for &'a str { } } +/// A builder for accessing an object property. +/// +/// The builder methods make it convenient to get and set properties +/// as well as to bind and call methods. +/// ``` +/// # use neon::prelude::*; +/// # fn foo(mut cx: FunctionContext) -> JsResult { +/// # let obj: Handle = cx.argument(0)?; +/// let x: f64 = obj +/// .prop(&mut cx, "x") +/// .get()?; +/// +/// obj.prop(&mut cx, "y") +/// .set(x)?; +/// +/// let s: String = obj.prop(&mut cx, "toString") +/// .bind()? +/// .apply()?; +/// # Ok(cx.string(s)) +/// # } +/// ``` +pub struct PropOptions<'a, 'cx, K> +where + 'cx: 'a, + K: PropertyKey, +{ + pub(crate) cx: &'a mut Cx<'cx>, + pub(crate) this: Handle<'cx, JsObject>, + pub(crate) key: K, +} + +impl<'a, 'cx, K> PropOptions<'a, 'cx, K> +where + 'cx: 'a, + K: PropertyKey, +{ + /// Gets the property from the object and attempts to convert it to a Rust value. + /// Equivalent to calling `R::from_js(cx, obj.get(cx)?)`. + /// + /// May throw an exception either during accessing the property or converting the + /// result type. + pub fn get>(&mut self) -> NeonResult { + let v = self.this.get_value(self.cx, self.key)?; + R::from_js(self.cx, v) + } + + /// Sets the property on the object to a value converted from Rust. + /// Equivalent to calling `obj.set(cx, v.try_into_js(cx)?)`. + /// + /// May throw an exception either during converting the value or setting the property. + pub fn set>(&mut self, v: V) -> NeonResult { + let v = v.try_into_js(self.cx)?; + self.this.set(self.cx, self.key, v) + } + + /// Gets the property from the object as a method and binds `this` to the object. + /// + /// May throw an exception either during accessing the property or downcasting it + /// to a function. + pub fn bind(&'a mut self) -> NeonResult> { + let callee: Handle = self.this.get(self.cx, self.key)?; + let this = Some(self.this.upcast()); + Ok(BindOptions { + cx: self.cx, + callee, + this, + args: smallvec![], + }) + } +} + /// The trait of all object types. pub trait Object: Value { + /// Create a [`PropOptions`] for accessing a property. + fn prop<'a, 'cx: 'a, K: PropertyKey>( + &self, + cx: &'a mut Cx<'cx>, + key: K, + ) -> PropOptions<'a, 'cx, K> { + let this: Handle<'_, JsObject> = + Handle::new_internal(unsafe { ValueInternal::from_local(cx.env(), self.to_local()) }); + PropOptions { cx, this, key } + } + /// Gets a property from a JavaScript object that may be `undefined` and /// attempts to downcast the value if it existed. fn get_opt<'a, V: Value, C: Context<'a>, K: PropertyKey>( diff --git a/crates/neon/src/sys/fun.rs b/crates/neon/src/sys/fun.rs index e4a709d39..9d9d461c1 100644 --- a/crates/neon/src/sys/fun.rs +++ b/crates/neon/src/sys/fun.rs @@ -83,34 +83,14 @@ where callback(env, info) } -pub unsafe fn call( - out: &mut Local, - env: Env, - fun: Local, - this: Local, - argc: i32, - argv: *const c_void, -) -> bool { - let status = napi::call_function( - env, - this, - fun, - argc as usize, - argv as *const _, - out as *mut _, - ); - - status == napi::Status::Ok -} - pub unsafe fn construct( out: &mut Local, env: Env, fun: Local, - argc: i32, + argc: usize, argv: *const c_void, ) -> bool { - let status = napi::new_instance(env, fun, argc as usize, argv as *const _, out as *mut _); + let status = napi::new_instance(env, fun, argc, argv as *const _, out as *mut _); status == napi::Status::Ok } diff --git a/crates/neon/src/types_impl/extract/with.rs b/crates/neon/src/types_impl/extract/with.rs index 0f631fe8a..a3784fd10 100644 --- a/crates/neon/src/types_impl/extract/with.rs +++ b/crates/neon/src/types_impl/extract/with.rs @@ -1,4 +1,14 @@ -use crate::{context::Cx, result::JsResult, types::extract::TryIntoJs}; +use crate::{ + context::Cx, + result::{JsResult, NeonResult}, + types::{ + extract::TryIntoJs, + function::{ + private::{ArgsVec, TryIntoArgumentsInternal}, + TryIntoArguments, + }, + }, +}; /// Wraps a closure that will be lazily evaluated when [`TryIntoJs::try_into_js`] is /// called. @@ -48,4 +58,21 @@ where } } +impl<'cx, F, O> TryIntoArgumentsInternal<'cx> for With +where + F: FnOnce(&mut Cx) -> O, + O: TryIntoArgumentsInternal<'cx>, +{ + fn try_into_args_vec(self, cx: &mut Cx<'cx>) -> NeonResult> { + (self.0)(cx).try_into_args_vec(cx) + } +} + +impl<'cx, F, O> TryIntoArguments<'cx> for With +where + F: FnOnce(&mut Cx) -> O, + O: TryIntoArguments<'cx>, +{ +} + impl super::private::Sealed for With where for<'cx> F: FnOnce(&mut Cx<'cx>) -> O {} diff --git a/crates/neon/src/types_impl/function/mod.rs b/crates/neon/src/types_impl/function/mod.rs index d5c52523e..88fb74ad2 100644 --- a/crates/neon/src/types_impl/function/mod.rs +++ b/crates/neon/src/types_impl/function/mod.rs @@ -3,15 +3,93 @@ use smallvec::smallvec; use crate::{ - context::Context, + context::{Context, Cx}, handle::Handle, object::Object, result::{JsResult, NeonResult}, - types::{JsFunction, JsObject, JsValue, Value}, + types::{ + call_local, + extract::{TryFromJs, TryIntoJs}, + private::ValueInternal, + JsFunction, JsObject, JsValue, Value, + }, }; pub(crate) mod private; +/// A builder for making a JavaScript function call like `parseInt("42")`. +/// +/// The builder methods make it convenient to assemble the call from parts: +/// ``` +/// # use neon::prelude::*; +/// # fn foo(mut cx: FunctionContext) -> JsResult { +/// # let parse_int: Handle = cx.global("parseInt")?; +/// let x: f64 = parse_int +/// .bind(&mut cx) +/// .arg("42")? +/// .apply()?; +/// # Ok(cx.number(x)) +/// # } +/// ``` +pub struct BindOptions<'a, 'cx: 'a> { + pub(crate) cx: &'a mut Cx<'cx>, + pub(crate) callee: Handle<'cx, JsValue>, + pub(crate) this: Option>, + pub(crate) args: private::ArgsVec<'cx>, +} + +impl<'a, 'cx: 'a> BindOptions<'a, 'cx> { + /// Set the value of `this` for the function call. + pub fn this>(&mut self, this: T) -> NeonResult<&mut Self> { + let v = this.try_into_js(self.cx)?; + self.this = Some(v.upcast()); + Ok(self) + } + + /// Replaces the arguments list with the given arguments. + pub fn args>(&mut self, a: A) -> NeonResult<&mut Self> { + self.args = a.try_into_args_vec(self.cx)?; + Ok(self) + } + + /// Replaces the arguments list with a list computed from a closure. + pub fn args_with(&mut self, f: F) -> NeonResult<&mut Self> + where + R: TryIntoArguments<'cx>, + F: FnOnce(&mut Cx<'cx>) -> R, + { + self.args = f(self.cx).try_into_args_vec(self.cx)?; + Ok(self) + } + + /// Add an argument to the arguments list. + pub fn arg>(&mut self, a: A) -> NeonResult<&mut Self> { + let v = a.try_into_js(self.cx)?; + self.args.push(v.upcast()); + Ok(self) + } + + /// Add an argument to the arguments list, computed from a closure. + pub fn arg_with(&mut self, f: F) -> NeonResult<&mut Self> + where + R: TryIntoJs<'cx>, + F: FnOnce(&mut Cx<'cx>) -> R, + { + let v = f(self.cx).try_into_js(self.cx)?; + self.args.push(v.upcast()); + Ok(self) + } + + /// Make the function call. If the function returns without throwing, the result value + /// is converted to a Rust value with `TryFromJs::from_js`. + pub fn apply>(&mut self) -> NeonResult { + let this = self.this.unwrap_or_else(|| self.cx.undefined().upcast()); + let v: Handle = + unsafe { call_local(self.cx, self.callee.to_local(), this, &self.args)? }; + R::from_js(self.cx, v) + } +} + /// A builder for making a JavaScript function call like `parseInt("42")`. /// /// The builder methods make it convenient to assemble the call from parts: @@ -111,6 +189,73 @@ impl<'a> ConstructOptions<'a> { } } +/// The trait for specifying values to be converted into arguments for a function call. +/// This trait is sealed and cannot be implemented by types outside of the Neon crate. +/// +/// **Note:** This trait is implemented for tuples of up to 32 JavaScript values, +/// but for the sake of brevity, only tuples up to size 8 are shown in this documentation. +pub trait TryIntoArguments<'cx>: private::TryIntoArgumentsInternal<'cx> {} + +impl<'cx> private::TryIntoArgumentsInternal<'cx> for () { + fn try_into_args_vec(self, _cx: &mut Cx<'cx>) -> NeonResult> { + Ok(smallvec![]) + } +} + +macro_rules! impl_into_arguments_expand { + { + $(#[$attrs:meta])? + [ $($prefix:ident ),* ]; + []; + } => {}; + + { + $(#[$attrs:meta])? + [ $($prefix:ident),* ]; + [ $head:ident $(, $tail:ident)* ]; + } => { + $(#[$attrs])? + impl<'cx, $($prefix: TryIntoJs<'cx> + 'cx, )* $head: TryIntoJs<'cx> + 'cx> private::TryIntoArgumentsInternal<'cx> for ($($prefix, )* $head, ) { + #[allow(non_snake_case)] + fn try_into_args_vec(self, cx: &mut Cx<'cx>) -> NeonResult> { + let ($($prefix, )* $head, ) = self; + Ok(smallvec![ $($prefix.try_into_js(cx)?.upcast(),)* $head.try_into_js(cx)?.upcast() ]) + } + } + + $(#[$attrs])? + impl<'cx, $($prefix: TryIntoJs<'cx> + 'cx, )* $head: TryIntoJs<'cx> + 'cx> TryIntoArguments<'cx> for ($($prefix, )* $head, ) {} + + impl_into_arguments_expand! { + $(#[$attrs])? + [ $($prefix, )* $head ]; + [ $($tail),* ]; + } + } +} + +macro_rules! impl_into_arguments { + { + [ $($show:ident),* ]; + [ $($hide:ident),* ]; + } => { + impl_into_arguments_expand! { []; [ $($show),* ]; } + impl_into_arguments_expand! { #[doc(hidden)] [ $($show),* ]; [ $($hide),* ]; } + } +} + +impl_into_arguments! { + // Tuples up to length 8 are included in the docs. + [V1, V2, V3, V4, V5, V6, V7, V8]; + + // Tuples up to length 32 are not included in the docs. + [ + V9, V10, V11, V12, V13, V14, V15, V16, + V17, V18, V19, V20, V21, V22, V23, V24, + V25, V26, V27, V28, V29, V30, V31, V32 + ]; +} + /// The trait for specifying arguments for a function call. This trait is sealed and cannot /// be implemented by types outside of the Neon crate. /// @@ -126,92 +271,56 @@ impl<'a> private::ArgumentsInternal<'a> for () { impl<'a> Arguments<'a> for () {} -macro_rules! impl_arguments { +macro_rules! impl_arguments_expand { { - [ $(($tprefix:ident, $vprefix:ident), )* ]; + $(#[$attrs:meta])? + [ $($prefix:ident),* ]; []; } => {}; { - [ $(($tprefix:ident, $vprefix:ident), )* ]; - [ $(#[$attr1:meta])? ($tname1:ident, $vname1:ident), $($(#[$attrs:meta])? ($tnames:ident, $vnames:ident), )* ]; + $(#[$attrs:meta])? + [ $($prefix:ident),* ]; + [ $head:ident $(, $tail:ident)* ]; } => { - $(#[$attr1])? - impl<'a, $($tprefix: Value, )* $tname1: Value> private::ArgumentsInternal<'a> for ($(Handle<'a, $tprefix>, )* Handle<'a, $tname1>, ) { + $(#[$attrs])? + impl<'a, $($prefix: Value, )* $head: Value> private::ArgumentsInternal<'a> for ($(Handle<'a, $prefix>, )* Handle<'a, $head>, ) { + #[allow(non_snake_case)] fn into_args_vec(self) -> private::ArgsVec<'a> { - let ($($vprefix, )* $vname1, ) = self; - smallvec![$($vprefix.upcast(),)* $vname1.upcast()] + let ($($prefix, )* $head, ) = self; + smallvec![$($prefix.upcast(),)* $head.upcast()] } } - $(#[$attr1])? - impl<'a, $($tprefix: Value, )* $tname1: Value> Arguments<'a> for ($(Handle<'a, $tprefix>, )* Handle<'a, $tname1>, ) {} + $(#[$attrs])? + impl<'a, $($prefix: Value, )* $head: Value> Arguments<'a> for ($(Handle<'a, $prefix>, )* Handle<'a, $head>, ) {} - impl_arguments! { - [ $(($tprefix, $vprefix), )* ($tname1, $vname1), ]; - [ $($(#[$attrs])? ($tnames, $vnames), )* ]; + impl_arguments_expand! { + $(#[$attrs])? + [ $($prefix, )* $head ]; + [ $($tail),* ]; } - }; - } + }; +} + +macro_rules! impl_arguments { + { + [ $($show:ident),* ]; + [ $($hide:ident),* ]; + } => { + impl_arguments_expand! { []; [ $($show),* ]; } + impl_arguments_expand! { #[doc(hidden)] [ $($show),* ]; [ $($hide),* ]; } + } +} impl_arguments! { - []; + // Tuples up to length 8 are included in the docs. + [V1, V2, V3, V4, V5, V6, V7, V8]; + + // Tuples up to length 32 are not included in the docs. [ - (V1, v1), - (V2, v2), - (V3, v3), - (V4, v4), - (V5, v5), - (V6, v6), - (V7, v7), - (V8, v8), - #[doc(hidden)] - (V9, v9), - #[doc(hidden)] - (V10, v10), - #[doc(hidden)] - (V11, v11), - #[doc(hidden)] - (V12, v12), - #[doc(hidden)] - (V13, v13), - #[doc(hidden)] - (V14, v14), - #[doc(hidden)] - (V15, v15), - #[doc(hidden)] - (V16, v16), - #[doc(hidden)] - (V17, v17), - #[doc(hidden)] - (V18, v18), - #[doc(hidden)] - (V19, v19), - #[doc(hidden)] - (V20, v20), - #[doc(hidden)] - (V21, v21), - #[doc(hidden)] - (V22, v22), - #[doc(hidden)] - (V23, v23), - #[doc(hidden)] - (V24, v24), - #[doc(hidden)] - (V25, v25), - #[doc(hidden)] - (V26, v26), - #[doc(hidden)] - (V27, v27), - #[doc(hidden)] - (V28, v28), - #[doc(hidden)] - (V29, v29), - #[doc(hidden)] - (V30, v30), - #[doc(hidden)] - (V31, v31), - #[doc(hidden)] - (V32, v32), + V9, V10, V11, V12, V13, V14, V15, V16, + V17, V18, V19, V20, V21, V22, V23, V24, + V25, V26, V27, V28, V29, V30, V31, V32 ]; } diff --git a/crates/neon/src/types_impl/function/private.rs b/crates/neon/src/types_impl/function/private.rs index 21a776191..c0b079ec1 100644 --- a/crates/neon/src/types_impl/function/private.rs +++ b/crates/neon/src/types_impl/function/private.rs @@ -1,9 +1,14 @@ use smallvec::SmallVec; -use crate::{handle::Handle, types::JsValue}; +use crate::{context::Cx, handle::Handle, result::NeonResult, types::JsValue}; pub type ArgsVec<'a> = SmallVec<[Handle<'a, JsValue>; 8]>; +/// This type marks the `TryIntoArguments` trait as sealed. +pub trait TryIntoArgumentsInternal<'cx> { + fn try_into_args_vec(self, cx: &mut Cx<'cx>) -> NeonResult>; +} + /// This type marks the `Arguments` trait as sealed. pub trait ArgumentsInternal<'a> { fn into_args_vec(self) -> ArgsVec<'a>; diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index 3f8677e51..f6dcbf4ff 100644 --- a/crates/neon/src/types_impl/mod.rs +++ b/crates/neon/src/types_impl/mod.rs @@ -18,22 +18,23 @@ pub(crate) mod utf8; use std::{ any, fmt::{self, Debug}, + mem::MaybeUninit, os::raw::c_void, }; use smallvec::smallvec; use crate::{ - context::{internal::Env, Context, FunctionContext}, + context::{internal::Env, Context, Cx, FunctionContext}, handle::{ internal::{SuperType, TransparentNoCopyWrapper}, Handle, }, object::Object, result::{JsResult, NeonResult, ResultExt, Throw}, - sys::{self, raw}, + sys::{self, bindings as napi, raw}, types::{ - function::{CallOptions, ConstructOptions}, + function::{BindOptions, CallOptions, ConstructOptions}, private::ValueInternal, utf8::Utf8, }, @@ -1050,7 +1051,7 @@ const V8_ARGC_LIMIT: usize = 65535; unsafe fn prepare_call<'a, 'b, C: Context<'a>>( cx: &mut C, args: &[Handle<'b, JsValue>], -) -> NeonResult<(i32, *const c_void)> { +) -> NeonResult<(usize, *const c_void)> { // Note: This cast is only save because `Handle<'_, JsValue>` is // guaranteed to have the same layout as a pointer because `Handle` // and `JsValue` are both `repr(C)` newtypes. @@ -1059,7 +1060,7 @@ unsafe fn prepare_call<'a, 'b, C: Context<'a>>( if argc > V8_ARGC_LIMIT { return cx.throw_range_error("too many arguments"); } - Ok((argc as i32, argv)) + Ok((argc, argv)) } impl JsFunction { @@ -1154,6 +1155,47 @@ impl JsFunction { } } +pub(crate) unsafe fn call_local<'a, 'b, C: Context<'a>, T, AS>( + cx: &mut C, + callee: raw::Local, + this: Handle<'b, T>, + args: AS, +) -> JsResult<'a, JsValue> +where + T: Value, + AS: AsRef<[Handle<'b, JsValue>]>, +{ + let (argc, argv) = unsafe { prepare_call(cx, args.as_ref()) }?; + let env = cx.env(); + let mut result: MaybeUninit = MaybeUninit::zeroed(); + + let status = napi::call_function( + env.to_raw(), + this.to_local(), + callee, + argc, + argv.cast(), + result.as_mut_ptr(), + ); + + match status { + sys::Status::InvalidArg if !sys::tag::is_function(env.to_raw(), callee) => { + return cx.throw_error("not a function"); + } + sys::Status::PendingException => { + return Err(Throw::new()); + } + status => { + assert_eq!(status, sys::Status::Ok); + } + } + + Ok(Handle::new_internal(JsValue::from_local( + env, + result.assume_init(), + ))) +} + impl JsFunction { /// Calls this function. /// @@ -1168,11 +1210,7 @@ impl JsFunction { T: Value, AS: AsRef<[Handle<'b, JsValue>]>, { - let (argc, argv) = unsafe { prepare_call(cx, args.as_ref()) }?; - let env = cx.env().to_raw(); - build(cx.env(), |out| unsafe { - sys::fun::call(out, env, self.to_local(), this.to_local(), argc, argv) - }) + unsafe { call_local(cx, self.to_local(), this, args) } } /// Calls this function for side effect, discarding its result. @@ -1211,6 +1249,18 @@ impl JsFunction { } } +impl JsFunction { + pub fn bind<'a, 'cx: 'a>(&self, cx: &'a mut Cx<'cx>) -> BindOptions<'a, 'cx> { + let callee = self.as_value(cx); + BindOptions { + cx, + callee, + this: None, + args: smallvec![], + } + } +} + impl JsFunction { /// Create a [`CallOptions`](function::CallOptions) for calling this function. pub fn call_with<'a, C: Context<'a>>(&self, _cx: &C) -> CallOptions<'a> { diff --git a/package-lock.json b/package-lock.json index 2324603d5..1336ba367 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6056,7 +6056,7 @@ } }, "pkgs/cargo-cp-artifact": { - "version": "0.1.8", + "version": "0.1.9", "license": "MIT", "bin": { "cargo-cp-artifact": "bin/cargo-cp-artifact.js" @@ -6066,7 +6066,7 @@ } }, "pkgs/create-neon": { - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "@neon-rs/manifest": "^0.1.0", @@ -6113,7 +6113,7 @@ "license": "MIT", "devDependencies": { "@playwright/test": "^1.40.1", - "cargo-cp-artifact": "^0.1.8", + "cargo-cp-artifact": "^0.1.9", "electron": "^27.1.3", "playwright": "^1.40.1" } @@ -6124,7 +6124,7 @@ "hasInstallScript": true, "license": "MIT", "devDependencies": { - "cargo-cp-artifact": "^0.1.8", + "cargo-cp-artifact": "^0.1.9", "chai": "^4.3.10", "mocha": "^10.2.0" } diff --git a/test/napi/lib/functions.js b/test/napi/lib/functions.js index bf5077c97..3efc13ecb 100644 --- a/test/napi/lib/functions.js +++ b/test/napi/lib/functions.js @@ -1,6 +1,21 @@ var addon = require(".."); var assert = require("chai").assert; +const STRICT = function () { + "use strict"; + return this; +}; +const SLOPPY = Function("return this;"); + +function isStrict(f) { + try { + f.caller; + return false; + } catch (e) { + return true; + } +} + describe("JsFunction", function () { it("return a JsFunction built in Rust", function () { assert.isFunction(addon.return_js_function()); @@ -28,6 +43,66 @@ describe("JsFunction", function () { ); }); + it("call a JsFunction built in JS with .bind().apply()", function () { + assert.equal( + addon.call_js_function_with_bind(function (a, b, c, d, e) { + return a * b * c * d * e; + }), + 1 * 2 * 3 * 4 * 5 + ); + }); + + it("call a JsFunction build in JS with .bind and .args_with", function () { + assert.equal( + addon.call_js_function_with_bind_and_args_with(function (a, b, c) { + return a + b + c; + }), + 1 + 2 + 3 + ); + }); + + it("call a JsFunction build in JS with .bind and .args and With", function () { + assert.equal( + addon.call_js_function_with_bind_and_args_and_with(function (a, b, c) { + return a + b + c; + }), + 1 + 2 + 3 + ); + }); + + it("call parseInt with .bind().apply()", function () { + assert.equal(addon.call_parse_int_with_bind(), 42); + }); + + it("bind a JsFunction to an object", function () { + const result = addon.bind_js_function_to_object(function () { + return this.prop; + }); + + assert.equal(result, 42); + }); + + it("bind a strict JsFunction to a number", function () { + assert.isTrue(isStrict(STRICT)); + + // strict mode functions are allowed to have a primitive this binding + const result = addon.bind_js_function_to_number(STRICT); + + assert.strictEqual(result, 42); + }); + + it("bind a sloppy JsFunction to a primitive", function () { + assert.isFalse(isStrict(SLOPPY)); + + // legacy JS functions (aka "sloppy mode") replace primitive this bindings + // with object wrappers, so 42 will get wrapped as new Number(42) + const result = addon.bind_js_function_to_number(SLOPPY); + + assert.instanceOf(result, Number); + assert.strictEqual(typeof result, "object"); + assert.strictEqual(result.valueOf(), 42); + }); + it("call a JsFunction with zero args", function () { assert.equal(addon.call_js_function_with_zero_args(), -Infinity); }); diff --git a/test/napi/lib/objects.js b/test/napi/lib/objects.js index 705fde8b7..1fe346862 100644 --- a/test/napi/lib/objects.js +++ b/test/napi/lib/objects.js @@ -124,4 +124,47 @@ describe("JsObject", function () { assert.strictEqual(addon.call_symbol_method(obj, sym), "hello"); }); + + it("extracts an object property with .prop()", function () { + const obj = { number: 3.141593 }; + + assert.strictEqual(addon.get_property_with_prop(obj), 3.141593); + }); + + it("sets an object property with .prop()", function () { + const obj = { number: 3.141593 }; + + addon.set_property_with_prop(obj); + + assert.strictEqual(obj.number, 42); + }); + + it("calls a method with .prop()", function () { + const obj = { + name: "Diana Prince", + setName(name) { + this.name = name; + }, + toString() { + return `[object ${this.name}]`; + }, + }; + + assert.strictEqual(obj.toString(), "[object Diana Prince]"); + assert.strictEqual( + addon.call_methods_with_prop(obj), + "[object Wonder Woman]" + ); + assert.strictEqual(obj.toString(), "[object Wonder Woman]"); + }); + + it("throws a TypeError when calling a non-method with .prop()", function () { + const obj = { + number: 42, + }; + + assert.throws(() => { + addon.call_non_method_with_prop(obj); + }, /not a function/); + }); }); diff --git a/test/napi/src/js/functions.rs b/test/napi/src/js/functions.rs index 042d0de9b..5ac3135eb 100644 --- a/test/napi/src/js/functions.rs +++ b/test/napi/src/js/functions.rs @@ -1,4 +1,4 @@ -use neon::prelude::*; +use neon::{prelude::*, types::extract::With}; fn add1(mut cx: FunctionContext) -> JsResult { let x = cx.argument::(0)?.value(&mut cx); @@ -26,6 +26,53 @@ pub fn call_js_function_idiomatically(mut cx: FunctionContext) -> JsResult JsResult { + let n: f64 = cx + .argument::(0)? + .bind(&mut cx) + .args((1, 2, 3))? + .arg(4)? + .arg_with(|cx| cx.number(5))? + .apply()?; + Ok(cx.number(n)) +} + +pub fn call_js_function_with_bind_and_args_with(mut cx: FunctionContext) -> JsResult { + let n: f64 = cx + .argument::(0)? + .bind(&mut cx) + .args_with(|cx| (1, 2, 3))? + .apply()?; + Ok(cx.number(n)) +} + +pub fn call_js_function_with_bind_and_args_and_with(mut cx: FunctionContext) -> JsResult { + let n: f64 = cx + .argument::(0)? + .bind(&mut cx) + .args(With(|_| (1, 2, 3)))? + .apply()?; + Ok(cx.number(n)) +} + +pub fn call_parse_int_with_bind(mut cx: FunctionContext) -> JsResult { + let parse_int: Handle = cx.global("parseInt")?; + let x: f64 = parse_int.bind(&mut cx).arg("41")?.apply()?; + Ok(cx.number(x + 1.0)) +} + +pub fn bind_js_function_to_object(mut cx: FunctionContext) -> JsResult { + let f = cx.argument::(0)?; + let obj = cx.empty_object(); + obj.prop(&mut cx, "prop").set(42)?; + f.bind(&mut cx).this(obj)?.apply() +} + +pub fn bind_js_function_to_number(mut cx: FunctionContext) -> JsResult { + let f = cx.argument::(0)?; + f.bind(&mut cx).this(42)?.apply() +} + fn get_math_max<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsFunction> { let math: Handle = cx.global("Math")?; let max: Handle = math.get(cx, "max")?; diff --git a/test/napi/src/js/objects.rs b/test/napi/src/js/objects.rs index 5e9013b6e..a4b1cd4ff 100644 --- a/test/napi/src/js/objects.rs +++ b/test/napi/src/js/objects.rs @@ -95,3 +95,30 @@ pub fn call_symbol_method(mut cx: FunctionContext) -> JsResult { let sym: Handle = cx.argument::(1)?; obj.call_method_with(&mut cx, sym)?.apply(&mut cx) } + +pub fn get_property_with_prop(mut cx: FunctionContext) -> JsResult { + let obj: Handle = cx.argument::(0)?; + let n: f64 = obj.prop(&mut cx, "number").get()?; + Ok(cx.number(n)) +} + +pub fn set_property_with_prop(mut cx: FunctionContext) -> JsResult { + let obj: Handle = cx.argument::(0)?; + let b = obj.prop(&mut cx, "number").set(42)?; + Ok(cx.boolean(b)) +} + +pub fn call_methods_with_prop(mut cx: FunctionContext) -> JsResult { + let obj: Handle = cx.argument::(0)?; + obj.prop(&mut cx, "setName") + .bind()? + .arg("Wonder Woman")? + .apply()?; + obj.prop(&mut cx, "toString").bind()?.apply() +} + +pub fn call_non_method_with_prop(mut cx: FunctionContext) -> JsResult { + let obj: Handle = cx.argument::(0)?; + obj.prop(&mut cx, "number").bind()?.apply()?; + Ok(cx.undefined()) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 627b3f80b..f815a266c 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -148,6 +148,18 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { "call_js_function_idiomatically", call_js_function_idiomatically, )?; + cx.export_function("call_js_function_with_bind", call_js_function_with_bind)?; + cx.export_function( + "call_js_function_with_bind_and_args_with", + call_js_function_with_bind_and_args_with, + )?; + cx.export_function( + "call_js_function_with_bind_and_args_and_with", + call_js_function_with_bind_and_args_and_with, + )?; + cx.export_function("call_parse_int_with_bind", call_parse_int_with_bind)?; + cx.export_function("bind_js_function_to_object", bind_js_function_to_object)?; + cx.export_function("bind_js_function_to_number", bind_js_function_to_number)?; cx.export_function( "call_js_function_with_zero_args", call_js_function_with_zero_args, @@ -291,6 +303,10 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("call_nullary_method", call_nullary_method)?; cx.export_function("call_unary_method", call_unary_method)?; cx.export_function("call_symbol_method", call_symbol_method)?; + cx.export_function("get_property_with_prop", get_property_with_prop)?; + cx.export_function("set_property_with_prop", set_property_with_prop)?; + cx.export_function("call_methods_with_prop", call_methods_with_prop)?; + cx.export_function("call_non_method_with_prop", call_non_method_with_prop)?; cx.export_function("create_date", create_date)?; cx.export_function("get_date_value", get_date_value)?;