Export magic, without explicit registration, is possible because of the linkme crate. When exporting, we use the crate to add a "create" function to a static slice inside Neon (doc hidden).
Previously, #[neon::main] generated an unmangled unsafe extern "C" fn napi_register_module_v1 inside the users crate. This known symbol is invoked by Node when an addon is loaded. Instead of creating and exporting this function in the macro, it is moved to Neon itself. This has a nice side effect of no longer emitting unsafe code in the user's crate.
Instead #[neon::main] will register their function in a global, doc hidden, slice inside Neon. Even though linkme supports arbitrary lengths, Neon continues to only support a single main. This is for a few reasons:
Backwards compatibility. Supporting multiple requires changing the signature to take a reference to context instead of owned. An owned context could cause undefined behavior since access is used for upholding ownership invariants in Neon.
Simplicity. Users probably only want one main. It's unexpected that multiple could exist. This could change in the future if users have need.
Small security improvement. Dependencies can't execute arbitrary JavaScript at addon load. This benefit is negligible since dependencies could already execute code by injecting life-before-main sections.
The primary benefit of this change is that #[neon::main] is now optional. If a user doesn't need to execute any special code at module load, they can skip it completely. There is a small behavior change where having multiple main becomes a runtime panic instead of a compile time error.
TryIntoJs
The TryIntoJs trait is added as an analog to TryFromJs. It is used to convert a Rust value into a JavaScript value. There are impls on many built-in types as well as the Json type introduced with extractors.
TryIntoJs is not an exact mirror of TryFromJs; there are two key differences:
Associated Output type added. Rust types have the opportunity to express the exact JavaScript type they will use. This is useful to avoid infallible downcasts.
No associated error type. Since we are converting to JavaScript, exceptions are the only reasonable way to fail.
Error
The neon::types::extract::Error type is a new extractor used to make error handling easy when working with #[neon::export] exported functions. At it's core, it is a wrapper around Box<dyn std::error::Error + Send + Sync + 'static>. This is very similar to types found in app error handling crates like anyhow.
There are two special parts of this error:
It carries a kind to determine what type of JS error is represented (e.g., Error, TypeError)
It implements TryIntoJs for converting to a JS error
Along with a blanket impl of TryIntoJs on Result where T: TryIntoJs, E: TryIntoJs, functions can return this error and a JavaScript error will be automatically thrown.
✨ Magic ✨
Error has a blanket impl of From<E> where E: Into<Box<dyn Error + Send + Sync + 'static>>. This means most errors can be automatically converted with ?, including anyhow::Error and &'static str. Users can change return signatures to Result<T, neon::types::extract::Error> and use idiomatic error handling.
Limitation: Error is somewhat un-idiomatic because it doesn't implement std::error::Error. This is because the blanket From<T> impl would conflict with the aforementioned From. Crates like anyhow and eyre have this same limitation.
#[neon::export]
The neon::export macro allows registering values to be exported at addon load. If a main function is not provided, they will be exported automatically. However, if a user registers a #[neon::main] they must manually export. This gives the opportunity to transform if necessary.
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// Must be called or registered values aren't exported
neon::registered().export()?;
// Alternatively, users can iterate and transform
for (name, create) in neon::registered() {
let value = create(&mut cx)?;
cx.export_value(name.to_upper_case(), value)?;
}
}
Globals
In most cases, it's as easy as adding #[neon::export] to the item being exported. This will work on both const and static.
#[neon::export]
const ANSWER: u8 = 42;
By default exported values use the Rust identity as the JavaScript export name. However, users can customize by including the name attribute in the annotation. This is especially useful if they would like the JavaScript field name to be an invalid Rust identifier.
#[neon::export(name = "Answer to the Ultimate Question of Life, the Universe, and Everything")]
const ANSWER: u8 = 42;
Users can use the Json extractor to serde_json::to_string and JSON.parse the response when exporting.
Since this is likely to be a common pattern and it impacts the Rust code, which otherwise doesn't need the wrapper, a convenience attribute can be used instead. This adds the Json wrapper when creating without changing the global value.
Functions register themselves identically to globals and similarly can use name = ".." to change the export name. The ✨ magic ✨ of function exports are provided by FromArgs and TryIntoJs. These allow a very simple wrapper that does two things:
Generates a tuple destructure from cx.args() using the function artiy
Call TryIntoJs::try_into_js on the return type
#[neon::export]
fn original(a: f64, b: f64) -> f64 {
a + b
}
fn transformed(mut cx: FunctionContext) -> JsResult<JsValue> {
let (a, b) = cx.args()?;
original(a, b).try_into_js(&mut cx)
}
The argument list is inspected to see if the first argument is FunctionContext. If it is, this will also be provided to the function. Unlike functions created with JsFunction::new, these get a borrowedFunctionContext in order to avoid soundness issues when calling try_into_js (could allow multiple contexts to be active).
#[neon::export]
fn original(_cx: &mut FunctionContext, a: f64, b: f64) -> f64 {
a + b
}
fn transformed(mut cx: FunctionContext) -> JsResult<JsValue> {
let (a, b) = cx.args()?;
original(&mut cx, a, b).try_into_js(&mut cx)
}
Since users may have renamed the FunctionContext import, we also allow a context attribute to force it to be included.
use neon::context::FunctionContext as Ctx;
#[neon::export(context)]
fn original(_cx: &mut Ctx, a: f64, b: f64) -> f64 {
a + b
}
fn transformed(mut cx: FunctionContext) -> JsResult<JsValue> {
let (a, b) = cx.args()?;
original(&mut cx, a, b).try_into_js(&mut cx)
}
Lastly, since many users will want to leverage serde for complex arguments and return values, wrappers can be automatically injected instead of needing to explicitly include the Json extractor by adding a json attribute.
Using json requires also inspecting the return type to see if it is a Result. If the return type is a Result, res.map(Json) is used instead of Json(res).
Tasks
The Neon Task API is an ergonomic way to schedule work to be performed on the libuv worker pool and return a promise when it completes. When using #[neon::export] on a function a task attribute can be added (#[neon::export(task)]) to make the body execute asynchronously on another thread instead.
#[neon::export(task)]
fn original(a: f64, b: f64) -> f64 {
a + b
}
fn transformed(mut cx: FunctionContext) -> JsResult<JsValue> {
let (a, b) = cx.args()?;
let promise = cx
.task(move || a + b)
.promise(|mut cx, res| res.try_into_js(&mut cx));
Ok(promise.upcast())
}
Unlike previous examples, all arguments must be Send. This means you cannot use this simplified version with exported functions that reference FunctionContext.
error: `FunctionContext` is not allowed with `task` attribute
--> test/napi/src/lib.rs:480:25
|
480 | fn task_parse(_cx: &mut FunctionContext, buf: Vec<u8>) -> Result<Json<Vec<String>>, Error> {
This PR builds on top of https://github.com/neon-bindings/neon/pull/1024 and makes several significant changes to module loading.
Export magic, without explicit registration, is possible because of the
linkme
crate. When exporting, we use the crate to add a "create" function to a static slice inside Neon (doc hidden).TODOFunctionContext
instead of borrowed. Filed follow-up: https://github.com/neon-bindings/neon/issues/1031Addon Initialization
Previously,
#[neon::main]
generated an unmangledunsafe extern "C" fn napi_register_module_v1
inside the users crate. This known symbol is invoked by Node when an addon is loaded. Instead of creating and exporting this function in the macro, it is moved to Neon itself. This has a nice side effect of no longer emittingunsafe
code in the user's crate.Instead
#[neon::main]
will register their function in a global, doc hidden, slice inside Neon. Even thoughlinkme
supports arbitrary lengths, Neon continues to only support a single main. This is for a few reasons:The primary benefit of this change is that
#[neon::main]
is now optional. If a user doesn't need to execute any special code at module load, they can skip it completely. There is a small behavior change where having multiplemain
becomes a runtime panic instead of a compile time error.TryIntoJs
The
TryIntoJs
trait is added as an analog toTryFromJs
. It is used to convert a Rust value into a JavaScript value. There are impls on many built-in types as well as theJson
type introduced with extractors.TryIntoJs
is not an exact mirror ofTryFromJs
; there are two key differences:Output
type added. Rust types have the opportunity to express the exact JavaScript type they will use. This is useful to avoid infallible downcasts.Error
The
neon::types::extract::Error
type is a new extractor used to make error handling easy when working with#[neon::export]
exported functions. At it's core, it is a wrapper aroundBox<dyn std::error::Error + Send + Sync + 'static>
. This is very similar to types found in app error handling crates likeanyhow
.There are two special parts of this error:
kind
to determine what type of JS error is represented (e.g.,Error
,TypeError
)TryIntoJs
for converting to a JS errorAlong with a blanket impl of
TryIntoJs
onResult
whereT: TryIntoJs, E: TryIntoJs
, functions can return this error and a JavaScript error will be automatically thrown.✨ Magic ✨
Error
has a blanket impl ofFrom<E>
whereE: Into<Box<dyn Error + Send + Sync + 'static>>
. This means most errors can be automatically converted with?
, includinganyhow::Error
and&'static str
. Users can change return signatures toResult<T, neon::types::extract::Error>
and use idiomatic error handling.Limitation:
Error
is somewhat un-idiomatic because it doesn't implementstd::error::Error
. This is because the blanketFrom<T>
impl would conflict with the aforementionedFrom
. Crates likeanyhow
andeyre
have this same limitation.#[neon::export]
The
neon::export
macro allows registering values to be exported at addon load. If amain
function is not provided, they will be exported automatically. However, if a user registers a#[neon::main]
they must manually export. This gives the opportunity to transform if necessary.Globals
In most cases, it's as easy as adding
#[neon::export]
to the item being exported. This will work on bothconst
andstatic
.By default exported values use the Rust identity as the JavaScript export name. However, users can customize by including the
name
attribute in the annotation. This is especially useful if they would like the JavaScript field name to be an invalid Rust identifier.Users can use the
Json
extractor toserde_json::to_string
andJSON.parse
the response when exporting.Since this is likely to be a common pattern and it impacts the Rust code, which otherwise doesn't need the wrapper, a convenience attribute can be used instead. This adds the
Json
wrapper when creating without changing the global value.Functions
Functions register themselves identically to globals and similarly can use
name = ".."
to change the export name. The ✨ magic ✨ of function exports are provided byFromArgs
andTryIntoJs
. These allow a very simple wrapper that does two things:cx.args()
using the function artiyTryIntoJs::try_into_js
on the return typeThe argument list is inspected to see if the first argument is
FunctionContext
. If it is, this will also be provided to the function. Unlike functions created withJsFunction::new
, these get a borrowedFunctionContext
in order to avoid soundness issues when callingtry_into_js
(could allow multiple contexts to be active).Since users may have renamed the
FunctionContext
import, we also allow acontext
attribute to force it to be included.Lastly, since many users will want to leverage
serde
for complex arguments and return values, wrappers can be automatically injected instead of needing to explicitly include theJson
extractor by adding ajson
attribute.Using
json
requires also inspecting the return type to see if it is aResult
. If the return type is aResult
,res.map(Json)
is used instead ofJson(res)
.Tasks
The Neon Task API is an ergonomic way to schedule work to be performed on the libuv worker pool and return a promise when it completes. When using
#[neon::export]
on a function atask
attribute can be added (#[neon::export(task)]
) to make the body execute asynchronously on another thread instead.Unlike previous examples, all arguments must be
Send
. This means you cannot use this simplified version with exported functions that referenceFunctionContext
.