diff --git a/Cargo.toml b/Cargo.toml index 5c09b2958..a36df9b90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,10 @@ event-queue-api = [] # Feature flag to include procedural macros proc-macros = ["neon-macros"] +# Feature flag to enable the `JsSymbol` API of RFC 38 +# https://github.com/neon-bindings/rfcs/pull/38 +symbol-api = [] + [package.metadata.docs.rs] features = ["docs-only", "event-handler-api", "proc-macros", "try-catch-api"] diff --git a/crates/neon-runtime/src/napi/bindings/functions.rs b/crates/neon-runtime/src/napi/bindings/functions.rs index b8d29b16e..af2c056c2 100644 --- a/crates/neon-runtime/src/napi/bindings/functions.rs +++ b/crates/neon-runtime/src/napi/bindings/functions.rs @@ -151,6 +151,13 @@ mod napi1 { fn get_property(env: Env, object: Value, key: Value, result: *mut Value) -> Status; + fn get_named_property( + env: Env, + object: Value, + utf8name: *const c_char, + result: *mut Value, + ) -> Status; + fn set_element(env: Env, object: Value, index: u32, value: Value) -> Status; fn get_element(env: Env, object: Value, index: u32, result: *mut Value) -> Status; @@ -198,6 +205,8 @@ mod napi1 { ) -> Status; fn run_script(env: Env, script: Value, result: *mut Value) -> Status; + + fn create_symbol(env: Env, description: Value, result: *mut Value) -> Status; } ); } diff --git a/crates/neon-runtime/src/napi/object.rs b/crates/neon-runtime/src/napi/object.rs index b5e96953a..305e2c320 100644 --- a/crates/neon-runtime/src/napi/object.rs +++ b/crates/neon-runtime/src/napi/object.rs @@ -1,7 +1,7 @@ -use std::mem::MaybeUninit; - use crate::napi::bindings as napi; use crate::raw::{Env, Local}; +use std::mem::MaybeUninit; +use std::os::raw::c_char; /// Mutates the `out` argument to refer to a `napi_value` containing a newly created JavaScript Object. pub unsafe fn new(out: &mut Local, env: Env) { @@ -131,3 +131,16 @@ pub unsafe fn set(out: &mut bool, env: Env, object: Local, key: Local, val: Loca *out } + +/// Returns an `Option` representing the value of the property of `object` named by +/// the `key` value, where `key` is a pointer to a null-terminated byte string. Returns `None` if +/// the value couldn't be retrieved. +pub unsafe fn get_named(env: Env, object: Local, key: *const u8) -> Option { + let mut local = MaybeUninit::uninit(); + if napi::get_named_property(env, object, key as *const _, local.as_mut_ptr()) + != napi::Status::Ok + { + return None; + } + Some(local.assume_init()) +} diff --git a/crates/neon-runtime/src/napi/primitive.rs b/crates/neon-runtime/src/napi/primitive.rs index 2df6f19ee..8fea1f812 100644 --- a/crates/neon-runtime/src/napi/primitive.rs +++ b/crates/neon-runtime/src/napi/primitive.rs @@ -1,5 +1,6 @@ use crate::napi::bindings as napi; use crate::raw::{Env, Local}; +use std::mem::MaybeUninit; /// Mutates the `out` argument provided to refer to the global `undefined` object. pub unsafe fn undefined(out: &mut Local, env: Env) { @@ -43,3 +44,14 @@ pub unsafe fn number_value(env: Env, p: Local) -> f64 { ); value } + +/// Returns a newly created `Local` containing a JavaScript symbol. +/// Panics if `desc` is not a `Local` representing a `ValueType::String` or a null pointer. +pub unsafe fn symbol(env: Env, desc: Local) -> Local { + let mut local = MaybeUninit::uninit(); + assert_eq!( + napi::create_symbol(env, desc, local.as_mut_ptr()), + napi::Status::Ok + ); + local.assume_init() +} diff --git a/crates/neon-runtime/src/napi/tag.rs b/crates/neon-runtime/src/napi/tag.rs index d0cd048b3..25bbd9988 100644 --- a/crates/neon-runtime/src/napi/tag.rs +++ b/crates/neon-runtime/src/napi/tag.rs @@ -34,6 +34,11 @@ pub unsafe fn is_string(env: Env, val: Local) -> bool { is_type(env, val, napi::ValueType::String) } +/// Is `val` a JavaScript symbol? +pub unsafe fn is_symbol(env: Env, val: Local) -> bool { + is_type(env, val, napi::ValueType::Symbol) +} + pub unsafe fn is_object(env: Env, val: Local) -> bool { is_type(env, val, napi::ValueType::Object) } diff --git a/src/context/mod.rs b/src/context/mod.rs index 325c005a4..cfe6ba3bd 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -164,6 +164,8 @@ use crate::types::boxed::{Finalize, JsBox}; #[cfg(feature = "napi-5")] use crate::types::date::{DateError, JsDate}; use crate::types::error::JsError; +#[cfg(all(feature = "napi-1", feature = "symbol-api"))] +use crate::types::symbol::JsSymbol; use crate::types::{ JsArray, JsBoolean, JsFunction, JsNull, JsNumber, JsObject, JsString, JsUndefined, JsValue, StringResult, Value, @@ -438,6 +440,15 @@ pub trait Context<'a>: ContextInternal<'a> { JsString::try_new(self, s) } + /// Convenience method for creating a `JsSymbol` value. + /// + /// If the string exceeds the limits of the JS engine, this method panics. + #[cfg(all(feature = "napi-1", feature = "symbol-api"))] + fn symbol>(&mut self, description: S) -> Handle<'a, JsSymbol> { + let desc = self.string(description); + JsSymbol::with_description(self, desc) + } + /// Convenience method for creating a `JsNull` value. fn null(&mut self) -> Handle<'a, JsNull> { #[cfg(feature = "legacy-runtime")] diff --git a/src/prelude.rs b/src/prelude.rs index ee29de95d..13decee8a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -22,6 +22,8 @@ pub use crate::register_module; pub use crate::result::{JsResult, JsResultExt, NeonResult}; #[cfg(feature = "legacy-runtime")] pub use crate::task::Task; +#[cfg(all(feature = "napi-1", feature = "symbol-api"))] +pub use crate::types::symbol::JsSymbol; pub use crate::types::{ BinaryData, JsArray, JsArrayBuffer, JsBoolean, JsBuffer, JsError, JsFunction, JsNull, JsNumber, JsObject, JsString, JsUndefined, JsValue, Value, diff --git a/src/types/mod.rs b/src/types/mod.rs index 0bfa4c219..eed2c000c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -67,7 +67,8 @@ //! of custom objects that own Rust data structures. //! - **Primitive types:** These are the built-in JavaScript datatypes that are not //! object types: [`JsNumber`](JsNumber), [`JsBoolean`](JsBoolean), -//! [`JsString`](JsString), [`JsNull`](JsNull), and [`JsUndefined`](JsUndefined). +//! [`JsString`](JsString), [`JsNull`](JsNull), [`JsSymbol`](JsSymbol), +//! and [`JsUndefined`](JsUndefined). //! //! [types]: https://raw.githubusercontent.com/neon-bindings/neon/main/doc/types.jpg //! [unknown]: https://mariusschulz.com/blog/the-unknown-type-in-typescript#the-unknown-type @@ -80,6 +81,8 @@ pub(crate) mod date; pub(crate) mod error; pub(crate) mod internal; +#[cfg(all(feature = "napi-1", feature = "symbol-api"))] +pub(crate) mod symbol; pub(crate) mod utf8; use self::internal::{FunctionCallback, ValueInternal}; @@ -105,6 +108,8 @@ pub use self::boxed::JsBox; #[cfg(feature = "napi-5")] pub use self::date::{DateError, DateErrorKind, JsDate}; pub use self::error::JsError; +#[cfg(all(feature = "napi-1", feature = "symbol-api"))] +pub use self::symbol::JsSymbol; pub(crate) fn build<'a, T: Managed, F: FnOnce(&mut raw::Local) -> bool>( env: Env, diff --git a/src/types/symbol.rs b/src/types/symbol.rs new file mode 100644 index 000000000..03aa5760e --- /dev/null +++ b/src/types/symbol.rs @@ -0,0 +1,81 @@ +use crate::context::Context; +use crate::handle::{Handle, Managed}; +use crate::types::internal::ValueInternal; +use crate::types::{Env, JsString, Value}; + +use neon_runtime::raw; + +/// A JavaScript symbol primitive value. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct JsSymbol(raw::Local); + +impl JsSymbol { + /// Create a new symbol. + /// Equivalent to calling `Symbol()` in JavaScript + pub fn new<'a, C: Context<'a>>(cx: &mut C) -> Handle<'a, JsSymbol> { + JsSymbol::new_internal(cx.env(), None) + } + + /// Create a new symbol with a description. + /// Equivalent to calling `Symbol(description)` in JavaScript + pub fn with_description<'a, C: Context<'a>>( + cx: &mut C, + desc: Handle<'a, JsString>, + ) -> Handle<'a, JsSymbol> { + JsSymbol::new_internal(cx.env(), Some(desc)) + } + + /// Get the optional symbol description, where `None` represents an undefined description. + pub fn description<'a, C: Context<'a>>(self, cx: &mut C) -> Option> { + let env = cx.env().to_raw(); + const DESCRIPTION_KEY: &[u8] = b"description\0"; + unsafe { + neon_runtime::object::get_named(env, self.to_raw(), DESCRIPTION_KEY.as_ptr()).and_then( + |local| { + if neon_runtime::tag::is_string(env, local) { + Some(Handle::new_internal(JsString(local))) + } else { + None + } + }, + ) + } + } + + pub(crate) fn new_internal<'a>( + env: Env, + desc: Option>, + ) -> Handle<'a, JsSymbol> { + unsafe { + let desc_local = match desc { + None => std::ptr::null_mut(), + Some(h) => h.to_raw(), + }; + let sym_local = neon_runtime::primitive::symbol(env.to_raw(), desc_local); + Handle::new_internal(JsSymbol(sym_local)) + } + } +} + +impl Value for JsSymbol {} + +impl Managed for JsSymbol { + fn to_raw(self) -> raw::Local { + self.0 + } + + fn from_raw(_: Env, h: raw::Local) -> Self { + JsSymbol(h) + } +} + +impl ValueInternal for JsSymbol { + fn name() -> String { + "symbol".to_string() + } + + fn is_typeof(env: Env, other: Other) -> bool { + unsafe { neon_runtime::tag::is_symbol(env.to_raw(), other.to_raw()) } + } +} diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index 0981e244d..a8501bd63 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] version = "*" path = "../.." default-features = false -features = ["default-panic-hook", "napi-6", "try-catch-api", "event-queue-api"] +features = ["default-panic-hook", "napi-6", "try-catch-api", "event-queue-api", "symbol-api"] diff --git a/test/napi/lib/objects.js b/test/napi/lib/objects.js index 18d11f77e..265884709 100644 --- a/test/napi/lib/objects.js +++ b/test/napi/lib/objects.js @@ -22,6 +22,15 @@ describe('JsObject', function() { assert.deepEqual({number: 9000, string: 'hello node'}, addon.return_js_object_with_mixed_content()); }); + it('return a JsObject with a symbol property key', function () { + const obj = addon.return_js_object_with_symbol_property_key(); + const propertySymbols = Object.getOwnPropertySymbols(obj); + assert.equal(propertySymbols.length, 1); + const [sym] = propertySymbols; + assert.typeOf(sym, "symbol"); + assert.equal(obj[sym], sym); + }) + it('gets a 16-byte, zeroed ArrayBuffer', function() { var b = addon.return_array_buffer(); assert.equal(b.byteLength, 16); diff --git a/test/napi/lib/symbols.js b/test/napi/lib/symbols.js new file mode 100644 index 000000000..2fb8f6430 --- /dev/null +++ b/test/napi/lib/symbols.js @@ -0,0 +1,42 @@ +const addon = require('..'); +const { assert } = require('chai'); + +describe('JsSymbol', function () { + it('should return a JsSymbol with a description built with the context helper in Rust', function () { + const sym = addon.return_js_symbol_from_context_helper(); + assert.typeOf(sym, 'symbol'); + assert.equal(sym.description, 'neon:context_helper'); + }); + + it('should return a JsSymbol with a description built in Rust', function () { + const description = 'neon:description' + const sym = addon.return_js_symbol_with_description(description); + assert.typeOf(sym, 'symbol'); + assert.equal(sym.description, description); + }); + + it('should return a JsSymbol without a description built in Rust', function () { + const sym = addon.return_js_symbol(); + assert.typeOf(sym, 'symbol'); + assert.equal(sym.description, undefined); + }); + + it('should read the description property in Rust', function () { + const sym = Symbol('neon:description'); + const description = addon.read_js_symbol_description(sym); + assert.equal(description, 'neon:description'); + }); + + it('should read an undefined description property in Rust', function () { + const sym = Symbol(); + const description = addon.read_js_symbol_description(sym); + assert.equal(description, undefined); + }); + + it('accepts and returns symbols', function () { + const symDesc = Symbol('neon:description'); + const symNoDesc = Symbol(); + assert.equal(addon.accept_and_return_js_symbol(symDesc), symDesc); + assert.equal(addon.accept_and_return_js_symbol(symNoDesc), symNoDesc); + }); +}); diff --git a/test/napi/lib/types.js b/test/napi/lib/types.js index 7b963840b..18380c316 100644 --- a/test/napi/lib/types.js +++ b/test/napi/lib/types.js @@ -70,6 +70,15 @@ describe('type checks', function() { assert(!addon.is_string(new String('1'))); }); + it('is_symbol', function () { + assert(addon.is_symbol(Symbol())); + assert(addon.is_symbol(Symbol("unique symbol"))); + assert(addon.is_symbol(Symbol.for('neon:description'))); + assert(addon.is_symbol(Symbol.iterator)); + assert(!addon.is_symbol(undefined)); + assert(!addon.is_symbol("anything other than symbol")); + }); + it('is_undefined', function () { assert(addon.is_undefined(undefined)); assert(!addon.is_undefined(null)); @@ -84,5 +93,9 @@ describe('type checks', function() { assert(addon.strict_equals(o1, o1)); assert(!addon.strict_equals(o1, o2)); assert(!addon.strict_equals(o1, 17)); + let s1 = Symbol(); + let s2 = Symbol(); + assert(addon.strict_equals(s1, s1)); + assert(!addon.strict_equals(s1, s2)); }); }); diff --git a/test/napi/src/js/objects.rs b/test/napi/src/js/objects.rs index dbb4db792..458cfdcb3 100644 --- a/test/napi/src/js/objects.rs +++ b/test/napi/src/js/objects.rs @@ -31,6 +31,13 @@ pub fn return_js_object_with_string(mut cx: FunctionContext) -> JsResult JsResult { + let js_object = cx.empty_object(); + let s = cx.symbol("neon:description"); + js_object.set(&mut cx, s, s)?; + Ok(js_object) +} + pub fn return_array_buffer(mut cx: FunctionContext) -> JsResult { let b: Handle = cx.array_buffer(16)?; Ok(b) diff --git a/test/napi/src/js/symbols.rs b/test/napi/src/js/symbols.rs new file mode 100644 index 000000000..1aebdba81 --- /dev/null +++ b/test/napi/src/js/symbols.rs @@ -0,0 +1,27 @@ +use neon::prelude::*; + +pub fn return_js_symbol_from_context_helper(mut cx: FunctionContext) -> JsResult { + Ok(cx.symbol("neon:context_helper")) +} + +pub fn return_js_symbol_with_description(mut cx: FunctionContext) -> JsResult { + let description: Handle = cx.argument(0)?; + Ok(JsSymbol::with_description(&mut cx, description)) +} + +pub fn return_js_symbol(mut cx: FunctionContext) -> JsResult { + Ok(JsSymbol::new(&mut cx)) +} + +pub fn read_js_symbol_description(mut cx: FunctionContext) -> JsResult { + let symbol: Handle = cx.argument(0)?; + symbol + .description(&mut cx) + .map(|v| Ok(v.upcast())) + .unwrap_or_else(|| Ok(cx.undefined().upcast())) +} + +pub fn accept_and_return_js_symbol(mut cx: FunctionContext) -> JsResult { + let sym: Handle = cx.argument(0)?; + Ok(sym) +} diff --git a/test/napi/src/js/types.rs b/test/napi/src/js/types.rs index 9c75fa05a..1dd1bcbcc 100644 --- a/test/napi/src/js/types.rs +++ b/test/napi/src/js/types.rs @@ -54,6 +54,12 @@ pub fn is_object(mut cx: FunctionContext) -> JsResult { Ok(cx.boolean(result)) } +pub fn is_symbol(mut cx: FunctionContext) -> JsResult { + let val: Handle = cx.argument(0)?; + let result = val.is_a::(&mut cx); + Ok(cx.boolean(result)) +} + pub fn is_undefined(mut cx: FunctionContext) -> JsResult { let val: Handle = cx.argument(0)?; let is_string = val.is_a::(&mut cx); diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index f081c07a1..86e148c53 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -10,6 +10,7 @@ mod js { pub mod numbers; pub mod objects; pub mod strings; + pub mod symbols; pub mod threads; pub mod types; } @@ -23,6 +24,7 @@ use js::functions::*; use js::numbers::*; use js::objects::*; use js::strings::*; +use js::symbols::*; use js::threads::*; use js::types::*; @@ -164,6 +166,10 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("return_js_object", return_js_object)?; cx.export_function("return_js_object_with_number", return_js_object_with_number)?; cx.export_function("return_js_object_with_string", return_js_object_with_string)?; + cx.export_function( + "return_js_object_with_symbol_property_key", + return_js_object_with_symbol_property_key, + )?; cx.export_function( "return_js_object_with_mixed_content", return_js_object_with_mixed_content, @@ -218,6 +224,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("is_number", is_number)?; cx.export_function("is_object", is_object)?; cx.export_function("is_string", is_string)?; + cx.export_function("is_symbol", is_symbol)?; cx.export_function("is_undefined", is_undefined)?; cx.export_function("strict_equals", strict_equals)?; @@ -258,5 +265,17 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("leak_channel", leak_channel)?; cx.export_function("drop_global_queue", drop_global_queue)?; + cx.export_function( + "return_js_symbol_from_context_helper", + return_js_symbol_from_context_helper, + )?; + cx.export_function( + "return_js_symbol_with_description", + return_js_symbol_with_description, + )?; + cx.export_function("return_js_symbol", return_js_symbol)?; + cx.export_function("read_js_symbol_description", read_js_symbol_description)?; + cx.export_function("accept_and_return_js_symbol", accept_and_return_js_symbol)?; + Ok(()) }