feat(napi): add napi8 features

This commit is contained in:
LongYinan 2021-04-21 17:19:45 +08:00
parent e9bb6d19ca
commit daef1956b2
No known key found for this signature in database
GPG key ID: C3666B7FC82ADAD7
22 changed files with 366 additions and 21 deletions

View file

@ -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 }}

View file

@ -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`.
<p>
<a href="https://docs.rs/crate/napi"><img src="https://docs.rs/napi/badge.svg"></img></a>
@ -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<JsNumber> {
let n = ctx.get::<JsNumber>(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 | ✅ |

View file

@ -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"]

View file

@ -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)
);
}
}

View file

@ -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<Arg, F>(
&self,
arg: Arg,
cleanup_fn: F,
) -> Result<AsyncCleanupHook>
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::<Arg, F>
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<Arg, F>(&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::<Arg, F>
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<Hint, Finalize>(
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<Arg, F>(
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"
);
}
}

View file

@ -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) })
}
}
};
}

View file

@ -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")]

View file

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

View file

@ -18,3 +18,4 @@ napi4 = ["napi3"]
napi5 = ["napi4"]
napi6 = ["napi5"]
napi7 = ["napi6"]
napi8 = ["napi7"]

View file

@ -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<unsafe extern "C" fn(handle: napi_async_cleanup_hook_handle, data: *mut c_void)>;
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__ {

View file

@ -8,7 +8,7 @@ version = "0.1.0"
crate-type = ["cdylib"]
[features]
latest = ["napi/napi7"]
latest = ["napi/napi8"]
napi3 = ["napi/napi3"]
[dependencies]

View file

@ -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')
}
})

View file

@ -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())
})

View file

@ -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
})
})

View file

@ -0,0 +1,3 @@
const bindings = require('../../index.node')
bindings.testAddRemovableAsyncCleanupHook()

View file

@ -0,0 +1,3 @@
const bindings = require('../../index.node')
bindings.testAddAsyncCleanupHook()

View file

@ -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(())
}

View file

@ -0,0 +1,14 @@
use napi::*;
#[js_function(1)]
pub fn detach_arraybuffer(ctx: CallContext) -> Result<JsUndefined> {
let input = ctx.get::<JsArrayBuffer>(0)?;
input.detach()?;
ctx.env.get_undefined()
}
#[js_function(1)]
pub fn is_detach_arraybuffer(ctx: CallContext) -> Result<JsBoolean> {
let input = ctx.get::<JsArrayBuffer>(0)?;
ctx.env.get_boolean(input.is_detached()?)
}

View file

@ -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(())
}

View file

@ -0,0 +1,30 @@
use napi::*;
#[js_function]
pub fn add_removable_async_cleanup_hook(ctx: CallContext) -> Result<JsUndefined> {
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<JsUndefined> {
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<JsUndefined> {
ctx
.env
.add_removable_async_cleanup_hook(1u32, |_arg: u32| {
println!("Exit from sub process");
})?;
ctx.env.get_undefined()
}

View file

@ -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(())
}

View file

@ -0,0 +1,15 @@
use napi::*;
#[js_function(1)]
pub fn seal_object(ctx: CallContext) -> Result<JsUndefined> {
let mut obj: JsObject = ctx.get(0)?;
obj.seal()?;
ctx.env.get_undefined()
}
#[js_function(1)]
pub fn freeze_object(ctx: CallContext) -> Result<JsUndefined> {
let mut obj: JsObject = ctx.get(0)?;
obj.freeze()?;
ctx.env.get_undefined()
}