Expand description

Thread-local storage for JavaScript threads.

At runtime, an instance of a Node.js addon can contain its own local storage, which can then be shared and accessed as needed from Rust in a Neon module. This can be useful for setting up long-lived state that needs to be shared between calls of an addon’s APIs.

For example, an addon may wish to track the thread ID of each of its instances:

static THREAD_ID: LocalKey<u32> = LocalKey::new();

pub fn thread_id<'cx, C: Context<'cx>>(cx: &mut C) -> NeonResult<u32> {
    THREAD_ID.get_or_try_init(cx, |cx| {
        let global = cx.global();
        let require: Handle<JsFunction> = global.get(cx, "require")?;
        let worker: Handle<JsObject> = require.call_with(cx)
            .arg(cx.string("node:worker_threads"))
            .apply(cx)?;
        let thread_id: Handle<JsNumber> = worker.get(cx, "threadId")?;
        Ok(thread_id.value(cx) as u32)
    }).cloned()
}

The Addon Lifecycle

For some use cases, a single shared global constant stored in a static variable might be sufficient:

static MY_CONSTANT: &'static str = "hello Neon";

This variable will be allocated when the addon is first loaded into the Node.js process. This works fine for single-threaded applications, or global thread-safe data.

However, since the addition of worker threads in Node v10, modules can be instantiated multiple times in a single Node process. So even though the dynamically-loaded binary library (i.e., the Rust implementation of the addon) is only loaded once in the running process, its #[main] function can be executed multiple times with distinct module objects, one per application thread:

The Node.js addon lifecycle, described in detail below.

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, for allocating and initializing thread-local data. (Technically, this data is stored in the addon’s module instance, 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 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:

static MY_CONSTRUCTOR: LocalKey<Root<JsFunction>> = LocalKey::new();

pub fn my_constructor<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> {
    let constructor = MY_CONSTRUCTOR.get_or_try_init(cx, |cx| {
        let constructor: Handle<JsFunction> = initialize_my_datatype(cx)?;
        Ok(constructor.root(cx))
    })?;
    Ok(constructor.to_inner(cx))
}

Notice that if this code were implemented without a LocalKey, it would panic whenever one thread stores an instance of the constructor and a different thread attempts to access it with the call to to_inner().

When to Use Thread-Local Storage

Single-threaded applications don’t generally need to worry about thread-local data. There are two cases where Neon apps should consider storing static data in a LocalKey storage cell:

  • Multi-threaded applications: If your Node application uses the Worker API, you’ll want to store any static data that might get access from multiple threads in thread-local data.
  • Libraries: If your addon is part of a library that could be used by multiple applications, you’ll want to store static data in thread-local data in case the addon ends up instantiated by multiple threads in some future application.

Why Not Use Standard TLS?

Since the JavaScript engine may not tie JavaScript threads 1:1 to system threads, it is recommended to use this module instead of the Rust standard thread-local storage when associating data with a JavaScript thread.

Structs

A JavaScript thread-local container that owns its contents, similar to std::thread::LocalKey but tied to a JavaScript thread rather than a system thread.