Since v10, Node.js supports an improved API for building native modules, known as N-API. N-API offers a clearer, more complete, and more stable API layer for writing Node.js plugins than previous Node versions did.
The Neon community has been hard at work porting the library to a new backend based on N-API.
Some key benefits of the new backend include:
- Compiled Neon modules will work in all versions of Node without needing to be recompiled, guaranteed!
- You can precompile Neon-based libraries to be completely transparent to downstream consumers.
- The build process is streamlined, making Neon apps more reliable and easier to debug.
- The stability guarantees of N-API allow us to avoid risk of incompatible changes to future releases of Neon.
Porting Neon to N-API has been mostly transparent, but it has required a few backwards-incompatible changes. This guide provides instructions on how to migrate existing apps to the new N-API backend.
Fortunately, the guaranteed stability of N-API means that once Neon users do this migration, we have increased confidence in the stability of Neon. We expect this to be the last major breaking change before reaching Neon 1.0.
If you have any trouble porting, please reach out to us with a Neon issue or on the community Slack! We want to help everyone upgrade as smoothly and seamlessly as possible.
The N-API backend of Neon requires a minimum Node version of 10.0.
To enable the N-API backend, you need to:
- Remove
build.rs
from the project directory andbuild = "build.rs"
from theCargo.toml
. The N-API backend does not require a Cargo build script. - Disable the default features (for now, the default features select the legacy backend) by setting
default-features = false
; and - Enable the appropriate feature flag in your
Cargo.toml
to select the N-API version you need support for (each N-API version N uses the feature flag"napi-N"
, for example"napi-4"
for N-API version 4).
As a rule, you should choose the oldest version of N-API that has the APIs you need. (We will be adding N-API version requirements to the Neon API docs to make this clearer in the future.) You can consult the official N-API feature matrix to see which N-API versions come with various versions of Node.
[dependencies.neon]
version = "0.8.2"
default-features = false
features = ["napi-4"]
Many methods that previously did not require context (e.g., JsString::size
) now require a context. In many cases, this means adding an additional argument or using a convenience method on the Context
trait.
Handle
is_a
downcast
Handle::downcast
also requires a second type argument for the context type. This can usually be inferred, so you can typically use _
.
Before:
value.downcast::<JsNumber>()
After:
value.downcast::<JsNumber, _>(&mut cx)
JsBoolean
value
JsNull
new
JsString
size
value
JsNumber
value
JsUndefined
new
PropertyKey
get_from
set_from
Handles no longer implement Eq
or PartialEq
, which had underspecified behavior. Use Value::strict_equals
instead to invoke the behavior of JavaScript's ===
operator.
The N-API backend introduces two categories of significant change:
- Embedding Rust data, which is no longer done through the awkward and complex
declare_types!
(i.e. classes) macro, but through a simpler primitive: theJsBox
API. - Concurrency, which is offered through the Event Queue API instead of the Task API or Event Handlers, both of which are deprecated and removed in the N-API backend.
The declare_types!
macro is deprecated and replaced by the JsBox
type.
Rationale: The declare_types!
macro provides a syntax for defining classes, but requires substantial boilerplate and is unergonomic for simple cases and tends to interact poorly with IDEs. It's also not flexible enough to express the full range of JavaScript classes syntax and semantics. With the JsBox
type, it's easy to embed Rust data in JavaScript objects, which can then be nested inside of more feature-rich classes defined in pure JavaScript (or TypeScript).
Before:
struct User {
first_name: String,
last_name: String,
}
impl User {
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
declare_types! {
class JsUser for User {
init(mut cx) {
let first_name = cx.argument::<JsString>(0)?;
let last_name = cx.argument::<JsString>(1)?;
Ok(User { first_name, last_name })
}
method full_name(mut cx) {
let this = cx.this();
let guard = cx.lock();
let user = this.borrow(&guard);
let full_name = user.full_name();
Ok(cx.string(full_name).upcast())
}
}
}
After:
On the Rust side, the wrapped type must implement the Finalize
trait, but this comes with a default implementation so it can be implemented with an empty impl
block:
struct User {
first_name: String,
last_name: String,
}
impl Finalize for User { }
The type can then be exposed to JavaScript with simple functions that wrap User
in a JsBox
:
fn create_user(mut cx: FunctionContext) -> JsResult<JsBox<User>> {
let first_name = cx.argument::<JsString>(0)?.value(&mut cx);
let last_name = cx.argument::<JsString>(1)?.value(&mut cx);
Ok(cx.boxed(User { first_name, last_name }))
}
fn user_full_name(mut cx: FunctionContext) -> JsResult<JsString> {
let user = cx.argument::<JsBox<User>>(0)?;
let full_name = user.full_name();
Ok(cx.string(full_name))
}
Finally, you can provide an idiomatic JavaScript interface to the type by wrapping the boxed type in a class:
class User {
constructor(firstName, lastName) {
this.boxed = addon.createUser(firstName, lastName);
}
fullName() {
return addon.userFullName(this.boxed);
}
}
The supported mechanism for concurrency is the Event Queue API (neon::event::EventQueue
). This feature has not yet stabilized, so to use this API, you'll also need to enable the "event-queue-api"
feature flag as well:
[dependencies.neon]
version = "0.8.1"
default-features = false
features = ["napi-4", "event-queue-api"]
The Task API (neon::task
) is deprecated, and should in most cases be translated to using the Event Queue API.
Rationale: The Task API was built on top of the low-level libuv thread pool, which manages the concurrency of the Node.js system internals and should rarely be exposed to user-level programs. For most use cases, Neon users took advantage of this API as the only way to implement background, asynchronous computations. The Event Queue API is a more general-purpose, convenient, and safe way of achieving that purpose.
That said, if you believe you need access to the libuv thread pool, please file an issue in the Neon repository with a description of your use case to let us know about it. We don't believe this is commonly needed, but we don't want to leave you stuck!
Before:
With the Task
API it was possible to define background computations off the main JavaScript thread, but these could only be run within the libuv thread pool--which runs all the system logic for the internals of Node.js. This gave Neon programmers a real power but forced them to contend with Node.js system tasks.
impl Task for MyTask {
type Output = i32;
type Error = String;
type JsEvent = JsNumber;
fn perform(&self) -> Result<Self::Output, Self::Error> {
// compute the result...
}
fn complete(self, mut cx: TaskContext, result: Result<Self::Output, Self::Error>) -> JsResult<Self::JsEvent> {
match result {
Ok(n) => {
Ok(cx.number(n))
}
Err(s) => {
cx.throw_error(s)
}
}
}
}
pub fn start_task(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let callback = cx.argument::<JsFunction>(0)?;
MyTask.schedule(callback);
Ok(cx.undefined())
}
After:
With the N-API backend, Neon programmers can use their own native threads and avoid competing with the Node.js system internals. This also brings some convenience since it doesn't require defining any custom trait implementations.
pub fn start_task(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let callback = cx.argument::<JsFunction>(0)?.root(&mut cx);
let queue = cx.queue();
std::thread::spawn(move || {
let result = // compute the result...
queue.send(move |mut cx| {
let callback = callback.into_inner(&mut cx);
let this = cx.undefined();
let args = match result {
Ok(n) => vec![
cx.null().upcast::<JsValue>(),
cx.number(result).upcast()
],
Err(msg) => vec![
cx.error(msg).upcast()
]
};
callback.call(&mut cx, this, args)?;
Ok(())
});
});
Ok(cx.undefined())
}
The Event Handler API (neon::event::EventHandler
) is deprecated and should be replaced by the Event Queue API.
Rationale: The Event Handler API had multiple issues with safety, memory leaks, and ergonomics (1, 2).
Before:
pub fn start_task(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let callback = cx.argument::<JsFunction>(0)?;
let handler = EventHandler::new(callback);
thread::spawn(move || {
let result = // compute the result...
handler.schedule(move |cx| {
vec![cx.number(result).upcast()]
});
});
Ok(cx.undefined())
}
After:
pub fn start_task(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let callback = cx.argument::<JsFunction>(0)?.root(&mut cx);
let queue = cx.queue();
std::thread::spawn(move || {
let result = // compute the result...
queue.send(move |mut cx| {
let callback = callback.into_inner(&mut cx);
let this = cx.undefined();
let args = vec![
cx.null().upcast::<JsValue>(),
cx.number(result).upcast()
];
callback.call(&mut cx, this, args)?;
Ok(())
});
});
Ok(cx.undefined())
}