diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fceb34b4..422e049d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['10', '12', '14', '15'] + node: ['12', '14', '16'] os: [ubuntu-latest, macos-latest, windows-latest] name: stable - ${{ matrix.os }} - node@${{ matrix.node }} diff --git a/README.md b/README.md index a7a3af3d..6c33b702 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > This project was initialized from [xray](https://github.com/atom/xray) -A minimal library for building compiled `NodeJS` add-ons in `Rust`. +A minimal library for building compiled `Node.js` add-ons in `Rust`.

@@ -22,17 +22,18 @@ A minimal library for building compiled `NodeJS` add-ons in `Rust`. ![Windows i686](https://github.com/napi-rs/napi-rs/workflows/Windows%20i686/badge.svg) [![FreeBSD](https://api.cirrus-ci.com/github/napi-rs/napi-rs.svg)](https://cirrus-ci.com/github/napi-rs/napi-rs?branch=main) -## Operating Systems - -| Linux | macOS | Windows | FreeBSD | -| ----- | ----- | ------- | ------- | -| ✓ | ✓ | ✓ | ✓ | - -## Node.js - -| Node10 | Node12 | Node14 | Node15 | -| ------ | ------ | ------ | ------ | -| ✓ | ✓ | ✓ | ✓ | +| | node12 | node14 | node16 | +| --------------------- | ------ | ------ | ------ | +| Windows x64 | ✓ | ✓ | ✓ | +| Windows x86 | ✓ | ✓ | ✓ | +| macOS x64 | ✓ | ✓ | ✓ | +| macOS aarch64 | ✓ | ✓ | ✓ | +| Linux x64 gnu | ✓ | ✓ | ✓ | +| Linux x64 musl | ✓ | ✓ | ✓ | +| Linux aarch64 gnu | ✓ | ✓ | ✓ | +| Linux arm gnueabihf | ✓ | ✓ | ✓ | +| Linux aarch64 android | ✓ | ✓ | ✓ | +| FreeBSD x64 | ✓ | ✓ | ✓ | This library depends on N-API and requires `Node@10.0.0` or later. @@ -47,7 +48,7 @@ One nice feature is that this crate allows you to build add-ons purely with the ### Define JavaScript functions ```rust -#[js_function(1)] // ------> arguments length, omit for zero +#[js_function(1)] // ------> arguments length fn fibonacci(ctx: CallContext) -> Result { let n = ctx.get::(0)?.try_into()?; ctx.env.create_int64(fibonacci_native(n)) @@ -108,11 +109,11 @@ name = "awesome" crate-type = ["cdylib"] [dependencies] -napi = "1.0" -napi-derive = "1.0" +napi = "1" +napi-derive = "1" [build-dependencies] -napi-build = "1.0" +napi-build = "1" ``` And create `build.rs` in your own project: @@ -209,6 +210,9 @@ yarn test | [napi_create_string_latin1](https://nodejs.org/api/n-api.html#n_api_napi_create_string_latin1) | 1 | v8.0.0 | ✅ | | [napi_create_string_utf16](https://nodejs.org/api/n-api.html#n_api_napi_create_string_utf16) | 1 | v8.0.0 | ✅ | | [napi_create_string_utf8](https://nodejs.org/api/n-api.html#n_api_napi_create_string_utf8) | 1 | v8.0.0 | ✅ | +| [napi_type_tag](https://nodejs.org/api/n-api.html#n_api_napi_type_tag) | 8 | v14.8.0, v12.19.0 | ⚠️ | + +> I have no plan to implement `nape_type_tag` and related API in `napi-rs`, because we have implemented a `rust` replacement in [TaggedObject](https://github.com/napi-rs/napi-rs/blob/main/napi/src/js_values/tagged_object.rs) which is more convenient and more compatible. ### [Functions to convert from N-API to C types](https://nodejs.org/api/n-api.html#n_api_functions_to_convert_from_n_api_to_c_types) @@ -258,3 +262,5 @@ yarn test | [napi_strict_equals](https://nodejs.org/api/n-api.html#n_api_napi_strict_equals) | 1 | v8.0.0 | ✅ | | [napi_detach_arraybuffer](https://nodejs.org/api/n-api.html#n_api_napi_detach_arraybuffer) | 7 | v13.3.0 | ✅ | | [napi_is_detached_arraybuffer](https://nodejs.org/api/n-api.html#n_api_napi_is_detached_arraybuffer) | 7 | v13.3.0 | ✅ | +| [napi_object_freeze](https://nodejs.org/api/n-api.html#n_api_napi_object_freeze) | 8 | v14.14.0, v12.20.0 | ✅ | +| [napi_object_seal](https://nodejs.org/api/n-api.html#n_api_napi_object_seal) | 8 | v14.14.0, v12.20.0 | ✅ | diff --git a/napi/Cargo.toml b/napi/Cargo.toml index 08a7d43f..eace90e7 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -19,6 +19,7 @@ napi4 = ["napi3", "napi-sys/napi4"] napi5 = ["napi4", "napi-sys/napi5"] napi6 = ["napi5", "napi-sys/napi6"] napi7 = ["napi6", "napi-sys/napi7"] +napi8 = ["napi7", "napi-sys/napi8"] serde-json = ["serde", "serde_json"] tokio_rt = ["futures", "tokio", "once_cell", "napi4"] diff --git a/napi/src/async_cleanup_hook.rs b/napi/src/async_cleanup_hook.rs new file mode 100644 index 00000000..481ceab9 --- /dev/null +++ b/napi/src/async_cleanup_hook.rs @@ -0,0 +1,28 @@ +use std::mem; + +use crate::{sys, Status}; + +/// Notice +/// The hook will be removed if `AsyncCleanupHook` was `dropped`. +/// If you want keep the hook until node process exited, call the `AsyncCleanupHook::forget`. +#[repr(transparent)] +pub struct AsyncCleanupHook(pub(crate) sys::napi_async_cleanup_hook_handle); + +impl AsyncCleanupHook { + /// Safe to forget it. + /// Things will be cleanup before process exited. + pub fn forget(self) { + mem::forget(self); + } +} + +impl Drop for AsyncCleanupHook { + fn drop(&mut self) { + let status = unsafe { sys::napi_remove_async_cleanup_hook(self.0) }; + assert!( + status == sys::Status::napi_ok, + "Delete async cleanup hook failed: {}", + Status::from(status) + ); + } +} diff --git a/napi/src/env.rs b/napi/src/env.rs index e3969ba2..31bc3347 100644 --- a/napi/src/env.rs +++ b/napi/src/env.rs @@ -14,8 +14,10 @@ use crate::{ Error, ExtendedErrorInfo, NodeVersion, Result, Status, }; +#[cfg(feature = "napi8")] +use crate::async_cleanup_hook::AsyncCleanupHook; #[cfg(feature = "napi3")] -use super::cleanup_env::{CleanupEnvHook, CleanupEnvHookData}; +use crate::cleanup_env::{CleanupEnvHook, CleanupEnvHookData}; #[cfg(all(feature = "serde-json"))] use crate::js_values::{De, Ser}; #[cfg(all(feature = "tokio_rt", feature = "napi4"))] @@ -1191,6 +1193,58 @@ impl Env { } } + #[cfg(feature = "napi8")] + /// Registers hook, which is a function of type `FnOnce(Arg)`, as a function to be run with the `arg` parameter once the current Node.js environment exits. + /// + /// Unlike [`add_env_cleanup_hook`](https://docs.rs/napi/latest/napi/struct.Env.html#method.add_env_cleanup_hook), the hook is allowed to be asynchronous. + /// + /// Otherwise, behavior generally matches that of [`add_env_cleanup_hook`](https://docs.rs/napi/latest/napi/struct.Env.html#method.add_env_cleanup_hook). + pub fn add_removable_async_cleanup_hook( + &self, + arg: Arg, + cleanup_fn: F, + ) -> Result + where + F: FnOnce(Arg), + Arg: 'static, + { + let mut handle = ptr::null_mut(); + check_status!(unsafe { + sys::napi_add_async_cleanup_hook( + self.0, + Some( + async_finalize:: + as unsafe extern "C" fn(handle: sys::napi_async_cleanup_hook_handle, data: *mut c_void), + ), + Box::leak(Box::new((arg, cleanup_fn))) as *mut (Arg, F) as *mut c_void, + &mut handle, + ) + })?; + Ok(AsyncCleanupHook(handle)) + } + + #[cfg(feature = "napi8")] + /// This API is very similar to [`add_removable_async_cleanup_hook`](https://docs.rs/napi/latest/napi/struct.Env.html#method.add_removable_async_cleanup_hook) + /// + /// Use this one if you don't want remove the cleanup hook anymore. + pub fn add_async_cleanup_hook(&self, arg: Arg, cleanup_fn: F) -> Result<()> + where + F: FnOnce(Arg), + Arg: 'static, + { + check_status!(unsafe { + sys::napi_add_async_cleanup_hook( + self.0, + Some( + async_finalize:: + as unsafe extern "C" fn(handle: sys::napi_async_cleanup_hook_handle, data: *mut c_void), + ), + Box::leak(Box::new((arg, cleanup_fn))) as *mut (Arg, F) as *mut c_void, + ptr::null_mut(), + ) + }) + } + /// # Serialize `Rust Struct` into `JavaScript Value` /// /// ``` @@ -1341,3 +1395,22 @@ unsafe extern "C" fn raw_finalize_with_custom_callback( let (hint, callback) = *Box::from_raw(finalize_hint as *mut (Hint, Finalize)); callback(hint, Env::from_raw(env)); } + +#[cfg(feature = "napi8")] +unsafe extern "C" fn async_finalize( + handle: sys::napi_async_cleanup_hook_handle, + data: *mut c_void, +) where + Arg: 'static, + F: FnOnce(Arg), +{ + let (arg, callback) = *Box::from_raw(data as *mut (Arg, F)); + callback(arg); + if !handle.is_null() { + let status = sys::napi_remove_async_cleanup_hook(handle); + assert!( + status == sys::Status::napi_ok, + "Remove async cleanup hook failed after async cleanup callback" + ); + } +} diff --git a/napi/src/js_values/mod.rs b/napi/src/js_values/mod.rs index 07cf9251..6df707ed 100644 --- a/napi/src/js_values/mod.rs +++ b/napi/src/js_values/mod.rs @@ -239,6 +239,18 @@ macro_rules! impl_js_value_methods { })?; Ok(result) } + + #[cfg(feature = "napi8")] + #[inline] + pub fn freeze(&mut self) -> Result<()> { + check_status!(unsafe { sys::napi_object_freeze(self.0.env, self.0.value) }) + } + + #[cfg(feature = "napi8")] + #[inline] + pub fn seal(&mut self) -> Result<()> { + check_status!(unsafe { sys::napi_object_seal(self.0.env, self.0.value) }) + } } }; } diff --git a/napi/src/lib.rs b/napi/src/lib.rs index 9bc0cc84..654859f5 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -6,7 +6,7 @@ //! //! ## Feature flags //! -//! ### napi1 ~ napi7 +//! ### napi1 ~ napi8 //! //! Because `NodeJS` N-API has versions. So there are feature flags to choose what version of `N-API` you want to build for. //! For example, if you want build a library which can be used by `node@10.17.0`, you should choose the `napi5` or lower. @@ -75,6 +75,10 @@ //! ``` //! +#[cfg(feature = "napi8")] +mod async_cleanup_hook; +#[cfg(feature = "napi8")] +pub use async_cleanup_hook::AsyncCleanupHook; mod async_work; mod call_context; #[cfg(feature = "napi3")] diff --git a/napi/src/status.rs b/napi/src/status.rs index d5560954..32d8c3f5 100644 --- a/napi/src/status.rs +++ b/napi/src/status.rs @@ -29,7 +29,7 @@ pub enum Status { ArrayBufferExpected, DetachableArraybufferExpected, WouldDeadlock, - Unknown = 1024, // unknown status. for example, using napi3 module in napi7 NodeJS, and generate an invalid napi3 status + Unknown = 1024, // unknown status. for example, using napi3 module in napi7 Node.js, and generate an invalid napi3 status } impl Display for Status { diff --git a/sys/Cargo.toml b/sys/Cargo.toml index 95705f92..9d7e22de 100644 --- a/sys/Cargo.toml +++ b/sys/Cargo.toml @@ -18,3 +18,4 @@ napi4 = ["napi3"] napi5 = ["napi4"] napi6 = ["napi5"] napi7 = ["napi6"] +napi8 = ["napi7"] diff --git a/sys/src/lib.rs b/sys/src/lib.rs index c04452a9..27a4e698 100644 --- a/sys/src/lib.rs +++ b/sys/src/lib.rs @@ -181,6 +181,18 @@ pub enum napi_key_conversion { napi_key_numbers_to_strings, } +#[cfg(feature = "napi8")] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct napi_async_cleanup_hook_handle__ { + _unused: [u8; 0], +} +#[cfg(feature = "napi8")] +pub type napi_async_cleanup_hook_handle = *mut napi_async_cleanup_hook_handle__; +#[cfg(feature = "napi8")] +pub type napi_async_cleanup_hook = + Option; + extern "C" { pub fn napi_get_last_error_info( env: napi_env, @@ -801,6 +813,25 @@ extern "C" { result: *mut bool, ) -> napi_status; } + +#[cfg(feature = "napi8")] +extern "C" { + pub fn napi_add_async_cleanup_hook( + env: napi_env, + hook: napi_async_cleanup_hook, + arg: *mut c_void, + remove_handle: *mut napi_async_cleanup_hook_handle, + ) -> napi_status; + + pub fn napi_remove_async_cleanup_hook( + remove_handle: napi_async_cleanup_hook_handle, + ) -> napi_status; + + pub fn napi_object_freeze(env: napi_env, object: napi_value) -> napi_status; + + pub fn napi_object_seal(env: napi_env, object: napi_value) -> napi_status; +} + #[repr(C)] #[derive(Copy, Clone)] pub struct napi_callback_scope__ { diff --git a/test_module/Cargo.toml b/test_module/Cargo.toml index 59889e7c..4bd5ab0e 100644 --- a/test_module/Cargo.toml +++ b/test_module/Cargo.toml @@ -8,7 +8,7 @@ version = "0.1.0" crate-type = ["cdylib"] [features] -latest = ["napi/napi7"] +latest = ["napi/napi8"] napi3 = ["napi/napi3"] [dependencies] diff --git a/test_module/__test__/napi7/arraybuffer.spec.ts b/test_module/__test__/napi7/arraybuffer.spec.ts new file mode 100644 index 00000000..08994f06 --- /dev/null +++ b/test_module/__test__/napi7/arraybuffer.spec.ts @@ -0,0 +1,33 @@ +import ava from 'ava' + +import { napiVersion } from '../napi-version' + +const bindings = require('../../index.node') + +const test = napiVersion >= 7 ? ava : ava.skip + +test('should be able to detach ArrayBuffer', (t) => { + const buf = Buffer.from('hello world') + const ab = buf.buffer.slice(0, buf.length) + try { + bindings.testDetachArrayBuffer(ab) + t.is(ab.byteLength, 0) + } catch (e) { + t.is(e.code, 'DetachableArraybufferExpected') + } +}) + +test('is detached arraybuffer should work fine', (t) => { + const buf = Buffer.from('hello world') + const ab = buf.buffer.slice(0, buf.length) + try { + bindings.testDetachArrayBuffer(ab) + const nonDetachedArrayBuffer = new ArrayBuffer(10) + const detachedArrayBuffer = new ArrayBuffer(0) + t.true(bindings.testIsDetachedArrayBuffer(ab)) + t.false(bindings.testIsDetachedArrayBuffer(nonDetachedArrayBuffer)) + t.true(bindings.testIsDetachedArrayBuffer(detachedArrayBuffer)) + } catch (e) { + t.is(e.code, 'DetachableArraybufferExpected') + } +}) diff --git a/test_module/__test__/napi8/async-cleanup.spec.ts b/test_module/__test__/napi8/async-cleanup.spec.ts new file mode 100644 index 00000000..cd0c80e5 --- /dev/null +++ b/test_module/__test__/napi8/async-cleanup.spec.ts @@ -0,0 +1,28 @@ +import { execSync } from 'child_process' +import { join } from 'path' + +import ava from 'ava' + +import { napiVersion } from '../napi-version' + +const bindings = require('../../index.node') + +const test = napiVersion >= 8 ? ava : ava.skip + +test('should be able to add async cleanup hook', (t) => { + const output = execSync( + `node ${join(__dirname, 'sub-process.js')}`, + ).toString() + t.is(output.trim(), 'Exit from sub process') +}) + +test('should be able to add removable async cleanup hook', (t) => { + const output = execSync( + `node ${join(__dirname, 'sub-process-removable.js')}`, + ).toString() + t.is(output.trim(), 'Exit from sub process') +}) + +test('should be able to remove cleanup hook after added', (t) => { + t.notThrows(() => bindings.testRemoveAsyncCleanupHook()) +}) diff --git a/test_module/__test__/napi8/object.spec.ts b/test_module/__test__/napi8/object.spec.ts new file mode 100644 index 00000000..699d65aa --- /dev/null +++ b/test_module/__test__/napi8/object.spec.ts @@ -0,0 +1,25 @@ +import ava from 'ava' + +import { napiVersion } from '../napi-version' + +const bindings = require('../../index.node') + +const test = napiVersion >= 8 ? ava : ava.skip + +test('should be able to freeze object', (t) => { + const obj: any = {} + bindings.testFreezeObject(obj) + t.true(Object.isFrozen(obj)) + t.throws(() => { + obj.a = 1 + }) +}) + +test('should be able to seal object', (t) => { + const obj: any = {} + bindings.testSealObject(obj) + t.true(Object.isSealed(obj)) + t.throws(() => { + obj.a = 1 + }) +}) diff --git a/test_module/__test__/napi8/sub-process-removable.js b/test_module/__test__/napi8/sub-process-removable.js new file mode 100644 index 00000000..a41fd8bd --- /dev/null +++ b/test_module/__test__/napi8/sub-process-removable.js @@ -0,0 +1,3 @@ +const bindings = require('../../index.node') + +bindings.testAddRemovableAsyncCleanupHook() diff --git a/test_module/__test__/napi8/sub-process.js b/test_module/__test__/napi8/sub-process.js new file mode 100644 index 00000000..bef22ed4 --- /dev/null +++ b/test_module/__test__/napi8/sub-process.js @@ -0,0 +1,3 @@ +const bindings = require('../../index.node') + +bindings.testAddAsyncCleanupHook() diff --git a/test_module/src/lib.rs b/test_module/src/lib.rs index a5791b25..0749e63f 100644 --- a/test_module/src/lib.rs +++ b/test_module/src/lib.rs @@ -13,6 +13,10 @@ mod napi5; #[cfg(feature = "latest")] mod napi6; #[cfg(feature = "latest")] +mod napi7; +#[cfg(feature = "latest")] +mod napi8; +#[cfg(feature = "latest")] mod tokio_rt; mod array; @@ -61,5 +65,9 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> { napi5::register_js(&mut exports)?; #[cfg(feature = "latest")] napi6::register_js(&mut exports)?; + #[cfg(feature = "latest")] + napi7::register_js(&mut exports)?; + #[cfg(feature = "latest")] + napi8::register_js(&mut exports)?; Ok(()) } diff --git a/test_module/src/napi7/buffer.rs b/test_module/src/napi7/buffer.rs new file mode 100644 index 00000000..2283015b --- /dev/null +++ b/test_module/src/napi7/buffer.rs @@ -0,0 +1,14 @@ +use napi::*; + +#[js_function(1)] +pub fn detach_arraybuffer(ctx: CallContext) -> Result { + let input = ctx.get::(0)?; + input.detach()?; + ctx.env.get_undefined() +} + +#[js_function(1)] +pub fn is_detach_arraybuffer(ctx: CallContext) -> Result { + let input = ctx.get::(0)?; + ctx.env.get_boolean(input.is_detached()?) +} diff --git a/test_module/src/napi7/mod.rs b/test_module/src/napi7/mod.rs new file mode 100644 index 00000000..46eb4f0d --- /dev/null +++ b/test_module/src/napi7/mod.rs @@ -0,0 +1,11 @@ +use napi::{JsObject, Result}; + +mod buffer; + +use buffer::*; + +pub fn register_js(exports: &mut JsObject) -> Result<()> { + exports.create_named_method("testDetachArrayBuffer", detach_arraybuffer)?; + exports.create_named_method("testIsDetachedArrayBuffer", is_detach_arraybuffer)?; + Ok(()) +} diff --git a/test_module/src/napi8/async_cleanup.rs b/test_module/src/napi8/async_cleanup.rs new file mode 100644 index 00000000..e2a1ed8e --- /dev/null +++ b/test_module/src/napi8/async_cleanup.rs @@ -0,0 +1,30 @@ +use napi::*; + +#[js_function] +pub fn add_removable_async_cleanup_hook(ctx: CallContext) -> Result { + let cleanup_hook = ctx + .env + .add_removable_async_cleanup_hook(1u32, |_arg: u32| { + println!("Exit from sub process"); + })?; + cleanup_hook.forget(); + ctx.env.get_undefined() +} + +#[js_function] +pub fn add_async_cleanup_hook(ctx: CallContext) -> Result { + ctx.env.add_async_cleanup_hook(1u32, |_arg: u32| { + println!("Exit from sub process"); + })?; + ctx.env.get_undefined() +} + +#[js_function] +pub fn remove_async_cleanup_hook(ctx: CallContext) -> Result { + ctx + .env + .add_removable_async_cleanup_hook(1u32, |_arg: u32| { + println!("Exit from sub process"); + })?; + ctx.env.get_undefined() +} diff --git a/test_module/src/napi8/mod.rs b/test_module/src/napi8/mod.rs new file mode 100644 index 00000000..1a36b4c0 --- /dev/null +++ b/test_module/src/napi8/mod.rs @@ -0,0 +1,19 @@ +use napi::{JsObject, Result}; + +mod async_cleanup; +mod object; + +use async_cleanup::*; +use object::*; + +pub fn register_js(exports: &mut JsObject) -> Result<()> { + exports.create_named_method("testSealObject", seal_object)?; + exports.create_named_method("testFreezeObject", freeze_object)?; + exports.create_named_method( + "testAddRemovableAsyncCleanupHook", + add_removable_async_cleanup_hook, + )?; + exports.create_named_method("testRemoveAsyncCleanupHook", remove_async_cleanup_hook)?; + exports.create_named_method("testAddAsyncCleanupHook", add_async_cleanup_hook)?; + Ok(()) +} diff --git a/test_module/src/napi8/object.rs b/test_module/src/napi8/object.rs new file mode 100644 index 00000000..d0b67bbd --- /dev/null +++ b/test_module/src/napi8/object.rs @@ -0,0 +1,15 @@ +use napi::*; + +#[js_function(1)] +pub fn seal_object(ctx: CallContext) -> Result { + let mut obj: JsObject = ctx.get(0)?; + obj.seal()?; + ctx.env.get_undefined() +} + +#[js_function(1)] +pub fn freeze_object(ctx: CallContext) -> Result { + let mut obj: JsObject = ctx.get(0)?; + obj.freeze()?; + ctx.env.get_undefined() +}