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

feat(neon-macros): Export Macro #1025

Merged
merged 11 commits into from
May 10, 2024
Merged

feat(neon-macros): Export Macro #1025

merged 11 commits into from
May 10, 2024

Conversation

kjvalencik
Copy link
Member

@kjvalencik kjvalencik commented Mar 26, 2024

This PR builds on top of #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).

TODO

Addon Initialization

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:

  1. 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.
  2. 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.

#[neon::export]
static NAMES: Json<&[&str]> = Json(&["Alice", "Bob"]);

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.

#[neon::export(json)]
static NAMES: &[&str] = &["Alice", "Bob"];

Functions

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 borrowed FunctionContext 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.

#[neon::export]
fn sort(Json(mut list): Json<Vec<String>>) -> Json<Vec<String>> {
    list.sort();
    Json(list)
}

// More succinctly
#[neon::export(json)]
fn sort(mut list: Vec<String>) -> Vec<String> {
    list.sort();
    list
}

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> {

@kjvalencik kjvalencik marked this pull request as draft March 26, 2024 21:00
@kjvalencik kjvalencik changed the title stash Export Macro Mar 26, 2024
@kjvalencik kjvalencik changed the title Export Macro feat(neon-macros): Export Macro Mar 27, 2024
@kjvalencik kjvalencik requested a review from dherman March 27, 2024 21:50
@kjvalencik kjvalencik mentioned this pull request Apr 1, 2024
2 tasks
Base automatically changed from kv/extract to main April 2, 2024 17:37
@kjvalencik kjvalencik force-pushed the kv/export branch 2 times, most recently from 0030a58 to a9df7be Compare April 2, 2024 18:57
@kjvalencik kjvalencik marked this pull request as ready for review April 2, 2024 22:35
Copy link
Collaborator

@dherman dherman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incredible. No joke, I have never been so excited for the DX of Neon!

My suggestions are mostly for documentation and comments.

crates/neon/src/types_impl/mod.rs Show resolved Hide resolved
crates/neon/src/types_impl/extract/error.rs Show resolved Hide resolved
crates/neon/src/types_impl/extract/error.rs Outdated Show resolved Hide resolved
crates/neon-macros/src/export/global/mod.rs Show resolved Hide resolved
crates/neon-macros/src/export/function/mod.rs Outdated Show resolved Hide resolved
crates/neon-macros/src/export/function/mod.rs Outdated Show resolved Hide resolved
crates/neon-macros/src/export/function/mod.rs Show resolved Hide resolved
crates/neon-macros/src/export/function/mod.rs Outdated Show resolved Hide resolved
crates/neon/src/macros.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@dherman dherman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I cannot wait for us to merge this one.

Just one markdown bug to fix -- I left a suggestion.

/// #### `context`
///
/// The `#[neon::export]` macro looks checks if the first argument has a type of
/// &mut FunctionContext` to determine if the [`Context`](crate::context::Context)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// &mut FunctionContext` to determine if the [`Context`](crate::context::Context)
/// `&mut FunctionContext` to determine if the [`Context`](crate::context::Context)

crates/neon/src/types_impl/extract/error.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@dherman dherman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@kjvalencik kjvalencik merged commit f0e8547 into main May 10, 2024
9 checks passed
@kjvalencik kjvalencik deleted the kv/export branch May 10, 2024 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants