diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index dd9077321..66c38722c 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -23,6 +23,7 @@ nodejs-sys = "0.13.0" libloading = "0.7.3" semver = "0.9.0" smallvec = "1.4.2" +once_cell = "1.10.0" neon-macros = { version = "=0.10.1", path = "../neon-macros" } [features] diff --git a/crates/neon/src/handle/root.rs b/crates/neon/src/handle/root.rs index 922c2c3bf..d241638e3 100644 --- a/crates/neon/src/handle/root.rs +++ b/crates/neon/src/handle/root.rs @@ -144,7 +144,12 @@ impl Root { } } - /// Return the referenced JavaScript object and allow it to be garbage collected + /// Return the referenced JavaScript object and allow it to be garbage collected. + /// + /// # Panics + /// + /// This method panics if it is called from a different JavaScript thread than the + /// one in which the handle was created. pub fn into_inner<'a, C: Context<'a>>(self, cx: &mut C) -> Handle<'a, T> { let env = cx.env(); let internal = self.into_napi_ref(cx); @@ -160,6 +165,11 @@ impl Root { /// Access the inner JavaScript object without consuming the `Root` /// This method aliases the reference without changing the reference count. It /// can be used in place of a clone immediately followed by a call to `into_inner`. + /// + /// # Panics + /// + /// This method panics if it is called from a different JavaScript thread than the + /// one in which the handle was created. pub fn to_inner<'a, C: Context<'a>>(&self, cx: &mut C) -> Handle<'a, T> { let env = cx.env(); let local = unsafe { reference::get(env.to_raw(), self.as_napi_ref(cx).0 as *mut _) }; diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index e55b4d9f3..f34d75744 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -87,6 +87,8 @@ pub mod prelude; pub mod reflect; pub mod result; mod sys; +#[cfg(feature = "napi-6")] +pub mod thread; pub mod types; #[doc(hidden)] diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 35c89d881..ae2fc877b 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -8,9 +8,13 @@ //! //! [napi-docs]: https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Arc, +use std::{ + any::Any, + marker::PhantomData, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, }; use crate::{ @@ -54,6 +58,157 @@ pub(crate) struct InstanceData { /// Shared `Channel` that is cloned to be returned by the `cx.channel()` method shared_channel: Channel, + + /// Table of user-defined instance-local cells. + locals: LocalTable, +} + +#[derive(Default)] +pub(crate) struct LocalTable { + cells: Vec, +} + +pub(crate) type LocalCellValue = Box; + +pub(crate) enum LocalCell { + /// Uninitialized state. + Uninit, + /// Intermediate "dirty" state representing the middle of a `get_or_try_init` transaction. + Trying, + /// Fully initialized state. + Init(LocalCellValue), +} + +impl LocalCell { + /// Establish the initial state at the beginning of the initialization protocol. + /// This method ensures that re-entrant initialization always panics (i.e. when + /// an existing `get_or_try_init` is in progress). + fn pre_init(&mut self, f: F) + where + F: FnOnce() -> LocalCell, + { + match self { + LocalCell::Uninit => { + *self = f(); + } + LocalCell::Trying => panic!("attempt to reinitialize Local during initialization"), + LocalCell::Init(_) => {} + } + } + + pub(crate) fn get<'cx, 'a, C>(cx: &'a mut C, id: usize) -> Option<&mut LocalCellValue> + where + C: Context<'cx>, + { + let cell = InstanceData::locals(cx).get(id); + match cell { + LocalCell::Init(ref mut b) => Some(b), + _ => None, + } + } + + pub(crate) fn get_or_init<'cx, 'a, C, F>(cx: &'a mut C, id: usize, f: F) -> &mut LocalCellValue + where + C: Context<'cx>, + F: FnOnce() -> LocalCellValue, + { + InstanceData::locals(cx) + .get(id) + .pre_init(|| LocalCell::Init(f())); + + LocalCell::get(cx, id).unwrap() + } + + pub(crate) fn get_or_try_init<'cx, 'a, C, E, F>( + cx: &'a mut C, + id: usize, + f: F, + ) -> Result<&mut LocalCellValue, E> + where + C: Context<'cx>, + F: FnOnce(&mut C) -> Result, + { + // Kick off a new transaction and drop it before getting the result. + { + let mut tx = TryInitTransaction::new(cx, id); + tx.run(|cx| Ok(f(cx)?))?; + } + + // If we're here, the transaction has succeeded, so get the result. + Ok(LocalCell::get(cx, id).unwrap()) + } +} + +impl Default for LocalCell { + fn default() -> Self { + LocalCell::Uninit + } +} + +impl LocalTable { + pub(crate) fn get(&mut self, index: usize) -> &mut LocalCell { + if index >= self.cells.len() { + self.cells.resize_with(index + 1, Default::default); + } + &mut self.cells[index] + } +} + +/// An RAII implementation of `LocalCell::get_or_try_init`, which ensures that +/// the state of a cell is properly managed through all possible control paths. +/// As soon as the transaction begins, the cell is labelled as being in a dirty +/// state (`LocalCell::Trying`), so that any additional re-entrant attempts to +/// initialize the cell will fail fast. The `Drop` implementation ensures that +/// after the transaction, the cell goes back to a clean state of either +/// `LocalCell::Uninit` if it fails or `LocalCell::Init` if it succeeds. +struct TryInitTransaction<'cx, 'a, C: Context<'cx>> { + cx: &'a mut C, + id: usize, + _lifetime: PhantomData<&'cx ()>, +} + +impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> { + fn new(cx: &'a mut C, id: usize) -> Self { + let mut tx = Self { + cx, + id, + _lifetime: PhantomData, + }; + tx.cell().pre_init(|| LocalCell::Trying); + tx + } + + /// _Post-condition:_ If this method returns an `Ok` result, the cell is in the + /// `LocalCell::Init` state. + fn run(&mut self, f: F) -> Result<(), E> + where + F: FnOnce(&mut C) -> Result, + { + if self.is_trying() { + let value = f(self.cx)?; + *self.cell() = LocalCell::Init(value); + } + Ok(()) + } + + fn cell(&mut self) -> &mut LocalCell { + InstanceData::locals(self.cx).get(self.id) + } + + fn is_trying(&mut self) -> bool { + match self.cell() { + LocalCell::Trying => true, + _ => false, + } + } +} + +impl<'cx, 'a, C: Context<'cx>> Drop for TryInitTransaction<'cx, 'a, C> { + fn drop(&mut self) { + if self.is_trying() { + *self.cell() = LocalCell::Uninit; + } + } } /// Wrapper for raw Node-API values to be dropped on the main thread @@ -83,7 +238,7 @@ impl InstanceData { /// # Safety /// No additional locking (e.g., `Mutex`) is necessary because holding a /// `Context` reference ensures serialized access. - pub(crate) fn get<'a, C: Context<'a>>(cx: &mut C) -> &'a mut InstanceData { + pub(crate) fn get<'cx, C: Context<'cx>>(cx: &mut C) -> &mut InstanceData { let env = cx.env().to_raw(); let data = unsafe { lifecycle::get_instance_data::(env).as_mut() }; @@ -107,26 +262,34 @@ impl InstanceData { id: InstanceId::next(), drop_queue: Arc::new(drop_queue), shared_channel, + locals: LocalTable::default(), }; unsafe { &mut *lifecycle::set_instance_data(env, data) } } /// Helper to return a reference to the `drop_queue` field of `InstanceData` - pub(crate) fn drop_queue<'a, C: Context<'a>>(cx: &mut C) -> Arc> { + pub(crate) fn drop_queue<'cx, C: Context<'cx>>( + cx: &mut C, + ) -> Arc> { Arc::clone(&InstanceData::get(cx).drop_queue) } /// Clones the shared channel and references it since new channels should start /// referenced, but the shared channel is unreferenced. - pub(crate) fn channel<'a, C: Context<'a>>(cx: &mut C) -> Channel { + pub(crate) fn channel<'cx, C: Context<'cx>>(cx: &mut C) -> Channel { let mut channel = InstanceData::get(cx).shared_channel.clone(); channel.reference(cx); channel } /// Unique identifier for this instance of the module - pub(crate) fn id<'a, C: Context<'a>>(cx: &mut C) -> InstanceId { + pub(crate) fn id<'cx, C: Context<'cx>>(cx: &mut C) -> InstanceId { InstanceData::get(cx).id } + + /// Helper to return a reference to the `locals` field of `InstanceData`. + pub(crate) fn locals<'cx, C: Context<'cx>>(cx: &mut C) -> &mut LocalTable { + &mut InstanceData::get(cx).locals + } } diff --git a/crates/neon/src/sys/async_work.rs b/crates/neon/src/sys/async_work.rs index e97b46d48..e0d81b05b 100644 --- a/crates/neon/src/sys/async_work.rs +++ b/crates/neon/src/sys/async_work.rs @@ -5,7 +5,7 @@ //! a more idiomatic Rust ownership pattern by passing the output of `execute` //! into the input of `complete`. //! -//! https://nodejs.org/api/n-api.html#n_api_simple_asynchronous_operations +//! See: [Async operations in Node-API](https://nodejs.org/api/n-api.html#n_api_simple_asynchronous_operations) use std::{ ffi::c_void, diff --git a/crates/neon/src/sys/promise.rs b/crates/neon/src/sys/promise.rs index d9e30cd09..83e3f65ee 100644 --- a/crates/neon/src/sys/promise.rs +++ b/crates/neon/src/sys/promise.rs @@ -1,6 +1,6 @@ //! JavaScript Promise and Deferred handle //! -//! https://nodejs.org/api/n-api.html#n_api_promises +//! See: [Promises in Node-API](https://nodejs.org/api/n-api.html#n_api_promises) use std::mem::MaybeUninit; diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs new file mode 100644 index 000000000..34a21fc05 --- /dev/null +++ b/crates/neon/src/thread/mod.rs @@ -0,0 +1,219 @@ +//! Thread-local storage for JavaScript threads. +//! +//! At runtime, an instance of a Node.js addon can contain its own local storage, +//! which can then be shared and accessed as needed from Rust in a Neon module. This can +//! be useful for setting up long-lived state that needs to be shared between calls +//! of an addon's APIs. +//! +//! For example, an addon may wish to track the [thread ID][threadId] of each of its +//! instances: +//! +//! ``` +//! # use neon::prelude::*; +//! # use neon::thread::LocalKey; +//! static THREAD_ID: LocalKey = LocalKey::new(); +//! +//! pub fn thread_id<'cx, C: Context<'cx>>(cx: &mut C) -> NeonResult { +//! THREAD_ID.get_or_try_init(cx, |cx| { +//! let global = cx.global(); +//! let require: Handle = global.get(cx, "require")?; +//! let worker: Handle = require.call_with(cx) +//! .arg(cx.string("node:worker_threads")) +//! .apply(cx)?; +//! let thread_id: Handle = worker.get(cx, "threadId")?; +//! Ok(thread_id.value(cx) as u32) +//! }).cloned() +//! } +//! ``` +//! +//! ### The Addon Lifecycle +//! +//! For some use cases, a single shared global constant stored in a `static` variable +//! might be sufficient: +//! +//! ``` +//! static MY_CONSTANT: &'static str = "hello Neon"; +//! ``` +//! +//! This variable will be allocated when the addon is first loaded into the Node.js +//! process. This works fine for single-threaded applications, or global thread-safe +//! data. +//! +//! However, since the addition of [worker threads][workers] in Node v10, +//! modules can be instantiated multiple times in a single Node process. So even +//! though the dynamically-loaded binary library (i.e., the Rust implementation of +//! the addon) is only loaded once in the running process, its [`#[main]`](crate::main) +//! function can be executed multiple times with distinct module objects, one per application +//! thread: +//! +//! ![The Node.js addon lifecycle, described in detail below.][lifecycle] +//! +//! This means that any thread-local data needs to be initialized separately for each +//! instance of the addon. This module provides a simple container type, [`LocalKey`](LocalKey), +//! for allocating and initializing thread-local data. (Technically, this data is stored in the +//! addon's [module instance][environment], which is equivalent to being thread-local.) +//! +//! A common example is when an addon needs to maintain a reference to a JavaScript value. A +//! reference can be [rooted](crate::handle::Root) and stored in a static, but references cannot +//! be used across separate threads. By placing the reference in thread-local storage, an +//! addon can ensure that each thread stores its own distinct reference: +//! +//! ``` +//! # use neon::prelude::*; +//! # use neon::thread::LocalKey; +//! # fn initialize_my_datatype<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { unimplemented!() } +//! static MY_CONSTRUCTOR: LocalKey> = LocalKey::new(); +//! +//! pub fn my_constructor<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { +//! let constructor = MY_CONSTRUCTOR.get_or_try_init(cx, |cx| { +//! let constructor: Handle = initialize_my_datatype(cx)?; +//! Ok(constructor.root(cx)) +//! })?; +//! Ok(constructor.to_inner(cx)) +//! } +//! ``` +//! +//! Notice that if this code were implemented without a `LocalKey`, it would panic whenever +//! one thread stores an instance of the constructor and a different thread attempts to +//! access it with the call to [`to_inner()`](crate::handle::Root::to_inner). +//! +//! ### When to Use Thread-Local Storage +//! +//! Single-threaded applications don't generally need to worry about thread-local data. +//! There are two cases where Neon apps should consider storing static data in a +//! `LocalKey` storage cell: +//! +//! - **Multi-threaded applications:** If your Node application uses the `Worker` +//! API, you'll want to store any static data that might get access from multiple +//! threads in thread-local data. +//! - **Libraries:** If your addon is part of a library that could be used by multiple +//! applications, you'll want to store static data in thread-local data in case the +//! addon ends up instantiated by multiple threads in some future application. +//! +//! ### Why Not Use Standard TLS? +//! +//! Since the JavaScript engine may not tie JavaScript threads 1:1 to system threads, +//! it is recommended to use this module instead of the Rust standard thread-local storage +//! when associating data with a JavaScript thread. +//! +//! [environment]: https://nodejs.org/api/n-api.html#environment-life-cycle-apis +//! [lifecycle]: https://raw.githubusercontent.com/neon-bindings/neon/main/doc/lifecycle.png +//! [workers]: https://nodejs.org/api/worker_threads.html +//! [threadId]: https://nodejs.org/api/worker_threads.html#workerthreadid + +use std::any::Any; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use once_cell::sync::OnceCell; + +use crate::context::Context; +use crate::lifecycle::LocalCell; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +fn next_id() -> usize { + COUNTER.fetch_add(1, Ordering::SeqCst) +} + +/// A JavaScript thread-local container that owns its contents, similar to +/// [`std::thread::LocalKey`](std::thread::LocalKey) but tied to a JavaScript thread rather +/// than a system thread. +/// +/// ### Initialization and Destruction +/// +/// Initialization is dynamically performed on the first call to one of the `init` methods +/// of `LocalKey`, and values that implement [`Drop`](std::ops::Drop) get destructed when +/// the JavaScript thread exits, i.e. when a worker thread terminates or the main thread +/// terminates on process exit. +#[derive(Default)] +pub struct LocalKey { + _type: PhantomData, + id: OnceCell, +} + +impl LocalKey { + /// Creates a new local value. This method is `const`, so it can be assigned to + /// static variables. + pub const fn new() -> Self { + Self { + _type: PhantomData, + id: OnceCell::new(), + } + } + + fn id(&self) -> usize { + *self.id.get_or_init(next_id) + } +} + +impl LocalKey { + /// Gets the current value of the cell. Returns `None` if the cell has not + /// yet been initialized. + pub fn get<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'cx T> + where + C: Context<'cx>, + { + // Unwrap safety: The type bound LocalKey and the fact that every LocalKey has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: Option<&T> = + LocalCell::get(cx, self.id()).map(|value| value.downcast_ref().unwrap()); + + // Safety: Since the Box is immutable and heap-allocated, it's guaranteed not to + // move or change for the duration of the context. + unsafe { std::mem::transmute::, Option<&'cx T>>(r) } + } + + /// Gets the current value of the cell, initializing it with the result of + /// calling `f` if it has not yet been initialized. + pub fn get_or_init<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> &'cx T + where + C: Context<'cx>, + F: FnOnce() -> T, + { + // Unwrap safety: The type bound LocalKey and the fact that every LocalKey has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: &T = LocalCell::get_or_init(cx, self.id(), || Box::new(f())) + .downcast_ref() + .unwrap(); + + // Safety: Since the Box is immutable and heap-allocated, it's guaranteed not to + // move or change for the duration of the context. + unsafe { std::mem::transmute::<&'a T, &'cx T>(r) } + } + + /// Gets the current value of the cell, initializing it with the result of + /// calling `f` if it has not yet been initialized. Returns `Err` if the + /// callback triggers a JavaScript exception. + /// + /// # Panics + /// + /// During the execution of `f`, calling any methods on this `LocalKey` that + /// attempt to initialize it will panic. + pub fn get_or_try_init<'cx, 'a, C, E, F>(&self, cx: &'a mut C, f: F) -> Result<&'cx T, E> + where + C: Context<'cx>, + F: FnOnce(&mut C) -> Result, + { + // Unwrap safety: The type bound LocalKey and the fact that every LocalKey has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: &T = LocalCell::get_or_try_init(cx, self.id(), |cx| Ok(Box::new(f(cx)?)))? + .downcast_ref() + .unwrap(); + + // Safety: Since the Box is immutable and heap-allocated, it's guaranteed not to + // move or change for the duration of the context. + Ok(unsafe { std::mem::transmute::<&'a T, &'cx T>(r) }) + } +} + +impl LocalKey { + /// Gets the current value of the cell, initializing it with the default value + /// if it has not yet been initialized. + pub fn get_or_init_default<'cx, 'a, C>(&self, cx: &'a mut C) -> &'cx T + where + C: Context<'cx>, + { + self.get_or_init(cx, Default::default) + } +} diff --git a/doc/lifecycle.png b/doc/lifecycle.png new file mode 100644 index 000000000..8d73c45f0 Binary files /dev/null and b/doc/lifecycle.png differ diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index f28abcf63..64ff0c10e 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -1,10 +1,16 @@ const assert = require("assert"); -const { Worker, isMainThread, parentPort } = require("worker_threads"); +const { + Worker, + isMainThread, + parentPort, + threadId, +} = require("worker_threads"); const addon = require(".."); // Receive a message, try that method and return the error message if (!isMainThread) { + addon.get_or_init_thread_id(threadId); parentPort.once("message", (message) => { try { switch (message) { @@ -17,6 +23,12 @@ if (!isMainThread) { case "get_or_init_clone": addon.get_or_init_clone(() => ({})); break; + case "get_thread_id": + { + let id = addon.get_or_init_thread_id(NaN); + parentPort.postMessage(id); + } + break; default: throw new Error(`Unexpected message: ${message}`); } @@ -30,6 +42,11 @@ if (!isMainThread) { return; } +// From here on, we're in the main thread. + +// Set the `THREAD_ID` Global value in the main thread cell. +addon.get_or_init_thread_id(threadId); + describe("Worker / Root Tagging Tests", () => { describe("Single Threaded", () => { it("should be able to stash a global with `get_and_replace`", () => { @@ -106,3 +123,68 @@ describe("Worker / Root Tagging Tests", () => { }); }); }); + +describe("Instance-local storage", () => { + it("should be able to read an instance local from the main thread", () => { + let lookedUpId = addon.get_or_init_thread_id(NaN); + assert(!Number.isNaN(lookedUpId)); + assert.strictEqual(lookedUpId, threadId); + }); + + it("should be able to store rooted objects in instance locals", () => { + addon.stash_global_object(); + assert.strictEqual(global, addon.unstash_global_object()); + }); + + it("should gracefully panic upon reentrant get_or_try_init", () => { + // 1. Global should start out uninitialized + assert.strictEqual(null, addon.get_reentrant_value()); + + // 2. Re-entrancy should panic + let innerClosureExecuted = false; + try { + let result = addon.reentrant_try_init(() => { + addon.reentrant_try_init(() => { + innerClosureExecuted = true; + }); + }); + assert.fail("should have panicked on re-entrancy"); + } catch (expected) { + assert.strictEqual( + innerClosureExecuted, + false, + "inner closure should not have executed" + ); + } + + try { + // 3. Local should still be uninitialized + assert.strictEqual(null, addon.get_reentrant_value()); + + // 4. Successful fallible initialization + let result = addon.reentrant_try_init(() => {}); + assert.strictEqual(42, result); + assert.strictEqual(42, addon.get_reentrant_value()); + } catch (unexpected) { + assert.fail("couldn't set reentrant local after initial failure"); + } + }); + + it("should allocate separate locals for each addon instance", (cb) => { + let mainThreadId = addon.get_or_init_thread_id(NaN); + assert(!Number.isNaN(mainThreadId)); + + const worker = new Worker(__filename); + + worker.once("message", (message) => { + assert.strictEqual(typeof message, "number"); + assert.notStrictEqual(message, mainThreadId); + let mainThreadIdAgain = addon.get_or_init_thread_id(NaN); + assert(!Number.isNaN(mainThreadIdAgain)); + assert.strictEqual(mainThreadIdAgain, mainThreadId); + cb(); + }); + + worker.postMessage("get_thread_id"); + }); +}); diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 913dae66b..83ea3cbc0 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use once_cell::sync::{Lazy, OnceCell}; use neon::prelude::*; +use neon::thread::LocalKey; pub fn get_and_replace(mut cx: FunctionContext) -> JsResult { static OBJECT: Lazy>>> = Lazy::new(Default::default); @@ -44,3 +45,48 @@ pub fn get_or_init_clone(mut cx: FunctionContext) -> JsResult { // test the `clone` method. Ok(o.clone(&mut cx).into_inner(&mut cx)) } + +static THREAD_ID: LocalKey = LocalKey::new(); + +pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { + let id = cx.argument::(0)?.value(&mut cx) as u32; + let id: &u32 = THREAD_ID.get_or_init(&mut cx, || id); + Ok(cx.number(*id)) +} + +static REENTRANT_LOCAL: LocalKey = LocalKey::new(); + +pub fn reentrant_try_init(mut cx: FunctionContext) -> JsResult { + let f = cx.argument::(0)?; + let n = REENTRANT_LOCAL.get_or_try_init(&mut cx, |cx| { + f.call_with(cx).exec(cx)?; + Ok(42) + })?; + Ok(cx.number(*n)) +} + +pub fn get_reentrant_value(mut cx: FunctionContext) -> JsResult { + let value = REENTRANT_LOCAL.get(&mut cx).cloned(); + match value { + Some(n) => Ok(cx.number(n).upcast()), + None => Ok(cx.null().upcast()), + } +} + +static GLOBAL_OBJECT: LocalKey> = LocalKey::new(); + +pub fn stash_global_object(mut cx: FunctionContext) -> JsResult { + GLOBAL_OBJECT.get_or_try_init(&mut cx, |cx| { + let global = cx.global(); + let global: Root = Root::new(cx, &global); + Ok(global) + })?; + Ok(cx.undefined()) +} + +pub fn unstash_global_object(mut cx: FunctionContext) -> JsResult { + Ok(match GLOBAL_OBJECT.get(&mut cx) { + Some(root) => root.to_inner(&mut cx).upcast(), + None => cx.null().upcast(), + }) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index f26a45a58..9fbad123f 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -340,6 +340,11 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("get_and_replace", js::workers::get_and_replace)?; cx.export_function("get_or_init", js::workers::get_or_init)?; cx.export_function("get_or_init_clone", js::workers::get_or_init_clone)?; + cx.export_function("get_or_init_thread_id", js::workers::get_or_init_thread_id)?; + cx.export_function("reentrant_try_init", js::workers::reentrant_try_init)?; + cx.export_function("get_reentrant_value", js::workers::get_reentrant_value)?; + cx.export_function("stash_global_object", js::workers::stash_global_object)?; + cx.export_function("unstash_global_object", js::workers::unstash_global_object)?; Ok(()) }