Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(neon): JsFunction::bind() and Object::prop() #1056

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cc1b061
Implement JsFunction::bind(), which creates a builder for calling a f…
dherman Jul 8, 2024
f7c05b8
prettier
dherman Jul 8, 2024
8c03fab
cargo fmt --all
dherman Jul 8, 2024
33b0c2a
Add PropOptions and `Object::prop()` for accessing object properties …
dherman Jul 9, 2024
acf0ec5
api doc formatting
dherman Jul 9, 2024
3d32a52
tweak test name for clarity
dherman Jul 9, 2024
db27665
Optimization: only do the function type check of the callee when call…
dherman Jul 12, 2024
a3f084d
rebase: {Bind,Prop}Options take Cx instead of generic C: Context
dherman Sep 8, 2024
589fabc
remove unnecessary sys::fun::call helper, and add test case for calli…
dherman Sep 9, 2024
59fa3f4
prettier
dherman Sep 9, 2024
c4fcf56
cargo fmt --all
dherman Sep 9, 2024
7ef24b5
style: move long type parameter list to where-clause
dherman Sep 9, 2024
d2c770d
more style nits (where-clauses)
dherman Sep 9, 2024
8d18e19
arg_with's closure doesn't need to require a Result, since TryIntoJs …
dherman Sep 9, 2024
b2a34c0
remove unnecessary unsafe: use .as_value() instead
dherman Sep 9, 2024
94a0c08
BindOptions::this() takes a TryIntoJs (and is therefore fallible)
dherman Sep 9, 2024
45be1f3
store object in `PropOptions` as a `JsObject` so it doesn't need a ty…
dherman Sep 10, 2024
bb4f231
avoid the double-type-check of eagerly downcasting to function when c…
dherman Sep 10, 2024
dd1ffdf
style: move the InvalidArg tag check to a match guard
dherman Sep 10, 2024
3257f01
argc can be usize now that we're not using the c++ backend
dherman Sep 11, 2024
6148d8e
`BindOptions::args_with()` and equivalent `BindOptions::args(With(...))`
dherman Sep 12, 2024
50a8832
more readable logic for impl_arguments! and impl_into_arguments! macros
dherman Sep 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 94 additions & 3 deletions crates/neon/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<JsString> {
/// # let obj: Handle<JsObject> = 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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this Clone?

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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be useful to add a to_value() helper to get this back out?

Additionally, can we add a set_with<V: TryIntoJs<'cx>(..) for similar reasons to arg_with(..)?

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<R: TryFromJs<'cx>>(&mut self) -> NeonResult<R> {
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<V: TryIntoJs<'cx>>(&mut self, v: V) -> NeonResult<bool> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we eliminate the bool? That has some odd historical context with the NAN backend and is meaningless now (AFAICT, it will always be true). Something useful might be NeonResult<&mut Self> because you could chain operations.

We could also add PropOptions::prop method that changes the key. It could make for a nice object builder API.

let o = cx.empty_object()
    .prop("a")
    .set("this is a")?
    .prop("b")
    .set("this is b")?
    .to_value();

The chaining is up to you, but at the least we should change the return to NeonResult<()>.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without chaining:

let o = cx.empty_object();
o.prop("a").set("this is a")?;
o.prop("b").set("this is b")?;
o.prop("c").set("this is c")?;

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<BindOptions<'a, 'cx>> {
let callee: Handle<JsValue> = 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to deprecate any of the other getter/setter methods? get, set and call_method_with are all redundant with prop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make them #[doc(hidden)] for now and we can file a followup issue to formally deprecate them a minor version or two down the road.

/// 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()) });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are extending the lifetime 'self to 'cx. If I understand correctly, this is safe because 'cx will always be the shortest possible lifetime of all handles. This is because any longer contexts will be "locked" with a &mut self.

In other words, this is safe because Neon invariants enforce 'self: 'cx for all handles.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should add this to a // SAFETY comment in the code.

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>(
Expand Down
24 changes: 2 additions & 22 deletions crates/neon/src/sys/fun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
29 changes: 28 additions & 1 deletion crates/neon/src/types_impl/extract/with.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -48,4 +58,21 @@ where
}
}

impl<'cx, F, O> TryIntoArgumentsInternal<'cx> for With<F, O>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, it makes a bit more sense to me for this implementation to be with the TryIntoArguments code rather than here, but if you think it's better here, I'm okay with that.

where
F: FnOnce(&mut Cx) -> O,
O: TryIntoArgumentsInternal<'cx>,
{
fn try_into_args_vec(self, cx: &mut Cx<'cx>) -> NeonResult<ArgsVec<'cx>> {
(self.0)(cx).try_into_args_vec(cx)
}
}

impl<'cx, F, O> TryIntoArguments<'cx> for With<F, O>
where
F: FnOnce(&mut Cx) -> O,
O: TryIntoArguments<'cx>,
{
}

impl<F, O> super::private::Sealed for With<F, O> where for<'cx> F: FnOnce(&mut Cx<'cx>) -> O {}
Loading