Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions crates/neon/src/macro_internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use crate::types::extract::Json;
#[cfg(all(feature = "napi-6", feature = "futures"))]
pub use self::futures::*;

pub mod object {
pub use crate::object::wrap::{unwrap, wrap};
}

#[cfg(all(feature = "napi-6", feature = "futures"))]
mod futures;

Expand Down
2 changes: 2 additions & 0 deletions crates/neon/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ use crate::{
#[cfg(feature = "napi-6")]
use crate::{result::JsResult, types::JsArray};

pub(crate) mod wrap;

/// A property key in a JavaScript object.
pub trait PropertyKey: Copy {
unsafe fn get_from<'c, C: Context<'c>>(
Expand Down
187 changes: 187 additions & 0 deletions crates/neon/src/object/wrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::{any::Any, error, ffi::c_void, fmt, mem::MaybeUninit, ptr};

use crate::{
context::{
internal::{ContextInternal, Env},
Context, Cx,
},
handle::Handle,
object::Object,
result::{NeonResult, ResultExt, Throw},
sys,
types::Finalize,
};

type BoxAny = Box<dyn Any + 'static>;

#[derive(Debug)]
pub struct WrapError(WrapErrorType);

impl WrapError {
fn already_wrapped() -> Self {
Self(WrapErrorType::AlreadyWrapped)
}

fn not_wrapped() -> Self {
Self(WrapErrorType::NotWrapped)
}

#[cfg(feature = "napi-8")]
fn foreign_type() -> Self {
Self(WrapErrorType::ForeignType)
}
}

impl fmt::Display for WrapError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl error::Error for WrapError {}

impl<T> ResultExt<T> for Result<T, WrapError> {
fn or_throw<'cx, C>(self, cx: &mut C) -> NeonResult<T>
where
C: Context<'cx>,
{
match self {
Ok(v) => Ok(v),
Err(err) => cx.throw_error(err.to_string()),
}
}
}

#[derive(Debug)]
enum WrapErrorType {
AlreadyWrapped,
NotWrapped,
#[cfg(feature = "napi-8")]
ForeignType,
}

impl fmt::Display for WrapErrorType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::AlreadyWrapped => write!(f, "Object is already wrapped"),
Self::NotWrapped => write!(f, "Object is not wrapped"),
#[cfg(feature = "napi-8")]
Self::ForeignType => write!(f, "Object is wrapped by another addon"),
}
}
}

pub fn wrap<T, V>(cx: &mut Cx, o: Handle<V>, v: T) -> NeonResult<Result<(), WrapError>>
where
T: Finalize + 'static,
V: Object,
{
let env = cx.env().to_raw();
let o = o.to_local();
let v = Box::into_raw(Box::new(Box::new(v) as BoxAny));

// # Safety
// The `finalize` function will be called when the JavaScript object is garbage
// collected. The `data` pointer is guaranteed to be the same pointer passed when
// wrapping.
unsafe extern "C" fn finalize<T>(env: sys::Env, data: *mut c_void, _hint: *mut c_void)
where
T: Finalize + 'static,
{
let data = Box::from_raw(data.cast::<BoxAny>());
let data = *data.downcast::<T>().unwrap();
let env = Env::from(env);

Cx::with_context(env, move |mut cx| data.finalize(&mut cx));
}

// # Safety
// The `env` value was obtained from a valid `Cx` and the `o` handle has
// already been verified to be an object.
unsafe {
match sys::wrap(
env,
o,
v.cast(),
Some(finalize::<T>),
ptr::null_mut(),
ptr::null_mut(),
) {
Err(sys::Status::InvalidArg) => {
// Wrap failed, we can safely free the value
let _ = Box::from_raw(v);

return Ok(Err(WrapError::already_wrapped()));
}
Err(sys::Status::PendingException) => {
// Wrap failed, we can safely free the value
let _ = Box::from_raw(v);

return Err(Throw::new());
}
// If an unexpected error occurs, we cannot safely free the value
// because `finalize` may be called later.
res => res.unwrap(),
}

#[cfg(feature = "napi-8")]
match sys::type_tag_object(env, o, &*crate::MODULE_TAG) {
Err(sys::Status::InvalidArg) => {
sys::remove_wrap(env, o, ptr::null_mut()).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd add a comment explaining why we're removing the wrap:

  • We can only guarantee our state invariants if we own the type tag
  • If we don't own the type tag, we don't know if someone else might be using the wrap
  • So we remove our wrap to allow the owner of the type tag to do what they want with the wrap state

One more thought: since we're removing the wrap, we could actually return ownership of the value back to the caller in the Err result.


// Unwrap succeeded, we can safely free the value
let _ = Box::from_raw(v);

return Ok(Err(WrapError::foreign_type()));
}
res => res.unwrap(),
}
}

Ok(Ok(()))
}

pub fn unwrap<'cx, T, V>(cx: &mut Cx, o: Handle<'cx, V>) -> NeonResult<Result<&'cx T, WrapError>>
where
T: Finalize + 'static,
V: Object,
{
let env = cx.env().to_raw();
let o = o.to_local();

#[cfg(feature = "napi-8")]
// # Safety
// The `env` value was obtained from a valid `Cx` and the `o` handle has
// already been verified to be an object.
unsafe {
let mut is_tagged = false;

match sys::check_object_type_tag(env, o, &*crate::MODULE_TAG, &mut is_tagged) {
Err(sys::Status::PendingException) => return Err(Throw::new()),
res => res.unwrap(),
}

if !is_tagged {
return Ok(Err(WrapError::not_wrapped()));
}
}

// # Safety
// The `env` value was obtained from a valid `Cx` and the `o` handle has
// already been verified to be an object.
let data = unsafe {
let mut data = MaybeUninit::<*mut BoxAny>::uninit();

match sys::unwrap(env, o, data.as_mut_ptr().cast()) {
Err(sys::Status::PendingException) => return Err(Throw::new()),
res => res.unwrap(),
}

// # Safety
// Since `unwrap` was successful, we know this is a valid pointer. On Node-API
// versions 8 and higher, we are also guaranteed it is a `BoxAny`.
&*data.assume_init()
};

Ok(data.downcast_ref().ok_or_else(WrapError::not_wrapped))
}
13 changes: 13 additions & 0 deletions crates/neon/src/sys/bindings/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ mod napi1 {
message: *const c_char,
message_len: usize,
);

fn wrap(
env: Env,
js_object: Value,
native_object: *mut c_void,
finalize_cb: Finalize,
finalize_hint: *mut c_void,
result: *mut Ref,
) -> Status;

fn unwrap(env: Env, js_object: Value, result: *mut *mut c_void) -> Status;

fn remove_wrap(env: Env, js_object: Value, result: *mut *mut c_void) -> Status;
}
);
}
Expand Down
21 changes: 21 additions & 0 deletions test/napi/lib/boxed.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,25 @@ describe("boxed", function () {

assert.throws(() => addon.person_greet(unit), /failed to downcast/);
});

it("should be able to wrap a Rust value in an object", () => {
const msg = "Hello, World!";
const o = {};

addon.wrapString(o, msg);
assert.strictEqual(addon.unwrapString(o), msg);
});

it("should not be able to wrap an object twice", () => {
const o = {};

addon.wrapString(o, "Hello, World!");
assert.throws(() => addon.wrapString(o, "nope"), /already wrapped/);
});

it("should not be able to unwrap an object that was not wrapped", () => {
const o = {};

assert.throws(() => addon.unwrapString(o), /not wrapped/);
});
});
12 changes: 12 additions & 0 deletions test/napi/src/js/boxed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,15 @@ fn boxed_string_concat(Boxed(this): Boxed<String>, rhs: String) -> String {
fn boxed_string_repeat(_cx: &mut FunctionContext, this: Boxed<String>, n: f64) -> String {
this.0.repeat(n as usize)
}

#[neon::export]
fn wrap_string(cx: &mut Cx, o: Handle<JsObject>, s: String) -> NeonResult<()> {
neon::macro_internal::object::wrap(cx, o, s)?.or_throw(cx)
}

#[neon::export]
fn unwrap_string(cx: &mut Cx, o: Handle<JsObject>) -> NeonResult<String> {
neon::macro_internal::object::unwrap(cx, o)?
.map(String::clone)
.or_throw(cx)
}
Loading