From 94111d4c19b9cee7f6d101b71bab0fce8bf4ac8a Mon Sep 17 00:00:00 2001 From: David Herman Date: Mon, 23 May 2022 21:47:06 -0700 Subject: [PATCH 01/33] Add `neon::instance::Global` API for storing instance-global data. --- crates/neon/Cargo.toml | 1 + crates/neon/src/instance/mod.rs | 72 +++++++++++++++++++++++++++++++++ crates/neon/src/lib.rs | 2 + crates/neon/src/lifecycle.rs | 48 ++++++++++++++++++++-- test/napi/lib/workers.js | 37 ++++++++++++++++- test/napi/src/js/workers.rs | 17 ++++++++ test/napi/src/lib.rs | 2 + 7 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 crates/neon/src/instance/mod.rs 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/instance/mod.rs b/crates/neon/src/instance/mod.rs new file mode 100644 index 000000000..4e847afba --- /dev/null +++ b/crates/neon/src/instance/mod.rs @@ -0,0 +1,72 @@ +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::InstanceData; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +fn next_id() -> usize { + COUNTER.fetch_add(1, Ordering::SeqCst) +} + +/// A cell that can be used to allocate data that is global to an instance +/// of a Neon addon. +pub struct Global { + _type: PhantomData, + id: OnceCell, +} + +impl Global { + pub const fn new() -> Self { + Self { + _type: PhantomData, + id: OnceCell::new(), + } + } + + fn id(&self) -> usize { + *self.id.get_or_init(next_id) + } +} + +impl Global { + pub fn borrow<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b T> + where + C: Context<'a>, + { + InstanceData::globals(cx)[self.id()] + .as_ref() + .map(|boxed| boxed.downcast_ref().unwrap()) + } + + pub fn borrow_mut<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b mut T> + where + C: Context<'a>, + { + InstanceData::globals(cx)[self.id()] + .as_mut() + .map(|boxed| boxed.downcast_mut().unwrap()) + } + + pub fn set<'a, 'b, C>(&self, cx: &'b mut C, v: T) + where + C: Context<'a>, + { + InstanceData::globals(cx)[self.id()] = Some(Box::new(v)); + } +} + +impl Global { + pub fn get<'a, C>(&self, cx: &mut C) -> Option + where + C: Context<'a>, + { + InstanceData::globals(cx)[self.id()] + .as_ref() + .map(|boxed| boxed.downcast_ref::().unwrap().clone()) + } +} diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index e55b4d9f3..6190b4e7d 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -81,6 +81,8 @@ pub mod context; pub mod event; pub mod handle; +#[cfg(feature = "napi-6")] +pub mod instance; pub mod meta; pub mod object; pub mod prelude; diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 35c89d881..f0d0be648 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, + ops::{Index, IndexMut}, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, }; use crate::{ @@ -54,6 +58,36 @@ pub(crate) struct InstanceData { /// Shared `Channel` that is cloned to be returned by the `cx.channel()` method shared_channel: Channel, + + /// Table of user-defined global cells. + globals: GlobalTable, +} + +pub(crate) struct GlobalTable { + cells: Vec>>, +} + +impl GlobalTable { + fn new() -> Self { + Self { cells: Vec::new() } + } +} + +impl Index for GlobalTable { + type Output = Option>; + + fn index(&self, index: usize) -> &Self::Output { + &self.cells[index] + } +} + +impl IndexMut for GlobalTable { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + if index >= self.cells.len() { + self.cells.resize_with(index + 1, Default::default); + } + &mut self.cells[index] + } } /// Wrapper for raw Node-API values to be dropped on the main thread @@ -83,7 +117,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<'a, C: Context<'a>>(cx: &mut C) -> &mut InstanceData { let env = cx.env().to_raw(); let data = unsafe { lifecycle::get_instance_data::(env).as_mut() }; @@ -107,6 +141,7 @@ impl InstanceData { id: InstanceId::next(), drop_queue: Arc::new(drop_queue), shared_channel, + globals: GlobalTable::new(), }; unsafe { &mut *lifecycle::set_instance_data(env, data) } @@ -129,4 +164,9 @@ impl InstanceData { pub(crate) fn id<'a, C: Context<'a>>(cx: &mut C) -> InstanceId { InstanceData::get(cx).id } + + /// Helper to return a reference to the `globals` field of `InstanceData`. + pub(crate) fn globals<'a, C: Context<'a>>(cx: &mut C) -> &mut GlobalTable { + &mut InstanceData::get(cx).globals + } } diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index f28abcf63..987ed3e7c 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -1,10 +1,11 @@ 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.set_thread_id(threadId); parentPort.once("message", (message) => { try { switch (message) { @@ -17,6 +18,12 @@ if (!isMainThread) { case "get_or_init_clone": addon.get_or_init_clone(() => ({})); break; + case "get_thread_id": + { + let id = addon.get_thread_id(); + parentPort.postMessage(id); + } + break; default: throw new Error(`Unexpected message: ${message}`); } @@ -30,6 +37,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.set_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 +118,26 @@ describe("Worker / Root Tagging Tests", () => { }); }); }); + +describe("Globals", () => { + it("should be able to read an instance global from the main thread", () => { + let lookedUpId = addon.get_thread_id(); + assert.strictEqual(lookedUpId, threadId); + }); + + it("should allocate separate globals for each addon instance", (cb) => { + let mainThreadId = addon.get_thread_id(); + + const worker = new Worker(__filename); + + worker.once("message", (message) => { + assert.strictEqual(typeof message, 'number'); + assert.notStrictEqual(message, mainThreadId); + let mainThreadIdAgain = addon.get_thread_id(); + 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..8fb0a4743 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -2,6 +2,7 @@ use std::sync::Mutex; use once_cell::sync::{Lazy, OnceCell}; +use neon::instance::Global; use neon::prelude::*; pub fn get_and_replace(mut cx: FunctionContext) -> JsResult { @@ -44,3 +45,19 @@ 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: Global = Global::new(); + +pub fn set_thread_id(mut cx: FunctionContext) -> JsResult { + let id = cx.argument::(0)?.value(&mut cx) as u32; + THREAD_ID.set(&mut cx, id); + Ok(cx.undefined()) +} + +pub fn get_thread_id(mut cx: FunctionContext) -> JsResult { + let id = THREAD_ID.get(&mut cx); + Ok(match id { + Some(id) => cx.number(id).upcast(), + None => cx.undefined().upcast(), + }) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index f26a45a58..21bb6dbd6 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -340,6 +340,8 @@ 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_thread_id", js::workers::get_thread_id)?; + cx.export_function("set_thread_id", js::workers::set_thread_id)?; Ok(()) } From f872b76f6685eba1ac27a7113429599188ac1ba4 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 25 May 2022 10:51:50 -0700 Subject: [PATCH 02/33] Address review comments: - `GlobalTable::default()` to avoid boilerplate `new()` method - Rename `borrow()` and `borrow_mut()` to `get()` and `get_mut()` - Add `'static` bound to global contents - Use `cloned()` in test code --- crates/neon/src/instance/mod.rs | 18 ++++-------------- crates/neon/src/lifecycle.rs | 13 ++++--------- test/napi/src/js/workers.rs | 2 +- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 4e847afba..67717783d 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -15,6 +15,7 @@ fn next_id() -> usize { /// A cell that can be used to allocate data that is global to an instance /// of a Neon addon. +#[derive(Default)] pub struct Global { _type: PhantomData, id: OnceCell, @@ -33,8 +34,8 @@ impl Global { } } -impl Global { - pub fn borrow<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b T> +impl Global { + pub fn get<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b T> where C: Context<'a>, { @@ -43,7 +44,7 @@ impl Global { .map(|boxed| boxed.downcast_ref().unwrap()) } - pub fn borrow_mut<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b mut T> + pub fn get_mut<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b mut T> where C: Context<'a>, { @@ -59,14 +60,3 @@ impl Global { InstanceData::globals(cx)[self.id()] = Some(Box::new(v)); } } - -impl Global { - pub fn get<'a, C>(&self, cx: &mut C) -> Option - where - C: Context<'a>, - { - InstanceData::globals(cx)[self.id()] - .as_ref() - .map(|boxed| boxed.downcast_ref::().unwrap().clone()) - } -} diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index f0d0be648..28597bbde 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -63,18 +63,13 @@ pub(crate) struct InstanceData { globals: GlobalTable, } +#[derive(Default)] pub(crate) struct GlobalTable { - cells: Vec>>, -} - -impl GlobalTable { - fn new() -> Self { - Self { cells: Vec::new() } - } + cells: Vec>>, } impl Index for GlobalTable { - type Output = Option>; + type Output = Option>; fn index(&self, index: usize) -> &Self::Output { &self.cells[index] @@ -141,7 +136,7 @@ impl InstanceData { id: InstanceId::next(), drop_queue: Arc::new(drop_queue), shared_channel, - globals: GlobalTable::new(), + globals: GlobalTable::default(), }; unsafe { &mut *lifecycle::set_instance_data(env, data) } diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 8fb0a4743..623fe8f2f 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -55,7 +55,7 @@ pub fn set_thread_id(mut cx: FunctionContext) -> JsResult { } pub fn get_thread_id(mut cx: FunctionContext) -> JsResult { - let id = THREAD_ID.get(&mut cx); + let id = THREAD_ID.get(&mut cx).cloned(); Ok(match id { Some(id) => cx.number(id).upcast(), None => cx.undefined().upcast(), From 6f12663f3a08d1fb59bca1b1ca5dddb80cbd046b Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 25 May 2022 10:57:23 -0700 Subject: [PATCH 03/33] Use `'cx` as the inner lifetime of contexts. --- crates/neon/src/instance/mod.rs | 12 ++++++------ crates/neon/src/lifecycle.rs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 67717783d..ba00f2be1 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -35,27 +35,27 @@ impl Global { } impl Global { - pub fn get<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b T> + pub fn get<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'a T> where - C: Context<'a>, + C: Context<'cx>, { InstanceData::globals(cx)[self.id()] .as_ref() .map(|boxed| boxed.downcast_ref().unwrap()) } - pub fn get_mut<'a, 'b, C>(&self, cx: &'b mut C) -> Option<&'b mut T> + pub fn get_mut<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'a mut T> where - C: Context<'a>, + C: Context<'cx>, { InstanceData::globals(cx)[self.id()] .as_mut() .map(|boxed| boxed.downcast_mut().unwrap()) } - pub fn set<'a, 'b, C>(&self, cx: &'b mut C, v: T) + pub fn set<'cx, 'a, C>(&self, cx: &'a mut C, v: T) where - C: Context<'a>, + C: Context<'cx>, { InstanceData::globals(cx)[self.id()] = Some(Box::new(v)); } diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 28597bbde..871162145 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -112,7 +112,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) -> &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() }; @@ -143,25 +143,25 @@ impl InstanceData { } /// 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 `globals` field of `InstanceData`. - pub(crate) fn globals<'a, C: Context<'a>>(cx: &mut C) -> &mut GlobalTable { + pub(crate) fn globals<'cx, C: Context<'cx>>(cx: &mut C) -> &mut GlobalTable { &mut InstanceData::get(cx).globals } } From 18e60f1c857e9e2dd686d84100018598644d7cbe Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 25 May 2022 11:14:48 -0700 Subject: [PATCH 04/33] Get rid of `[]` overloading for `GlobalTable` in favor of an inherent `get()` method. --- crates/neon/src/instance/mod.rs | 8 +++++--- crates/neon/src/lifecycle.rs | 13 ++----------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index ba00f2be1..9e8c8308f 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -39,7 +39,8 @@ impl Global { where C: Context<'cx>, { - InstanceData::globals(cx)[self.id()] + InstanceData::globals(cx) + .get(self.id()) .as_ref() .map(|boxed| boxed.downcast_ref().unwrap()) } @@ -48,7 +49,8 @@ impl Global { where C: Context<'cx>, { - InstanceData::globals(cx)[self.id()] + InstanceData::globals(cx) + .get(self.id()) .as_mut() .map(|boxed| boxed.downcast_mut().unwrap()) } @@ -57,6 +59,6 @@ impl Global { where C: Context<'cx>, { - InstanceData::globals(cx)[self.id()] = Some(Box::new(v)); + *InstanceData::globals(cx).get(self.id()) = Some(Box::new(v)); } } diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 871162145..31eb47922 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -10,7 +10,6 @@ use std::{ any::Any, - ops::{Index, IndexMut}, sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -68,16 +67,8 @@ pub(crate) struct GlobalTable { cells: Vec>>, } -impl Index for GlobalTable { - type Output = Option>; - - fn index(&self, index: usize) -> &Self::Output { - &self.cells[index] - } -} - -impl IndexMut for GlobalTable { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { +impl GlobalTable { + pub(crate) fn get(&mut self, index: usize) -> &mut Option> { if index >= self.cells.len() { self.cells.resize_with(index + 1, Default::default); } From b8b089b33869599079ef5753f76a8e473d2afa5f Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 25 May 2022 12:12:52 -0700 Subject: [PATCH 05/33] Immutable version of API: - `Global::get()` returns an `Option<&T>` - `Global::get_or_init()` returns an `&T` - Lifetime of returned reference is the inner `'cx` since the boxed reference is immutable --- crates/neon/src/instance/mod.rs | 29 ++++++++++++++++------------- test/napi/lib/workers.js | 15 +++++++++------ test/napi/src/js/workers.rs | 14 +++----------- test/napi/src/lib.rs | 3 +-- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 9e8c8308f..48967b359 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -35,30 +35,33 @@ impl Global { } impl Global { - pub fn get<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'a T> + pub fn get<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'cx T> where C: Context<'cx>, { - InstanceData::globals(cx) + let r: Option<&T> = InstanceData::globals(cx) .get(self.id()) .as_ref() - .map(|boxed| boxed.downcast_ref().unwrap()) + .map(|boxed| boxed.downcast_ref().unwrap()); + + unsafe { + std::mem::transmute(r) + } } - pub fn get_mut<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'a mut T> + 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, { - InstanceData::globals(cx) + let r: &T = InstanceData::globals(cx) .get(self.id()) - .as_mut() - .map(|boxed| boxed.downcast_mut().unwrap()) - } + .get_or_insert_with(|| Box::new(f())) + .downcast_ref() + .unwrap(); - pub fn set<'cx, 'a, C>(&self, cx: &'a mut C, v: T) - where - C: Context<'cx>, - { - *InstanceData::globals(cx).get(self.id()) = Some(Box::new(v)); + unsafe { + std::mem::transmute(r) + } } } diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index 987ed3e7c..943cab8de 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -5,7 +5,7 @@ const addon = require(".."); // Receive a message, try that method and return the error message if (!isMainThread) { - addon.set_thread_id(threadId); + addon.get_or_init_thread_id(threadId); parentPort.once("message", (message) => { try { switch (message) { @@ -20,7 +20,7 @@ if (!isMainThread) { break; case "get_thread_id": { - let id = addon.get_thread_id(); + let id = addon.get_or_init_thread_id(NaN); parentPort.postMessage(id); } break; @@ -40,7 +40,7 @@ if (!isMainThread) { // From here on, we're in the main thread. // Set the `THREAD_ID` Global value in the main thread cell. -addon.set_thread_id(threadId); +addon.get_or_init_thread_id(threadId); describe("Worker / Root Tagging Tests", () => { describe("Single Threaded", () => { @@ -121,19 +121,22 @@ describe("Worker / Root Tagging Tests", () => { describe("Globals", () => { it("should be able to read an instance global from the main thread", () => { - let lookedUpId = addon.get_thread_id(); + let lookedUpId = addon.get_or_init_thread_id(NaN); + assert(!Number.isNaN(lookedUpId)); assert.strictEqual(lookedUpId, threadId); }); it("should allocate separate globals for each addon instance", (cb) => { - let mainThreadId = addon.get_thread_id(); + 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_thread_id(); + let mainThreadIdAgain = addon.get_or_init_thread_id(NaN); + assert(!Number.isNaN(mainThreadIdAgain)); assert.strictEqual(mainThreadIdAgain, mainThreadId); cb(); }); diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 623fe8f2f..c2a02f399 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -48,16 +48,8 @@ pub fn get_or_init_clone(mut cx: FunctionContext) -> JsResult { static THREAD_ID: Global = Global::new(); -pub fn set_thread_id(mut cx: FunctionContext) -> JsResult { +pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { let id = cx.argument::(0)?.value(&mut cx) as u32; - THREAD_ID.set(&mut cx, id); - Ok(cx.undefined()) -} - -pub fn get_thread_id(mut cx: FunctionContext) -> JsResult { - let id = THREAD_ID.get(&mut cx).cloned(); - Ok(match id { - Some(id) => cx.number(id).upcast(), - None => cx.undefined().upcast(), - }) + let id: &u32 = THREAD_ID.get_or_init(&mut cx, || id); + Ok(cx.number(*id)) } diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 21bb6dbd6..3ff9fa788 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -340,8 +340,7 @@ 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_thread_id", js::workers::get_thread_id)?; - cx.export_function("set_thread_id", js::workers::set_thread_id)?; + cx.export_function("get_or_init_thread_id", js::workers::get_or_init_thread_id)?; Ok(()) } From cf0d32adc7574eaabbed2f485a70d2aca2d7d799 Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 25 May 2022 12:22:55 -0700 Subject: [PATCH 06/33] Explicitly name the types in the transmute Co-authored-by: K.J. Valencik --- crates/neon/src/instance/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 48967b359..445de08b1 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -61,7 +61,7 @@ impl Global { .unwrap(); unsafe { - std::mem::transmute(r) + std::mem::transmute::<&'a T, &'cx T>(r) } } } From f5cd0bad91715d0d6fc78fc81332aaf46e70ef84 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 25 May 2022 12:23:39 -0700 Subject: [PATCH 07/33] Explicitly name the types in the transmute. --- crates/neon/src/instance/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 445de08b1..2915e1641 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -45,7 +45,7 @@ impl Global { .map(|boxed| boxed.downcast_ref().unwrap()); unsafe { - std::mem::transmute(r) + std::mem::transmute::, Option<&'cx T>>(r) } } From aee77f45386b07449d698ad07bc74f33b77c97da Mon Sep 17 00:00:00 2001 From: David Herman Date: Thu, 26 May 2022 20:09:38 -0700 Subject: [PATCH 08/33] Add `get_or_try_init` and `get_or_init_default` - Also rename `get_or_init` to `get_or_init_with` - Also add `get_or_init` that takes an owned init value --- crates/neon/src/instance/mod.rs | 52 ++++++++++++++++++++++++++++++++- test/napi/src/js/workers.rs | 2 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 2915e1641..0d102071f 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -6,6 +6,7 @@ use once_cell::sync::OnceCell; use crate::context::Context; use crate::lifecycle::InstanceData; +use crate::result::NeonResult; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -49,7 +50,22 @@ impl Global { } } - pub fn get_or_init<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> &'cx T + pub fn get_or_init<'cx, 'a, C>(&self, cx: &'a mut C, value: T) -> &'cx T + where + C: Context<'cx>, + { + let r: &T = InstanceData::globals(cx) + .get(self.id()) + .get_or_insert(Box::new(value)) + .downcast_ref() + .unwrap(); + + unsafe { + std::mem::transmute::<&'a T, &'cx T>(r) + } + } + + pub fn get_or_init_with<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> &'cx T where C: Context<'cx>, F: FnOnce() -> T, @@ -64,4 +80,38 @@ impl Global { std::mem::transmute::<&'a T, &'cx T>(r) } } + + pub fn get_or_try_init<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> NeonResult<&'cx T> + where + C: Context<'cx>, + F: FnOnce(&mut C) -> NeonResult, + { + if let Some(value) = self.get(cx) { + return Ok(value); + } + + let value = f(cx)?; + + if self.get(cx).is_some() { + panic!("Global already initialized during get_or_try_init callback"); + } + + let r: &T = InstanceData::globals(cx).get(self.id()) + .insert(Box::new(value)) + .downcast_ref() + .unwrap(); + + Ok(unsafe { + std::mem::transmute::<&'a T, &'cx T>(r) + }) + } +} + +impl Global { + pub fn get_or_init_default<'cx, 'a, C>(&self, cx: &'a mut C) -> &'cx T + where + C: Context<'cx>, + { + self.get_or_init_with(cx, Default::default) + } } diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index c2a02f399..afb0a3978 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -50,6 +50,6 @@ static THREAD_ID: Global = Global::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); + let id: &u32 = THREAD_ID.get_or_init(&mut cx, id); Ok(cx.number(*id)) } From 9075b76b8dafe8d79eb56ef5d48f92d8052274b1 Mon Sep 17 00:00:00 2001 From: David Herman Date: Tue, 31 May 2022 19:07:14 -0700 Subject: [PATCH 09/33] Protect re-entrant cases with "dirty" state checking - Uses an RAII pattern to ensure `get_or_try_init` always terminates cleanly - All initialization paths are checked for the dirty state to avoid re-entrancy - Also adds API docs and safety comments --- crates/neon/src/instance/mod.rs | 81 +++++++++------- crates/neon/src/lifecycle.rs | 164 +++++++++++++++++++++++++++++++- test/napi/lib/workers.js | 25 +++++ test/napi/src/js/workers.rs | 19 ++++ test/napi/src/lib.rs | 2 + 5 files changed, 251 insertions(+), 40 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 0d102071f..4584709b6 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use once_cell::sync::OnceCell; use crate::context::Context; -use crate::lifecycle::InstanceData; +use crate::lifecycle::GlobalCell; use crate::result::NeonResult; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -23,6 +23,8 @@ pub struct Global { } impl Global { + /// Creates a new global value. This method is `const`, so it can be assigned to + /// static variables. pub const fn new() -> Self { Self { _type: PhantomData, @@ -36,78 +38,83 @@ impl Global { } impl Global { + /// Gets the current value of the global. Returns `None` if the global has not + /// yet been initialized. pub fn get<'cx, 'a, C>(&self, cx: &'a mut C) -> Option<&'cx T> where C: Context<'cx>, { - let r: Option<&T> = InstanceData::globals(cx) - .get(self.id()) - .as_ref() - .map(|boxed| boxed.downcast_ref().unwrap()); - - unsafe { - std::mem::transmute::, Option<&'cx T>>(r) - } + // Safety: The type bound Global and the fact that every Global has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: Option<&T> = + GlobalCell::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 global, initializing it with `value` if it has + /// not yet been initialized. pub fn get_or_init<'cx, 'a, C>(&self, cx: &'a mut C, value: T) -> &'cx T where C: Context<'cx>, { - let r: &T = InstanceData::globals(cx) - .get(self.id()) - .get_or_insert(Box::new(value)) + // Safety: The type bound Global and the fact that every Global has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: &T = GlobalCell::get_or_init(cx, self.id(), Box::new(value)) .downcast_ref() .unwrap(); - unsafe { - std::mem::transmute::<&'a T, &'cx T>(r) - } + // 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 global, initializing it with the result of + /// calling `f` if it has not yet been initialized. pub fn get_or_init_with<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> &'cx T where C: Context<'cx>, F: FnOnce() -> T, { - let r: &T = InstanceData::globals(cx) - .get(self.id()) - .get_or_insert_with(|| Box::new(f())) + // Safety: The type bound Global and the fact that every Global has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: &T = GlobalCell::get_or_init_with(cx, self.id(), || Box::new(f())) .downcast_ref() .unwrap(); - unsafe { - std::mem::transmute::<&'a T, &'cx T>(r) - } + // 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 global, initializing it with the result of + /// calling `f` if it has not yet been initialized. Returns `Err` if the + /// callback triggers a JavaScript exception. + /// + /// During the execution of `f`, calling any methods on this `Global` that + /// attempt to initialize it will panic. pub fn get_or_try_init<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> NeonResult<&'cx T> where C: Context<'cx>, F: FnOnce(&mut C) -> NeonResult, { - if let Some(value) = self.get(cx) { - return Ok(value); - } - - let value = f(cx)?; - - if self.get(cx).is_some() { - panic!("Global already initialized during get_or_try_init callback"); - } - - let r: &T = InstanceData::globals(cx).get(self.id()) - .insert(Box::new(value)) + // Safety: The type bound Global and the fact that every Global has a unique + // id guarantees that the cell is only ever assigned instances of type T. + let r: &T = GlobalCell::get_or_try_init(cx, self.id(), |cx| Ok(Box::new(f(cx)?)))? .downcast_ref() .unwrap(); - - Ok(unsafe { - std::mem::transmute::<&'a T, &'cx T>(r) - }) + + // 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 Global { + /// Gets the current value of the global, 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>, diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 31eb47922..cb04c92e8 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -10,6 +10,7 @@ use std::{ any::Any, + marker::PhantomData, sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -20,6 +21,7 @@ use crate::{ context::Context, event::Channel, handle::root::NapiRef, + result::NeonResult, sys::{lifecycle, raw::Env, tsfn::ThreadsafeFunction}, types::promise::NodeApiDeferred, }; @@ -64,11 +66,107 @@ pub(crate) struct InstanceData { #[derive(Default)] pub(crate) struct GlobalTable { - cells: Vec>>, + cells: Vec, +} + +pub(crate) enum GlobalCell { + /// Uninitialized state. + Uninit, + /// Intermediate "dirty" state representing the middle of a `get_or_try_init` transaction. + Trying, + /// Fully initialized state. + Init(Box), +} + +pub(crate) type GlobalCellValue = Box; + +impl GlobalCell { + /// 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() -> GlobalCell, + { + match self { + GlobalCell::Uninit => { + *self = f(); + } + GlobalCell::Trying => panic!("attempt to reinitialize Global during initialization"), + GlobalCell::Init(_) => {} + } + } + + pub(crate) fn get<'cx, 'a, C>(cx: &'a mut C, id: usize) -> Option<&mut GlobalCellValue> + where + C: Context<'cx>, + { + let cell = InstanceData::globals(cx).get(id); + match cell { + GlobalCell::Init(ref mut b) => Some(b), + _ => None, + } + } + + pub(crate) fn get_or_init<'cx, 'a, C>( + cx: &'a mut C, + id: usize, + value: GlobalCellValue, + ) -> &mut GlobalCellValue + where + C: Context<'cx>, + { + InstanceData::globals(cx) + .get(id) + .pre_init(|| GlobalCell::Init(value)); + + GlobalCell::get(cx, id).unwrap() + } + + pub(crate) fn get_or_init_with<'cx, 'a, C, F>( + cx: &'a mut C, + id: usize, + f: F, + ) -> &mut GlobalCellValue + where + C: Context<'cx>, + F: FnOnce() -> GlobalCellValue, + { + InstanceData::globals(cx) + .get(id) + .pre_init(|| GlobalCell::Init(f())); + + GlobalCell::get(cx, id).unwrap() + } + + pub(crate) fn get_or_try_init<'cx, 'a, C, F>( + cx: &'a mut C, + id: usize, + f: F, + ) -> NeonResult<&mut GlobalCellValue> + where + C: Context<'cx>, + F: FnOnce(&mut C) -> NeonResult, + { + // 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(GlobalCell::get(cx, id).unwrap()) + } +} + +impl Default for GlobalCell { + fn default() -> Self { + GlobalCell::Uninit + } } impl GlobalTable { - pub(crate) fn get(&mut self, index: usize) -> &mut Option> { + pub(crate) fn get(&mut self, index: usize) -> &mut GlobalCell { if index >= self.cells.len() { self.cells.resize_with(index + 1, Default::default); } @@ -76,6 +174,64 @@ impl GlobalTable { } } +/// An RAII implementation of `GlobalCell::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 (`GlobalCell::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 +/// `GlobalCell::Uninit` if it fails or `GlobalCell::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(|| GlobalCell::Trying); + tx + } + + /// _Post-condition:_ If this method returns an `Ok` result, the cell is in the + /// `GlobalCell::Init` state. + fn run(&mut self, f: F) -> NeonResult<()> + where + F: FnOnce(&mut C) -> NeonResult, + { + if self.is_trying() { + let value = f(self.cx)?; + *self.cell() = GlobalCell::Init(value); + } + Ok(()) + } + + fn cell(&mut self) -> &mut GlobalCell { + InstanceData::globals(self.cx).get(self.id) + } + + fn is_trying(&mut self) -> bool { + if let GlobalCell::Trying = *self.cell() { + true + } else { + false + } + } +} + +impl<'cx, 'a, C: Context<'cx>> Drop for TryInitTransaction<'cx, 'a, C> { + fn drop(&mut self) { + if self.is_trying() { + *self.cell() = GlobalCell::Uninit; + } + } +} + /// Wrapper for raw Node-API values to be dropped on the main thread pub(crate) enum DropData { Deferred(NodeApiDeferred), @@ -134,7 +290,9 @@ impl InstanceData { } /// Helper to return a reference to the `drop_queue` field of `InstanceData` - pub(crate) fn drop_queue<'cx, C: Context<'cx>>(cx: &mut C) -> Arc> { + pub(crate) fn drop_queue<'cx, C: Context<'cx>>( + cx: &mut C, + ) -> Arc> { Arc::clone(&InstanceData::get(cx).drop_queue) } diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index 943cab8de..546835997 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -126,6 +126,31 @@ describe("Globals", () => { assert.strictEqual(lookedUpId, threadId); }); + 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 + try { + let result = addon.reentrant_try_init(() => { + addon.reentrant_try_init(() => {}); + }); + assert.fail("should have panicked on re-entrancy"); + } catch (expected) { } + + try { + // 3. Global 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 global after initial failure"); + } + }); + it("should allocate separate globals for each addon instance", (cb) => { let mainThreadId = addon.get_or_init_thread_id(NaN); assert(!Number.isNaN(mainThreadId)); diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index afb0a3978..2a2a5032f 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -53,3 +53,22 @@ pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { let id: &u32 = THREAD_ID.get_or_init(&mut cx, id); Ok(cx.number(*id)) } + +static REENTRANT_GLOBAL: Global = Global::new(); + +pub fn reentrant_try_init(mut cx: FunctionContext) -> JsResult { + let f = cx.argument::(0)?; + let n = REENTRANT_GLOBAL.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_GLOBAL.get(&mut cx).cloned(); + match value { + Some(n) => Ok(cx.number(n).upcast()), + None => Ok(cx.null().upcast()), + } +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 3ff9fa788..05da9f406 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -341,6 +341,8 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { 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)?; Ok(()) } From b28f439a71952472bb0f40400db2a03af21d4d8e Mon Sep 17 00:00:00 2001 From: David Herman Date: Tue, 31 May 2022 20:12:50 -0700 Subject: [PATCH 10/33] Use `GlobalCellValue` shorthand in type definition of `GlobalCell`. --- crates/neon/src/lifecycle.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index cb04c92e8..4d8db8ff6 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -69,17 +69,17 @@ pub(crate) struct GlobalTable { cells: Vec, } +pub(crate) type GlobalCellValue = Box; + pub(crate) enum GlobalCell { /// Uninitialized state. Uninit, /// Intermediate "dirty" state representing the middle of a `get_or_try_init` transaction. Trying, /// Fully initialized state. - Init(Box), + Init(GlobalCellValue), } -pub(crate) type GlobalCellValue = Box; - impl GlobalCell { /// Establish the initial state at the beginning of the initialization protocol. /// This method ensures that re-entrant initialization always panics (i.e. when From bbc4f2283e8336ae2564e9ab84fcf5241fad86c0 Mon Sep 17 00:00:00 2001 From: David Herman Date: Tue, 31 May 2022 20:17:48 -0700 Subject: [PATCH 11/33] Prettier fixups --- test/napi/lib/workers.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index 546835997..fbee28d8d 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -1,5 +1,10 @@ const assert = require("assert"); -const { Worker, isMainThread, parentPort, threadId } = require("worker_threads"); +const { + Worker, + isMainThread, + parentPort, + threadId, +} = require("worker_threads"); const addon = require(".."); @@ -136,7 +141,7 @@ describe("Globals", () => { addon.reentrant_try_init(() => {}); }); assert.fail("should have panicked on re-entrancy"); - } catch (expected) { } + } catch (expected) {} try { // 3. Global should still be uninitialized @@ -158,7 +163,7 @@ describe("Globals", () => { const worker = new Worker(__filename); worker.once("message", (message) => { - assert.strictEqual(typeof message, 'number'); + assert.strictEqual(typeof message, "number"); assert.notStrictEqual(message, mainThreadId); let mainThreadIdAgain = addon.get_or_init_thread_id(NaN); assert(!Number.isNaN(mainThreadIdAgain)); From ff75d2d473210f78f2bf316fd797e1ccde526e20 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 12:58:25 -0700 Subject: [PATCH 12/33] Add a test for storing rooted objects in instance globals --- test/napi/lib/workers.js | 5 +++++ test/napi/src/js/workers.rs | 16 ++++++++++++++++ test/napi/src/lib.rs | 2 ++ 3 files changed, 23 insertions(+) diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index fbee28d8d..96e4b9aaf 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -131,6 +131,11 @@ describe("Globals", () => { assert.strictEqual(lookedUpId, threadId); }); + it("should be able to store rooted objects in instance globals", () => { + 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()); diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 2a2a5032f..9de5a93f4 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -72,3 +72,19 @@ pub fn get_reentrant_value(mut cx: FunctionContext) -> JsResult { None => Ok(cx.null().upcast()), } } + +static GLOBAL_OBJECT: Global> = Global::new(); + +pub fn stash_global_object(mut cx: FunctionContext) -> JsResult { + let global = cx.global(); + let global: Root = Root::new(&mut cx, &global); + GLOBAL_OBJECT.get_or_init(&mut cx, 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 05da9f406..9fbad123f 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -343,6 +343,8 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { 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(()) } From d9b8251463e667bcb467dbb41d9401511c9704db Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 13:32:20 -0700 Subject: [PATCH 13/33] Minor style cleanup for `TryInitTransaction::is_trying()` --- crates/neon/src/lifecycle.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 4d8db8ff6..a1d0c3e8b 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -216,10 +216,9 @@ impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> { } fn is_trying(&mut self) -> bool { - if let GlobalCell::Trying = *self.cell() { - true - } else { - false + match self.cell() { + GlobalCell::Trying => true, + _ => false, } } } From a66e511c241352f481103e82210f5d856271f099 Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 1 Jun 2022 20:16:49 -0700 Subject: [PATCH 14/33] Global::new() can use the derived Default::default() Co-authored-by: K.J. Valencik --- crates/neon/src/instance/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 4584709b6..e6a0cb735 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -26,10 +26,7 @@ impl Global { /// Creates a new global value. This method is `const`, so it can be assigned to /// static variables. pub const fn new() -> Self { - Self { - _type: PhantomData, - id: OnceCell::new(), - } + Default::default() } fn id(&self) -> usize { From 507332d698699a14865db074b4cc28da033ab35a Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 20:21:07 -0700 Subject: [PATCH 15/33] Undo previous commit, which failed to type-check. --- crates/neon/src/instance/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index e6a0cb735..4584709b6 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -26,7 +26,10 @@ impl Global { /// Creates a new global value. This method is `const`, so it can be assigned to /// static variables. pub const fn new() -> Self { - Default::default() + Self { + _type: PhantomData, + id: OnceCell::new(), + } } fn id(&self) -> usize { From 06969010e19464ce595526a9caf4e76612fc219f Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 20:31:52 -0700 Subject: [PATCH 16/33] Test that inner closure isn't even executed on re-entrant initialization. --- test/napi/lib/workers.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index 96e4b9aaf..d842c0541 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -141,12 +141,21 @@ describe("Globals", () => { 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(() => {}); + addon.reentrant_try_init(() => { + innerClosureExecuted = true; + }); }); assert.fail("should have panicked on re-entrancy"); - } catch (expected) {} + } catch (expected) { + assert.strictEqual( + innerClosureExecuted, + false, + "inner closure should not have executed" + ); + } try { // 3. Global should still be uninitialized From 574877b1b2164dbfc9a5897bf2378dc68341092e Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 20:47:00 -0700 Subject: [PATCH 17/33] Make `get_or_try_init` generic for any `Result` --- crates/neon/src/instance/mod.rs | 4 ++-- crates/neon/src/lifecycle.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 4584709b6..1f2a9ba91 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -95,10 +95,10 @@ impl Global { /// /// During the execution of `f`, calling any methods on this `Global` that /// attempt to initialize it will panic. - pub fn get_or_try_init<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> NeonResult<&'cx T> + 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) -> NeonResult, + F: FnOnce(&mut C) -> Result, { // Safety: The type bound Global and the fact that every Global has a unique // id guarantees that the cell is only ever assigned instances of type T. diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index a1d0c3e8b..62ee2d528 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -139,14 +139,14 @@ impl GlobalCell { GlobalCell::get(cx, id).unwrap() } - pub(crate) fn get_or_try_init<'cx, 'a, C, F>( + pub(crate) fn get_or_try_init<'cx, 'a, C, E, F>( cx: &'a mut C, id: usize, f: F, - ) -> NeonResult<&mut GlobalCellValue> + ) -> Result<&mut GlobalCellValue, E> where C: Context<'cx>, - F: FnOnce(&mut C) -> NeonResult, + F: FnOnce(&mut C) -> Result, { // Kick off a new transaction and drop it before getting the result. { @@ -200,9 +200,9 @@ impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> { /// _Post-condition:_ If this method returns an `Ok` result, the cell is in the /// `GlobalCell::Init` state. - fn run(&mut self, f: F) -> NeonResult<()> + fn run(&mut self, f: F) -> Result<(), E> where - F: FnOnce(&mut C) -> NeonResult, + F: FnOnce(&mut C) -> Result, { if self.is_trying() { let value = f(self.cx)?; From b397e2de83e33f18b005863fd6788d5887710bb3 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 20:49:25 -0700 Subject: [PATCH 18/33] Change "safety" to "unwrap safety" for the `.unwrap()` comment. --- crates/neon/src/instance/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 1f2a9ba91..7dc28ab62 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -44,7 +44,7 @@ impl Global { where C: Context<'cx>, { - // Safety: The type bound Global and the fact that every Global has a unique + // Unwrap safety: The type bound Global and the fact that every Global has a unique // id guarantees that the cell is only ever assigned instances of type T. let r: Option<&T> = GlobalCell::get(cx, self.id()).map(|value| value.downcast_ref().unwrap()); @@ -60,7 +60,7 @@ impl Global { where C: Context<'cx>, { - // Safety: The type bound Global and the fact that every Global has a unique + // Unwrap safety: The type bound Global and the fact that every Global has a unique // id guarantees that the cell is only ever assigned instances of type T. let r: &T = GlobalCell::get_or_init(cx, self.id(), Box::new(value)) .downcast_ref() @@ -78,7 +78,7 @@ impl Global { C: Context<'cx>, F: FnOnce() -> T, { - // Safety: The type bound Global and the fact that every Global has a unique + // Unwrap safety: The type bound Global and the fact that every Global has a unique // id guarantees that the cell is only ever assigned instances of type T. let r: &T = GlobalCell::get_or_init_with(cx, self.id(), || Box::new(f())) .downcast_ref() @@ -100,7 +100,7 @@ impl Global { C: Context<'cx>, F: FnOnce(&mut C) -> Result, { - // Safety: The type bound Global and the fact that every Global has a unique + // Unwrap safety: The type bound Global and the fact that every Global has a unique // id guarantees that the cell is only ever assigned instances of type T. let r: &T = GlobalCell::get_or_try_init(cx, self.id(), |cx| Ok(Box::new(f(cx)?)))? .downcast_ref() From bc7d09ea6808b96c24007cb5a15a116c6fd57da4 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 1 Jun 2022 21:04:14 -0700 Subject: [PATCH 19/33] Use `get_or_try_init` for the global object test, to only root the object when needed --- test/napi/src/js/workers.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 9de5a93f4..1c11e0c91 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -76,9 +76,11 @@ pub fn get_reentrant_value(mut cx: FunctionContext) -> JsResult { static GLOBAL_OBJECT: Global> = Global::new(); pub fn stash_global_object(mut cx: FunctionContext) -> JsResult { - let global = cx.global(); - let global: Root = Root::new(&mut cx, &global); - GLOBAL_OBJECT.get_or_init(&mut cx, global); + 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()) } From 3a0c041327619a3ef395b46f4a32e5baa6147d7c Mon Sep 17 00:00:00 2001 From: David Herman Date: Fri, 3 Jun 2022 01:03:31 -0700 Subject: [PATCH 20/33] Rename `Global` to `Local` and add top-level API docs for the `neon::instance` module. --- crates/neon/src/instance/mod.rs | 71 ++++++++++++++++------- crates/neon/src/lifecycle.rs | 95 +++++++++++++++---------------- crates/neon/src/sys/async_work.rs | 2 +- crates/neon/src/sys/promise.rs | 2 +- test/napi/lib/workers.js | 12 ++-- test/napi/src/js/workers.rs | 12 ++-- 6 files changed, 110 insertions(+), 84 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index 7dc28ab62..d669d3782 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -1,3 +1,31 @@ +//! Instance-local storage. +//! +//! At runtime, an instance of a Node.js addon can contain its own local storage, +//! which can then be shared and accessed as needed between Rust modules. 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 that makes use of a +//! [Tokio runtime](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html) +//! can store the runtime in a [`Local`](Local) cell: +//! +//! ``` +//! # use neon::prelude::*; +//! # use neon::instance::Local; +//! # struct Runtime; +//! # type Result = std::result::Result; +//! # impl Runtime {fn new() -> Result { Err(()) }} +//! static RUNTIME: Local = Local::new(); +//! +//! pub fn runtime<'cx, C: Context<'cx>>(cx: &mut C) -> Result<&'cx Runtime> { +//! RUNTIME.get_or_try_init(cx, |_| Runtime::new()) +//! } +//! ``` +//! +//! Because Node.js supports [worker threads](https://nodejs.org/api/worker_threads.html), +//! a Node.js addon may have multiple instances in a running process. Local +//! storage is instantiated separately for each instance of the addon. + use std::any::Any; use std::marker::PhantomData; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -5,8 +33,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use once_cell::sync::OnceCell; use crate::context::Context; -use crate::lifecycle::GlobalCell; -use crate::result::NeonResult; +use crate::lifecycle::LocalCell; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -14,16 +41,16 @@ fn next_id() -> usize { COUNTER.fetch_add(1, Ordering::SeqCst) } -/// A cell that can be used to allocate data that is global to an instance +/// A cell that can be used to allocate data that is local to an instance /// of a Neon addon. #[derive(Default)] -pub struct Global { +pub struct Local { _type: PhantomData, id: OnceCell, } -impl Global { - /// Creates a new global value. This method is `const`, so it can be assigned to +impl Local { + /// Creates a new local value. This method is `const`, so it can be assigned to /// static variables. pub const fn new() -> Self { Self { @@ -37,32 +64,32 @@ impl Global { } } -impl Global { - /// Gets the current value of the global. Returns `None` if the global has not +impl Local { + /// 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 Global and the fact that every Global has a unique + // Unwrap safety: The type bound Local and the fact that every Local has a unique // id guarantees that the cell is only ever assigned instances of type T. let r: Option<&T> = - GlobalCell::get(cx, self.id()).map(|value| value.downcast_ref().unwrap()); + 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 global, initializing it with `value` if it has + /// Gets the current value of the cell, initializing it with `value` if it has /// not yet been initialized. pub fn get_or_init<'cx, 'a, C>(&self, cx: &'a mut C, value: T) -> &'cx T where C: Context<'cx>, { - // Unwrap safety: The type bound Global and the fact that every Global has a unique + // Unwrap safety: The type bound Local and the fact that every Local has a unique // id guarantees that the cell is only ever assigned instances of type T. - let r: &T = GlobalCell::get_or_init(cx, self.id(), Box::new(value)) + let r: &T = LocalCell::get_or_init(cx, self.id(), Box::new(value)) .downcast_ref() .unwrap(); @@ -71,16 +98,16 @@ impl Global { unsafe { std::mem::transmute::<&'a T, &'cx T>(r) } } - /// Gets the current value of the global, initializing it with the result of + /// 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_with<'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 Global and the fact that every Global has a unique + // Unwrap safety: The type bound Local and the fact that every Local has a unique // id guarantees that the cell is only ever assigned instances of type T. - let r: &T = GlobalCell::get_or_init_with(cx, self.id(), || Box::new(f())) + let r: &T = LocalCell::get_or_init_with(cx, self.id(), || Box::new(f())) .downcast_ref() .unwrap(); @@ -89,20 +116,20 @@ impl Global { unsafe { std::mem::transmute::<&'a T, &'cx T>(r) } } - /// Gets the current value of the global, initializing it with the result of + /// 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. /// - /// During the execution of `f`, calling any methods on this `Global` that + /// During the execution of `f`, calling any methods on this `Local` 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 Global and the fact that every Global has a unique + // Unwrap safety: The type bound Local and the fact that every Local has a unique // id guarantees that the cell is only ever assigned instances of type T. - let r: &T = GlobalCell::get_or_try_init(cx, self.id(), |cx| Ok(Box::new(f(cx)?)))? + let r: &T = LocalCell::get_or_try_init(cx, self.id(), |cx| Ok(Box::new(f(cx)?)))? .downcast_ref() .unwrap(); @@ -112,8 +139,8 @@ impl Global { } } -impl Global { - /// Gets the current value of the global, initializing it with the default value +impl Local { + /// 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 diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 62ee2d528..1274fe3b6 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -21,7 +21,6 @@ use crate::{ context::Context, event::Channel, handle::root::NapiRef, - result::NeonResult, sys::{lifecycle, raw::Env, tsfn::ThreadsafeFunction}, types::promise::NodeApiDeferred, }; @@ -60,50 +59,50 @@ pub(crate) struct InstanceData { /// Shared `Channel` that is cloned to be returned by the `cx.channel()` method shared_channel: Channel, - /// Table of user-defined global cells. - globals: GlobalTable, + /// Table of user-defined instance-local cells. + locals: LocalTable, } #[derive(Default)] -pub(crate) struct GlobalTable { - cells: Vec, +pub(crate) struct LocalTable { + cells: Vec, } -pub(crate) type GlobalCellValue = Box; +pub(crate) type LocalCellValue = Box; -pub(crate) enum GlobalCell { +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(GlobalCellValue), + Init(LocalCellValue), } -impl GlobalCell { +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() -> GlobalCell, + F: FnOnce() -> LocalCell, { match self { - GlobalCell::Uninit => { + LocalCell::Uninit => { *self = f(); } - GlobalCell::Trying => panic!("attempt to reinitialize Global during initialization"), - GlobalCell::Init(_) => {} + 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 GlobalCellValue> + pub(crate) fn get<'cx, 'a, C>(cx: &'a mut C, id: usize) -> Option<&mut LocalCellValue> where C: Context<'cx>, { - let cell = InstanceData::globals(cx).get(id); + let cell = InstanceData::locals(cx).get(id); match cell { - GlobalCell::Init(ref mut b) => Some(b), + LocalCell::Init(ref mut b) => Some(b), _ => None, } } @@ -111,42 +110,42 @@ impl GlobalCell { pub(crate) fn get_or_init<'cx, 'a, C>( cx: &'a mut C, id: usize, - value: GlobalCellValue, - ) -> &mut GlobalCellValue + value: LocalCellValue, + ) -> &mut LocalCellValue where C: Context<'cx>, { - InstanceData::globals(cx) + InstanceData::locals(cx) .get(id) - .pre_init(|| GlobalCell::Init(value)); + .pre_init(|| LocalCell::Init(value)); - GlobalCell::get(cx, id).unwrap() + LocalCell::get(cx, id).unwrap() } pub(crate) fn get_or_init_with<'cx, 'a, C, F>( cx: &'a mut C, id: usize, f: F, - ) -> &mut GlobalCellValue + ) -> &mut LocalCellValue where C: Context<'cx>, - F: FnOnce() -> GlobalCellValue, + F: FnOnce() -> LocalCellValue, { - InstanceData::globals(cx) + InstanceData::locals(cx) .get(id) - .pre_init(|| GlobalCell::Init(f())); + .pre_init(|| LocalCell::Init(f())); - GlobalCell::get(cx, id).unwrap() + 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 GlobalCellValue, E> + ) -> Result<&mut LocalCellValue, E> where C: Context<'cx>, - F: FnOnce(&mut C) -> Result, + F: FnOnce(&mut C) -> Result, { // Kick off a new transaction and drop it before getting the result. { @@ -155,18 +154,18 @@ impl GlobalCell { } // If we're here, the transaction has succeeded, so get the result. - Ok(GlobalCell::get(cx, id).unwrap()) + Ok(LocalCell::get(cx, id).unwrap()) } } -impl Default for GlobalCell { +impl Default for LocalCell { fn default() -> Self { - GlobalCell::Uninit + LocalCell::Uninit } } -impl GlobalTable { - pub(crate) fn get(&mut self, index: usize) -> &mut GlobalCell { +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); } @@ -174,13 +173,13 @@ impl GlobalTable { } } -/// An RAII implementation of `GlobalCell::get_or_try_init`, which ensures that +/// 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 (`GlobalCell::Trying`), so that any additional re-entrant attempts to +/// 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 -/// `GlobalCell::Uninit` if it fails or `GlobalCell::Init` if it succeeds. +/// `LocalCell::Uninit` if it fails or `LocalCell::Init` if it succeeds. struct TryInitTransaction<'cx, 'a, C: Context<'cx>> { cx: &'a mut C, id: usize, @@ -194,30 +193,30 @@ impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> { id, _lifetime: PhantomData, }; - tx.cell().pre_init(|| GlobalCell::Trying); + tx.cell().pre_init(|| LocalCell::Trying); tx } /// _Post-condition:_ If this method returns an `Ok` result, the cell is in the - /// `GlobalCell::Init` state. + /// `LocalCell::Init` state. fn run(&mut self, f: F) -> Result<(), E> where - F: FnOnce(&mut C) -> Result, + F: FnOnce(&mut C) -> Result, { if self.is_trying() { let value = f(self.cx)?; - *self.cell() = GlobalCell::Init(value); + *self.cell() = LocalCell::Init(value); } Ok(()) } - fn cell(&mut self) -> &mut GlobalCell { - InstanceData::globals(self.cx).get(self.id) + fn cell(&mut self) -> &mut LocalCell { + InstanceData::locals(self.cx).get(self.id) } fn is_trying(&mut self) -> bool { match self.cell() { - GlobalCell::Trying => true, + LocalCell::Trying => true, _ => false, } } @@ -226,7 +225,7 @@ impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> { impl<'cx, 'a, C: Context<'cx>> Drop for TryInitTransaction<'cx, 'a, C> { fn drop(&mut self) { if self.is_trying() { - *self.cell() = GlobalCell::Uninit; + *self.cell() = LocalCell::Uninit; } } } @@ -282,7 +281,7 @@ impl InstanceData { id: InstanceId::next(), drop_queue: Arc::new(drop_queue), shared_channel, - globals: GlobalTable::default(), + locals: LocalTable::default(), }; unsafe { &mut *lifecycle::set_instance_data(env, data) } @@ -308,8 +307,8 @@ impl InstanceData { InstanceData::get(cx).id } - /// Helper to return a reference to the `globals` field of `InstanceData`. - pub(crate) fn globals<'cx, C: Context<'cx>>(cx: &mut C) -> &mut GlobalTable { - &mut InstanceData::get(cx).globals + /// 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/test/napi/lib/workers.js b/test/napi/lib/workers.js index d842c0541..64ff0c10e 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -124,14 +124,14 @@ describe("Worker / Root Tagging Tests", () => { }); }); -describe("Globals", () => { - it("should be able to read an instance global from the main thread", () => { +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 globals", () => { + it("should be able to store rooted objects in instance locals", () => { addon.stash_global_object(); assert.strictEqual(global, addon.unstash_global_object()); }); @@ -158,7 +158,7 @@ describe("Globals", () => { } try { - // 3. Global should still be uninitialized + // 3. Local should still be uninitialized assert.strictEqual(null, addon.get_reentrant_value()); // 4. Successful fallible initialization @@ -166,11 +166,11 @@ describe("Globals", () => { assert.strictEqual(42, result); assert.strictEqual(42, addon.get_reentrant_value()); } catch (unexpected) { - assert.fail("couldn't set reentrant global after initial failure"); + assert.fail("couldn't set reentrant local after initial failure"); } }); - it("should allocate separate globals for each addon instance", (cb) => { + it("should allocate separate locals for each addon instance", (cb) => { let mainThreadId = addon.get_or_init_thread_id(NaN); assert(!Number.isNaN(mainThreadId)); diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 1c11e0c91..566f8ba63 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -2,7 +2,7 @@ use std::sync::Mutex; use once_cell::sync::{Lazy, OnceCell}; -use neon::instance::Global; +use neon::instance::Local; use neon::prelude::*; pub fn get_and_replace(mut cx: FunctionContext) -> JsResult { @@ -46,7 +46,7 @@ pub fn get_or_init_clone(mut cx: FunctionContext) -> JsResult { Ok(o.clone(&mut cx).into_inner(&mut cx)) } -static THREAD_ID: Global = Global::new(); +static THREAD_ID: Local = Local::new(); pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { let id = cx.argument::(0)?.value(&mut cx) as u32; @@ -54,11 +54,11 @@ pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { Ok(cx.number(*id)) } -static REENTRANT_GLOBAL: Global = Global::new(); +static REENTRANT_LOCAL: Local = Local::new(); pub fn reentrant_try_init(mut cx: FunctionContext) -> JsResult { let f = cx.argument::(0)?; - let n = REENTRANT_GLOBAL.get_or_try_init(&mut cx, |cx| { + let n = REENTRANT_LOCAL.get_or_try_init(&mut cx, |cx| { f.call_with(cx).exec(cx)?; Ok(42) })?; @@ -66,14 +66,14 @@ pub fn reentrant_try_init(mut cx: FunctionContext) -> JsResult { } pub fn get_reentrant_value(mut cx: FunctionContext) -> JsResult { - let value = REENTRANT_GLOBAL.get(&mut cx).cloned(); + 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: Global> = Global::new(); +static GLOBAL_OBJECT: Local> = Local::new(); pub fn stash_global_object(mut cx: FunctionContext) -> JsResult { GLOBAL_OBJECT.get_or_try_init(&mut cx, |cx| { From 33388778cdbbec387558b135bfe6e15954fe8813 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 09:19:58 -0700 Subject: [PATCH 21/33] Improvements to `neon::instance` top-level API docs --- crates/neon/src/instance/mod.rs | 82 ++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/crates/neon/src/instance/mod.rs b/crates/neon/src/instance/mod.rs index d669d3782..2b717a577 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/instance/mod.rs @@ -5,26 +5,84 @@ //! be useful for setting up long-lived state that needs to be shared between calls //! of an addon's APIs. //! -//! For example, an addon that makes use of a -//! [Tokio runtime](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html) -//! can store the runtime in a [`Local`](Local) cell: +//! For example, an addon may wish to track the [thread ID][threadId] of each of its +//! instances: //! //! ``` //! # use neon::prelude::*; //! # use neon::instance::Local; -//! # struct Runtime; -//! # type Result = std::result::Result; -//! # impl Runtime {fn new() -> Result { Err(()) }} -//! static RUNTIME: Local = Local::new(); +//! static THREAD_ID: Local = Local::new(); //! -//! pub fn runtime<'cx, C: Context<'cx>>(cx: &mut C) -> Result<&'cx Runtime> { -//! RUNTIME.get_or_try_init(cx, |_| Runtime::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 threadId: Handle = worker.get(cx, "threadId")?; +//! Ok(threadId.value(cx) as u32) +//! }).cloned() //! } //! ``` //! -//! Because Node.js supports [worker threads](https://nodejs.org/api/worker_threads.html), -//! a Node.js addon may have multiple instances in a running process. Local -//! storage is instantiated separately for each instance of the addon. +//! ### 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 immutable +//! data. +//! +//! However, since the addition of [worker threads][workers] in Node v10, +//! modules can be instantiated multiple times in a single Node process. This means +//! that while the dynamically-loaded binary library (i.e., the Rust implementation of +//! the addon) is only loaded once in the running process, but its `main()` function +//! is executed multiple times with distinct module objects, once per application thread: +//! +//! ![The Node.js addon lifecycle, described in detail below.][lifecycle] +//! +//! This means that any instance-local data needs to be initialized separately for each +//! instance of the addon. This module provides a simple container type, [`Local`](Local), +//! for allocating and initializing instance-local data. For example, a custom datatype +//! cannot be shared across separate threads and must be instance-local: +//! +//! ``` +//! # use neon::prelude::*; +//! # use neon::instance::Local; +//! # fn initialize_my_datatype<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { unimplemented!() } +//! static MY_CONSTRUCTOR: Local> = Local::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)) +//! } +//! ``` +//! +//! ### When to Use Instance-Local Storage +//! +//! Single-threaded applications don't generally need to worry about instance data. +//! There are two cases where Neon apps should consider storing static data in a +//! `Local` 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 instance-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 instance-local data in case the +//! addon ends up instantiated by multiple threads in some future application. +//! +//! [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; From 42eec50ff674e58ffbb36ee116c0f1c492467d6c Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 10:37:36 -0700 Subject: [PATCH 22/33] Rename `neon::instance` to `neon::thread` and `Local` to `LocalKey` --- crates/neon/src/lib.rs | 4 +- crates/neon/src/{instance => thread}/mod.rs | 58 ++++++++++++--------- test/napi/src/js/workers.rs | 8 +-- 3 files changed, 39 insertions(+), 31 deletions(-) rename crates/neon/src/{instance => thread}/mod.rs (79%) diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 6190b4e7d..f34d75744 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -81,14 +81,14 @@ pub mod context; pub mod event; pub mod handle; -#[cfg(feature = "napi-6")] -pub mod instance; pub mod meta; pub mod object; 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/instance/mod.rs b/crates/neon/src/thread/mod.rs similarity index 79% rename from crates/neon/src/instance/mod.rs rename to crates/neon/src/thread/mod.rs index 2b717a577..365b6a5e8 100644 --- a/crates/neon/src/instance/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -1,4 +1,4 @@ -//! Instance-local storage. +//! 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 between Rust modules. This can @@ -10,8 +10,8 @@ //! //! ``` //! # use neon::prelude::*; -//! # use neon::instance::Local; -//! static THREAD_ID: Local = Local::new(); +//! # 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| { @@ -47,16 +47,16 @@ //! //! ![The Node.js addon lifecycle, described in detail below.][lifecycle] //! -//! This means that any instance-local data needs to be initialized separately for each -//! instance of the addon. This module provides a simple container type, [`Local`](Local), -//! for allocating and initializing instance-local data. For example, a custom datatype -//! cannot be shared across separate threads and must be instance-local: +//! 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. For example, a custom datatype +//! cannot be shared across separate threads and must be thread-local: //! //! ``` //! # use neon::prelude::*; -//! # use neon::instance::Local; +//! # use neon::thread::LocalKey; //! # fn initialize_my_datatype<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { unimplemented!() } -//! static MY_CONSTRUCTOR: Local> = Local::new(); +//! 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| { @@ -67,17 +67,17 @@ //! } //! ``` //! -//! ### When to Use Instance-Local Storage +//! ### When to Use Thread-Local Storage //! -//! Single-threaded applications don't generally need to worry about instance data. +//! 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 -//! `Local` storage cell: +//! `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 instance-local data. +//! 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 instance-local data in case the +//! 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. //! //! [lifecycle]: https://raw.githubusercontent.com/neon-bindings/neon/main/doc/lifecycle.png @@ -99,15 +99,23 @@ fn next_id() -> usize { COUNTER.fetch_add(1, Ordering::SeqCst) } -/// A cell that can be used to allocate data that is local to an instance -/// of a Neon addon. +/// 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 Local { +pub struct LocalKey { _type: PhantomData, id: OnceCell, } -impl Local { +impl LocalKey { /// Creates a new local value. This method is `const`, so it can be assigned to /// static variables. pub const fn new() -> Self { @@ -122,14 +130,14 @@ impl Local { } } -impl Local { +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 Local and the fact that every Local has a unique + // 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()); @@ -145,7 +153,7 @@ impl Local { where C: Context<'cx>, { - // Unwrap safety: The type bound Local and the fact that every Local has a unique + // 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(value)) .downcast_ref() @@ -163,7 +171,7 @@ impl Local { C: Context<'cx>, F: FnOnce() -> T, { - // Unwrap safety: The type bound Local and the fact that every Local has a unique + // 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_with(cx, self.id(), || Box::new(f())) .downcast_ref() @@ -178,14 +186,14 @@ impl Local { /// calling `f` if it has not yet been initialized. Returns `Err` if the /// callback triggers a JavaScript exception. /// - /// During the execution of `f`, calling any methods on this `Local` that + /// 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 Local and the fact that every Local has a unique + // 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() @@ -197,7 +205,7 @@ impl Local { } } -impl Local { +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 diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 566f8ba63..2a30b964a 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -2,8 +2,8 @@ use std::sync::Mutex; use once_cell::sync::{Lazy, OnceCell}; -use neon::instance::Local; use neon::prelude::*; +use neon::thread::LocalKey; pub fn get_and_replace(mut cx: FunctionContext) -> JsResult { static OBJECT: Lazy>>> = Lazy::new(Default::default); @@ -46,7 +46,7 @@ pub fn get_or_init_clone(mut cx: FunctionContext) -> JsResult { Ok(o.clone(&mut cx).into_inner(&mut cx)) } -static THREAD_ID: Local = Local::new(); +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; @@ -54,7 +54,7 @@ pub fn get_or_init_thread_id(mut cx: FunctionContext) -> JsResult { Ok(cx.number(*id)) } -static REENTRANT_LOCAL: Local = Local::new(); +static REENTRANT_LOCAL: LocalKey = LocalKey::new(); pub fn reentrant_try_init(mut cx: FunctionContext) -> JsResult { let f = cx.argument::(0)?; @@ -73,7 +73,7 @@ pub fn get_reentrant_value(mut cx: FunctionContext) -> JsResult { } } -static GLOBAL_OBJECT: Local> = Local::new(); +static GLOBAL_OBJECT: LocalKey> = LocalKey::new(); pub fn stash_global_object(mut cx: FunctionContext) -> JsResult { GLOBAL_OBJECT.get_or_try_init(&mut cx, |cx| { From 85d99f5c1ee3227cf3eccc36672ab24717d442d0 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 14:12:35 -0700 Subject: [PATCH 23/33] Some more documentation text about the relationships between instances and threads. --- crates/neon/src/thread/mod.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 365b6a5e8..fc37d62b6 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -49,8 +49,12 @@ //! //! 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. For example, a custom datatype -//! cannot be shared across separate threads and must be thread-local: +//! 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 defines a custom datatype, which cannot be used across +//! separate threads. The constructor for the datatype can be [rooted](crate::handle::Root) and +//! saved in thread-local storage: //! //! ``` //! # use neon::prelude::*; @@ -67,6 +71,10 @@ //! } //! ``` //! +//! Notice that if this code were implemented without a `LocalKey`, it would fail 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. @@ -80,6 +88,13 @@ //! 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? +//! +//! Because 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 tying data to 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 From c906fbc97e1a64fd04977a60c3945f72723d02c4 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 18:23:55 -0700 Subject: [PATCH 24/33] Addresses some of @kjvalencik's review suggestions: - Eliminate `get_or_init` and rename `get_or_init_with` to `get_or_init` - Add `# Panic` section to doc comment --- crates/neon/src/lifecycle.rs | 21 +-------------------- crates/neon/src/thread/mod.rs | 25 +++++-------------------- test/napi/src/js/workers.rs | 2 +- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/crates/neon/src/lifecycle.rs b/crates/neon/src/lifecycle.rs index 1274fe3b6..ae2fc877b 100644 --- a/crates/neon/src/lifecycle.rs +++ b/crates/neon/src/lifecycle.rs @@ -107,26 +107,7 @@ impl LocalCell { } } - pub(crate) fn get_or_init<'cx, 'a, C>( - cx: &'a mut C, - id: usize, - value: LocalCellValue, - ) -> &mut LocalCellValue - where - C: Context<'cx>, - { - InstanceData::locals(cx) - .get(id) - .pre_init(|| LocalCell::Init(value)); - - LocalCell::get(cx, id).unwrap() - } - - pub(crate) fn get_or_init_with<'cx, 'a, C, F>( - cx: &'a mut C, - id: usize, - f: F, - ) -> &mut LocalCellValue + 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, diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index fc37d62b6..9a5078a15 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -162,33 +162,16 @@ impl LocalKey { unsafe { std::mem::transmute::, Option<&'cx T>>(r) } } - /// Gets the current value of the cell, initializing it with `value` if it has - /// not yet been initialized. - pub fn get_or_init<'cx, 'a, C>(&self, cx: &'a mut C, value: T) -> &'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: &T = LocalCell::get_or_init(cx, self.id(), Box::new(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::<&'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. - pub fn get_or_init_with<'cx, 'a, C, F>(&self, cx: &'a mut C, f: F) -> &'cx T + 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_with(cx, self.id(), || Box::new(f())) + let r: &T = LocalCell::get_or_init(cx, self.id(), || Box::new(f())) .downcast_ref() .unwrap(); @@ -201,6 +184,8 @@ impl LocalKey { /// 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> @@ -227,6 +212,6 @@ impl LocalKey { where C: Context<'cx>, { - self.get_or_init_with(cx, Default::default) + self.get_or_init(cx, Default::default) } } diff --git a/test/napi/src/js/workers.rs b/test/napi/src/js/workers.rs index 2a30b964a..83ea3cbc0 100644 --- a/test/napi/src/js/workers.rs +++ b/test/napi/src/js/workers.rs @@ -50,7 +50,7 @@ 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); + let id: &u32 = THREAD_ID.get_or_init(&mut cx, || id); Ok(cx.number(*id)) } From 0f57620f4ff4c2aa7406b536fb45da58fc95058d Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 8 Jun 2022 18:25:49 -0700 Subject: [PATCH 25/33] Clarify doc text Co-authored-by: K.J. Valencik --- crates/neon/src/thread/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 9a5078a15..a43d85ea4 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -1,7 +1,7 @@ //! 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 between Rust modules. This can +//! 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. //! From 14a2fe472e9f0db2489913e5c5f1cce96128059d Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 8 Jun 2022 18:26:05 -0700 Subject: [PATCH 26/33] Idiomatic Rust variable name in doc example Co-authored-by: K.J. Valencik --- crates/neon/src/thread/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index a43d85ea4..89a211c68 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -20,8 +20,8 @@ //! let worker: Handle = require.call_with(cx) //! .arg(cx.string("node:worker_threads")) //! .apply(cx)?; -//! let threadId: Handle = worker.get(cx, "threadId")?; -//! Ok(threadId.value(cx) as u32) +//! let thread_id: Handle = worker.get(cx, "threadId")?; +//! Ok(thread_id.value(cx) as u32) //! }).cloned() //! } //! ``` From 019c2d37f8b460e2449888c8954636d99eb981d4 Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 8 Jun 2022 18:26:25 -0700 Subject: [PATCH 27/33] Link to `neon::main` docs in doc comment Co-authored-by: K.J. Valencik --- crates/neon/src/thread/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 89a211c68..6e7584e8e 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -42,7 +42,7 @@ //! However, since the addition of [worker threads][workers] in Node v10, //! modules can be instantiated multiple times in a single Node process. This means //! that while the dynamically-loaded binary library (i.e., the Rust implementation of -//! the addon) is only loaded once in the running process, but its `main()` function +//! the addon) is only loaded once in the running process, but its [`#[main]`](neon::main) function //! is executed multiple times with distinct module objects, once per application thread: //! //! ![The Node.js addon lifecycle, described in detail below.][lifecycle] From 58f80b3c2370b45f13459a3e44596b8fb00b374b Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 8 Jun 2022 18:26:47 -0700 Subject: [PATCH 28/33] Clarifying doc text about cross-thread sharing Co-authored-by: K.J. Valencik --- crates/neon/src/thread/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 6e7584e8e..454595f99 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -52,8 +52,8 @@ //! 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 defines a custom datatype, which cannot be used across -//! separate threads. The constructor for the datatype can be [rooted](crate::handle::Root) and +//! A common example is when an addon needs to maintain a reference to a JavaScript value. References cannot +//! be used across separate threads. The constructor for the datatype can be [rooted](crate::handle::Root) and //! saved in thread-local storage: //! //! ``` From f3cc0ab7c690a09cdb0924899a405788dcf14c92 Mon Sep 17 00:00:00 2001 From: Dave Herman Date: Wed, 8 Jun 2022 18:27:07 -0700 Subject: [PATCH 29/33] s/fail/panic/ in doc text Co-authored-by: K.J. Valencik --- crates/neon/src/thread/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 454595f99..5f22f0e1e 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -71,7 +71,7 @@ //! } //! ``` //! -//! Notice that if this code were implemented without a `LocalKey`, it would fail whenever +//! 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). //! From 2005e6103317d4d164ba5ce8e5730a115c0d6847 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 18:34:14 -0700 Subject: [PATCH 30/33] More docs improvements: - Add addon lifecycle diagram - Add panics notes to `Root::{into_inner, to_inner}` - Replace "immutable" with "thread-safe" in list of safe cases --- crates/neon/src/handle/root.rs | 12 +++++++++++- crates/neon/src/thread/mod.rs | 2 +- doc/lifecycle.png | Bin 0 -> 37752 bytes 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 doc/lifecycle.png 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/thread/mod.rs b/crates/neon/src/thread/mod.rs index 5f22f0e1e..3f27cf9a9 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -36,7 +36,7 @@ //! ``` //! //! 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 immutable +//! process. This works fine for single-threaded applications, or global thread-safe //! data. //! //! However, since the addition of [worker threads][workers] in Node v10, diff --git a/doc/lifecycle.png b/doc/lifecycle.png new file mode 100644 index 0000000000000000000000000000000000000000..8d73c45f0dfb900ddbc336e8b61844907c6f4623 GIT binary patch literal 37752 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 literal 0 HcmV?d00001 From d50ca6377eeb562e2a34c4018ade9781aa80b402 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 19:20:49 -0700 Subject: [PATCH 31/33] Link to `crate::main` in the docs instead of `neon::main` --- crates/neon/src/thread/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 3f27cf9a9..4ef5563c3 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -42,8 +42,9 @@ //! However, since the addition of [worker threads][workers] in Node v10, //! modules can be instantiated multiple times in a single Node process. This means //! that while the dynamically-loaded binary library (i.e., the Rust implementation of -//! the addon) is only loaded once in the running process, but its [`#[main]`](neon::main) function -//! is executed multiple times with distinct module objects, once per application thread: +//! the addon) is only loaded once in the running process, but its [`#[main]`](crate::main) +//! function is executed multiple times with distinct module objects, once per application +//! thread: //! //! ![The Node.js addon lifecycle, described in detail below.][lifecycle] //! From ece0c02cc2a259087cffe368e1d0cc9fb5288e97 Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 8 Jun 2022 19:27:07 -0700 Subject: [PATCH 32/33] More copy editing in the docs --- crates/neon/src/thread/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 4ef5563c3..74186e19d 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -53,9 +53,10 @@ //! 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. References cannot -//! be used across separate threads. The constructor for the datatype can be [rooted](crate::handle::Root) and -//! saved in thread-local storage: +//! 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 refereces 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::*; From 310cb5beb41f9de17bff4699e716d9e07ce1139f Mon Sep 17 00:00:00 2001 From: David Herman Date: Thu, 9 Jun 2022 07:17:45 -0700 Subject: [PATCH 33/33] A few last copy-editing nits --- crates/neon/src/thread/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/neon/src/thread/mod.rs b/crates/neon/src/thread/mod.rs index 74186e19d..34a21fc05 100644 --- a/crates/neon/src/thread/mod.rs +++ b/crates/neon/src/thread/mod.rs @@ -40,10 +40,10 @@ //! data. //! //! However, since the addition of [worker threads][workers] in Node v10, -//! modules can be instantiated multiple times in a single Node process. This means -//! that while the dynamically-loaded binary library (i.e., the Rust implementation of -//! the addon) is only loaded once in the running process, but its [`#[main]`](crate::main) -//! function is executed multiple times with distinct module objects, once per application +//! 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] @@ -54,7 +54,7 @@ //! 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 refereces cannot +//! 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: //! @@ -92,9 +92,9 @@ //! //! ### Why Not Use Standard TLS? //! -//! Because the JavaScript engine may not tie JavaScript threads 1:1 to system threads, +//! 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 tying data to a JavaScript thread. +//! 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