Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feature(neon): API for thread-local data #902

Merged
merged 33 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
94111d4
Add `neon::instance::Global` API for storing instance-global data.
dherman May 24, 2022
f872b76
Address review comments:
dherman May 25, 2022
6f12663
Use `'cx` as the inner lifetime of contexts.
dherman May 25, 2022
18e60f1
Get rid of `[]` overloading for `GlobalTable` in favor of an inherent…
dherman May 25, 2022
b8b089b
Immutable version of API:
dherman May 25, 2022
cf0d32a
Explicitly name the types in the transmute
dherman May 25, 2022
f5cd0ba
Explicitly name the types in the transmute.
dherman May 25, 2022
aee77f4
Add `get_or_try_init` and `get_or_init_default`
dherman May 27, 2022
9075b76
Protect re-entrant cases with "dirty" state checking
dherman Jun 1, 2022
b28f439
Use `GlobalCellValue` shorthand in type definition of `GlobalCell`.
dherman Jun 1, 2022
bbc4f22
Prettier fixups
dherman Jun 1, 2022
ff75d2d
Add a test for storing rooted objects in instance globals
dherman Jun 1, 2022
d9b8251
Minor style cleanup for `TryInitTransaction::is_trying()`
dherman Jun 1, 2022
a66e511
Global::new() can use the derived Default::default()
dherman Jun 2, 2022
507332d
Undo previous commit, which failed to type-check.
dherman Jun 2, 2022
0696901
Test that inner closure isn't even executed on re-entrant initializat…
dherman Jun 2, 2022
574877b
Make `get_or_try_init` generic for any `Result<T, E>`
dherman Jun 2, 2022
b397e2d
Change "safety" to "unwrap safety" for the `.unwrap()` comment.
dherman Jun 2, 2022
bc7d09e
Use `get_or_try_init` for the global object test, to only root the ob…
dherman Jun 2, 2022
3a0c041
Rename `Global` to `Local` and add top-level API docs for the `neon::…
dherman Jun 3, 2022
3338877
Improvements to `neon::instance` top-level API docs
dherman Jun 8, 2022
42eec50
Rename `neon::instance` to `neon::thread` and `Local` to `LocalKey`
dherman Jun 8, 2022
85d99f5
Some more documentation text about the relationships between instance…
dherman Jun 8, 2022
c906fbc
Addresses some of @kjvalencik's review suggestions:
dherman Jun 9, 2022
0f57620
Clarify doc text
dherman Jun 9, 2022
14a2fe4
Idiomatic Rust variable name in doc example
dherman Jun 9, 2022
019c2d3
Link to `neon::main` docs in doc comment
dherman Jun 9, 2022
58f80b3
Clarifying doc text about cross-thread sharing
dherman Jun 9, 2022
f3cc0ab
s/fail/panic/ in doc text
dherman Jun 9, 2022
2005e61
More docs improvements:
dherman Jun 9, 2022
d50ca63
Link to `crate::main` in the docs instead of `neon::main`
dherman Jun 9, 2022
ece0c02
More copy editing in the docs
dherman Jun 9, 2022
310cb5b
A few last copy-editing nits
dherman Jun 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/neon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 11 additions & 1 deletion crates/neon/src/handle/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ impl<T: Object> Root<T> {
}
}

/// 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);
Expand All @@ -160,6 +165,11 @@ impl<T: Object> Root<T> {
/// 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 _) };
Expand Down
2 changes: 2 additions & 0 deletions crates/neon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub mod prelude;
pub mod reflect;
pub mod result;
mod sys;
#[cfg(feature = "napi-6")]
pub mod thread;
pub mod types;

#[doc(hidden)]
Expand Down
177 changes: 170 additions & 7 deletions crates/neon/src/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
//!
//! [napi-docs]: https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis

use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
use std::{
any::Any,
marker::PhantomData,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};

use crate::{
Expand Down Expand Up @@ -54,6 +58,157 @@ pub(crate) struct InstanceData {

/// Shared `Channel` that is cloned to be returned by the `cx.channel()` method
shared_channel: Channel,

/// Table of user-defined instance-local cells.
locals: LocalTable,
}

#[derive(Default)]
pub(crate) struct LocalTable {
cells: Vec<LocalCell>,
}

pub(crate) type LocalCellValue = Box<dyn Any + Send + 'static>;

pub(crate) enum LocalCell {
/// Uninitialized state.
Uninit,
/// Intermediate "dirty" state representing the middle of a `get_or_try_init` transaction.
Trying,
/// Fully initialized state.
Init(LocalCellValue),
}

impl LocalCell {
/// Establish the initial state at the beginning of the initialization protocol.
/// This method ensures that re-entrant initialization always panics (i.e. when
/// an existing `get_or_try_init` is in progress).
fn pre_init<F>(&mut self, f: F)
where
F: FnOnce() -> LocalCell,
{
match self {
LocalCell::Uninit => {
*self = f();
}
LocalCell::Trying => panic!("attempt to reinitialize Local during initialization"),
LocalCell::Init(_) => {}
}
}

pub(crate) fn get<'cx, 'a, C>(cx: &'a mut C, id: usize) -> Option<&mut LocalCellValue>
where
C: Context<'cx>,
{
let cell = InstanceData::locals(cx).get(id);
match cell {
LocalCell::Init(ref mut b) => Some(b),
_ => None,
}
}

pub(crate) fn get_or_init<'cx, 'a, C, F>(cx: &'a mut C, id: usize, f: F) -> &mut LocalCellValue
where
C: Context<'cx>,
F: FnOnce() -> LocalCellValue,
{
InstanceData::locals(cx)
.get(id)
.pre_init(|| LocalCell::Init(f()));

LocalCell::get(cx, id).unwrap()
}

pub(crate) fn get_or_try_init<'cx, 'a, C, E, F>(
cx: &'a mut C,
id: usize,
f: F,
) -> Result<&mut LocalCellValue, E>
where
C: Context<'cx>,
F: FnOnce(&mut C) -> Result<LocalCellValue, E>,
{
// Kick off a new transaction and drop it before getting the result.
{
let mut tx = TryInitTransaction::new(cx, id);
tx.run(|cx| Ok(f(cx)?))?;
}

// If we're here, the transaction has succeeded, so get the result.
Ok(LocalCell::get(cx, id).unwrap())
}
}

impl Default for LocalCell {
fn default() -> Self {
LocalCell::Uninit
}
}

impl LocalTable {
pub(crate) fn get(&mut self, index: usize) -> &mut LocalCell {
if index >= self.cells.len() {
self.cells.resize_with(index + 1, Default::default);
}
&mut self.cells[index]
}
}

/// An RAII implementation of `LocalCell::get_or_try_init`, which ensures that
/// the state of a cell is properly managed through all possible control paths.
/// As soon as the transaction begins, the cell is labelled as being in a dirty
/// state (`LocalCell::Trying`), so that any additional re-entrant attempts to
/// initialize the cell will fail fast. The `Drop` implementation ensures that
/// after the transaction, the cell goes back to a clean state of either
/// `LocalCell::Uninit` if it fails or `LocalCell::Init` if it succeeds.
struct TryInitTransaction<'cx, 'a, C: Context<'cx>> {
cx: &'a mut C,
id: usize,
_lifetime: PhantomData<&'cx ()>,
dherman marked this conversation as resolved.
Show resolved Hide resolved
}

impl<'cx, 'a, C: Context<'cx>> TryInitTransaction<'cx, 'a, C> {
fn new(cx: &'a mut C, id: usize) -> Self {
let mut tx = Self {
cx,
id,
_lifetime: PhantomData,
};
tx.cell().pre_init(|| LocalCell::Trying);
tx
}

/// _Post-condition:_ If this method returns an `Ok` result, the cell is in the
/// `LocalCell::Init` state.
fn run<E, F>(&mut self, f: F) -> Result<(), E>
where
F: FnOnce(&mut C) -> Result<LocalCellValue, E>,
{
if self.is_trying() {
dherman marked this conversation as resolved.
Show resolved Hide resolved
let value = f(self.cx)?;
*self.cell() = LocalCell::Init(value);
}
Ok(())
}

fn cell(&mut self) -> &mut LocalCell {
InstanceData::locals(self.cx).get(self.id)
}

fn is_trying(&mut self) -> bool {
match self.cell() {
LocalCell::Trying => true,
_ => false,
}
}
}

impl<'cx, 'a, C: Context<'cx>> Drop for TryInitTransaction<'cx, 'a, C> {
dherman marked this conversation as resolved.
Show resolved Hide resolved
fn drop(&mut self) {
if self.is_trying() {
*self.cell() = LocalCell::Uninit;
}
}
}

/// Wrapper for raw Node-API values to be dropped on the main thread
Expand Down Expand Up @@ -83,7 +238,7 @@ impl InstanceData {
/// # Safety
/// No additional locking (e.g., `Mutex`) is necessary because holding a
/// `Context` reference ensures serialized access.
pub(crate) fn get<'a, C: Context<'a>>(cx: &mut C) -> &'a mut InstanceData {
pub(crate) fn get<'cx, C: Context<'cx>>(cx: &mut C) -> &mut InstanceData {
dherman marked this conversation as resolved.
Show resolved Hide resolved
let env = cx.env().to_raw();
let data = unsafe { lifecycle::get_instance_data::<InstanceData>(env).as_mut() };

Expand All @@ -107,26 +262,34 @@ impl InstanceData {
id: InstanceId::next(),
drop_queue: Arc::new(drop_queue),
shared_channel,
locals: LocalTable::default(),
};

unsafe { &mut *lifecycle::set_instance_data(env, data) }
}

/// Helper to return a reference to the `drop_queue` field of `InstanceData`
pub(crate) fn drop_queue<'a, C: Context<'a>>(cx: &mut C) -> Arc<ThreadsafeFunction<DropData>> {
pub(crate) fn drop_queue<'cx, C: Context<'cx>>(
cx: &mut C,
) -> Arc<ThreadsafeFunction<DropData>> {
Arc::clone(&InstanceData::get(cx).drop_queue)
}

/// Clones the shared channel and references it since new channels should start
/// referenced, but the shared channel is unreferenced.
pub(crate) fn channel<'a, C: Context<'a>>(cx: &mut C) -> Channel {
pub(crate) fn channel<'cx, C: Context<'cx>>(cx: &mut C) -> Channel {
let mut channel = InstanceData::get(cx).shared_channel.clone();
channel.reference(cx);
channel
}

/// Unique identifier for this instance of the module
pub(crate) fn id<'a, C: Context<'a>>(cx: &mut C) -> InstanceId {
pub(crate) fn id<'cx, C: Context<'cx>>(cx: &mut C) -> InstanceId {
InstanceData::get(cx).id
}

/// Helper to return a reference to the `locals` field of `InstanceData`.
pub(crate) fn locals<'cx, C: Context<'cx>>(cx: &mut C) -> &mut LocalTable {
&mut InstanceData::get(cx).locals
}
}
2 changes: 1 addition & 1 deletion crates/neon/src/sys/async_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/neon/src/sys/promise.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Loading