23 releases

0.2.4 Jan 2, 2024
0.2.3 Mar 8, 2022
0.1.6 Feb 23, 2022
0.1.3 Jan 29, 2022

#243 in Development tools

Download history 30/week @ 2024-09-21

104 downloads per month
Used in nodex-plugin-helloworld

MIT license

12MB
4.5K SLoC

Nodex - Nodejs eXtension 🥳

Yet another crate to create native nodejs addons :)

This crate aims to make creating native nodejs addons very easy and comfortable.

click here: uuhan/nodex@dev to see the most recent developments.

Platform Support

  • linux
  • macos
  • windows (>=0.2.1)

Changelog

releases

Usage

[lib]
crate-type = ["cdylib"]

[dependencies.nodex-api]
version = "0.2.3"
features = ["v8"]

The default napi version is set to v1, you can use other version with your need.

We have v1,v2,v3,...v8 versions.

Currently, nodex just reexports nodex-api:

[lib]
crate-type = ["cdylib"]

[dependencies.nodex]
version = "0.2.3"
features = ["v8"]

Napi Level

v1

  • NapiValueT::wrap::<T, Finalizer>() - Wraps a native instance, call finalizer when value is garbage-collected.
  • NapiValueT::remove_wrap::<T>() - Remove the wrapped native instance. The finalizer will not be called if the wrapped instance is removed.
  • NapiValueT::unwrap::<T>() - Access the wrapped instance.
  • NapiValueT::gc::<Finalizer>() - Hook fired when value is gabage-collected.

v3

  • NapiEnv::add_cleanup_hook() - Do the cleanup when nodejs environment exits.

v4

  • NapiThreadsafeFunction::<Data, const N: usize> - Thread safe function.

v5

  • NapiValueT::finalizer() - Adds a napi_finalize callback which will be called when the JavaScript object is ready for gc.

v6

  • NapiEnv::set_instance_data::<Data, Finalizer> - Set data to current agent.
  • NapiENv::get_instance_data::<Data> - Get Option<&mut Data> from current agent.

v8

  • NapiEnv::add_async_cleanup_hook() - Do the cleanup when nodejs environment exits, asynchronous.

Examples

Init Module

simply define your module by:

use nodex::prelude::*;
nodex::napi_module!(init);
fn init(env: NapiEnv, exports: JsObject) -> NapiResult<()> {
    Ok(())
}

Version Guard

make sure the node api version is large or equal than your compiled addon's.

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    nodex::napi_guard!(env.napi_version()?);
    Ok(())
}

Nodejs Version & Napi Version

get the runtime version:

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    let node_version = env.node_version()?;
    let napi_version = env.napi_version()?;
    Ok(())
}

Define Js Variable

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    // String & Symbol
    let label: JsSymbol = env.symbol()?;
    let name: JsString = env.string("")?;

    // Object
    let mut obj: JsObject = env.object()?;
    obj.set_property(name, env.null()?)?;

    // Function
    let func: JsFunction = env.func(move |this, (a1, a2, a3): (JsValue, JsValue, JsValue)| {
        let env = this.env();
        a1.as_function()?.call(this, ())?;
        a1.as_function()?.call(this, env.string("I am from rust world.")?)
    })?;

    let func: JsFunction = env.func(move |this, a1: JsFunction| {
        let env = this.env();
        a1.call(this, env.string("I am from rust world.")?)
    })?;

    let class: JsClass = env.class("myclass", |mut this, a1: JsNumber| {
        this.set_named_property("a1", a1)?;
        Ok(this)
    }, &[])?;

    // Error
    let error: JsError = JsError::error(env, "error", Some("code"))?;

    Ok(())
}

Napi handle scope

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    // napi handle scope
    let _scope: NapiHandleScope = env.handle_scope()?;
    let _escapable_scope: NapiEscapableHandleScope = env.escapable_handle_scope()?;
    Ok(())
}

Napi cleanup hook

sync

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    env.add_cleanup_hook(|| {
        println!("clean hook fired");
        Ok(())
    })?;

    let hook_to_remove = env.add_cleanup_hook(|| {
        println!("clean hook fired");
        Ok(())
    })?;

    hook_to_remove.remove()?;
    Ok(())
}

aync

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    match env.add_async_cleanup_hook(|hook| {
        // DO SOME CLEANUP
        // NB: should call remove after done
        hook.remove()
    })? {
        Some(hook) => {
            // NB: also the hook can be removed before it is fired.
            hook.remove()?;
        }
        None => {}
    }

    Ok(())
}

Set Property Descriptor

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    let mut obj: JsObject = env.object()?;
    obj.define_properties(&[DescriptorValueBuilder::new()
        .with_utf8name("myvalue")
        .with_value(env.string("myvalue")?)
        .build()?])?;

    obj.define_properties(&[DescriptorMethodBuilder::new()
        .with_utf8name("mymethod")
        .with_method(move |this, ()| this.env().double(200.))
        .build()?])?;

    obj.define_properties(&[DescriptorAccessorBuilder::new()
        .with_utf8name("myaccessor")
        .with_getter(|this| this.env().double(100.))
        .with_setter(|_this: JsObject, n: JsNumber| {
            println!("setter: {}", n.get_value_int32()?);
            Ok(())
        })
        .build()?])?;

    Ok(())
}

Create An Async Work

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    // without shared state
    env.async_work(
        "my-test-async-task",
        (),
        move |_| {
            // you can do the hard work in the thread-pool context.
            // NB: js work is not allowed here.
            println!("execute async task");
        },
        move |_, status, _| {
            // you can do some js work in this context
            println!("[{}] complete async task", status);
            Ok(())
        },
    )?
    .queue()?;

    Ok(())
}

gabage-collected hook

for napi less than 5, implement by napi_wrap, otherwise by napi_add_finalizer.

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    let mut obj = env.object()?;
    obj.gc(move |_| {
        println!("obj garbage-collected");
        Ok(())
    });

    Ok(())
}

Wrap native instance

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    let mut obj = env.object()?;
    obj.wrap([1usize; 2], move |_, wrapped| {
        Ok(())
    })?;
    obj.unwrap::<[usize; 2]>()?; // access the wrapped instance
    obj.remove_wrap::<[usize; 2]>()?; // the finalizer will not be called
    Ok(())
}

Thread safe function

require: napi >= 4

use nodex::prelude::*;
fn env(env: NapiEnv) -> NapiResult<()> {
    let tsfn = NapiThreadsafeFunction::<_, 0>::new(
        env,
        "tsfn-task",
        env.func(|this, a1: JsString| {
            println!("callback result: {}", a1.get()?);
            this.env().undefined()
        })?,
        // finalizer
        move |_| Ok(()),
        // js-callback
        move |f, data: String| {
            f.call(env.object()?, env.string(&data)?)?;
            Ok(())
        },
    )?;

    std::thread::spawn(move || {
        tsfn.non_blocking("hello, world - 1".into()).unwrap();
        tsfn.non_blocking("hello, world - 2".into()).unwrap();
        tsfn.release().unwrap();
    });
    Ok(())
}

Promise for some heavy work

use nodex::prelude::*;
fn test(env: NapiEnv) -> NapiResult<()> {
let promise: JsPromise<JsString, JsError> = env.promise(
    move |result| {
        for i in 1..=3 {
            std::thread::sleep(std::time::Duration::from_secs(1));
            println!("[{}] Doing...", i);
        }

        *result = true;
    },
    move |promise, _, result| {
        let env = promise.env();
        if result {
            promise.resolve(env.string("the promise is resolved.")?)?;
        } else {
            promise.reject(env.error("the promise is rejected.")?)?;
        }
        Ok(())
    },
)?;
Ok(())
}
// the `promise.value()` can return to js world as a Promise

Run script

use nodex::prelude::*;
fn script(env: NapiEnv) -> NapiResult<()> {
    let func: Function<JsUndefined> = env.run_script(
        r#"
            function hello() {
                console.log(this);
            }

            hello
        "#,
    )?;

    func.call(env.global()?.object(), ())?;
    Ok(())
}

More

examples/demo

Run:

bash demo.sh

How to participate in

Code of conduct

cat >> .git/hooks/pre-push << EOF
#!/bin/sh

cargo fmt || exit
cargo clippy -- -D warnings || exit
EOF

chmod +x .git/hooks/pre-push

TODO

  • ergonomical api design.
  • export the codebase from crates world, make it easy to call rust function from js world.
  • import the huge codebase from npm world, make it easy to call js function from rust side.
    • sweet syntax, like: let lodash = nodex::import!(lodash);
  • nodejs async runtime to drive rust async code
    • async runtime for async rust
    • macros like: #nodex::rt async fn main(), so you can use nodejs to run any rust async-code.
      • node --require=main.node
      • rust code introspection with nodejs repl
  • cargo-nodex cargo subcommand to make ease of create nodejs addons, e.g. auto generate ts typings.
    • cargo nodex build
    • cargo nodex typings
    • cargo nodex package

License

Licensed under either of

at your option.

Dependencies