From 30031f09ed64876ff6291be5dd2e837771944bb4 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 12 Nov 2021 17:22:57 +0800 Subject: [PATCH] feat(napi): create ThreadsafeFunction from JsFunction --- README.md | 2 +- crates/backend/src/typegen.rs | 2 +- crates/napi/src/bindgen_runtime/js_values.rs | 3 ++ .../src/bindgen_runtime/js_values/function.rs | 1 + crates/napi/src/env.rs | 2 +- crates/napi/src/js_values/function.rs | 19 ++++++++- crates/napi/src/threadsafe_function.rs | 6 +-- examples/napi-compat-mode/src/class.rs | 2 +- examples/napi-compat-mode/src/env.rs | 2 +- examples/napi/__test__/typegen.spec.ts.md | 2 + examples/napi/__test__/typegen.spec.ts.snap | Bin 952 -> 998 bytes examples/napi/__test__/values.spec.ts | 39 ++++++++++++++++-- examples/napi/index.d.ts | 2 + examples/napi/src/lib.rs | 1 + examples/napi/src/threadsafe_function.rs | 37 +++++++++++++++++ 15 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 crates/napi/src/bindgen_runtime/js_values/function.rs create mode 100644 examples/napi/src/threadsafe_function.rs diff --git a/README.md b/README.md index 2601d706..c6d4a5b6 100644 --- a/README.md +++ b/README.md @@ -245,5 +245,5 @@ yarn test | (NOT YET) | global | 1 | v8.0.0 | | JsSymbol | Symbol | 1 | v8.0.0 | | (NOT YET) | ArrayBuffer/TypedArray | 1 | v8.0.0 | -| (NOT YET) | threadsafe function | 4 | v10.6.0 | napi4 | +| JsFunction | threadsafe function | 4 | v10.6.0 | napi4 | | BigInt | BigInt | 6 | v10.7.0 | napi6 | diff --git a/crates/backend/src/typegen.rs b/crates/backend/src/typegen.rs index 2ea81792..9a8a5232 100644 --- a/crates/backend/src/typegen.rs +++ b/crates/backend/src/typegen.rs @@ -74,7 +74,7 @@ static KNOWN_TYPES: Lazy> = Lazy::new(|| { ("symbol", "symbol"), ("external", "object"), ("AbortSignal", "AbortSignal"), - ("Function", "(...args: any[]) => any"), + ("JsFunction", "(...args: any[]) => any"), ]); map diff --git a/crates/napi/src/bindgen_runtime/js_values.rs b/crates/napi/src/bindgen_runtime/js_values.rs index 61500a53..30efd91e 100644 --- a/crates/napi/src/bindgen_runtime/js_values.rs +++ b/crates/napi/src/bindgen_runtime/js_values.rs @@ -8,6 +8,7 @@ mod bigint; mod boolean; mod buffer; mod either; +mod function; mod map; mod nil; mod number; @@ -22,6 +23,8 @@ pub use array::*; pub use bigint::*; pub use buffer::*; pub use either::*; +#[cfg(feature = "napi4")] +pub use function::*; pub use nil::*; pub use object::*; pub use string::*; diff --git a/crates/napi/src/bindgen_runtime/js_values/function.rs b/crates/napi/src/bindgen_runtime/js_values/function.rs new file mode 100644 index 00000000..17f75ff0 --- /dev/null +++ b/crates/napi/src/bindgen_runtime/js_values/function.rs @@ -0,0 +1 @@ +pub use crate::JsFunction; diff --git a/crates/napi/src/env.rs b/crates/napi/src/env.rs index 999dbe42..aa466f71 100644 --- a/crates/napi/src/env.rs +++ b/crates/napi/src/env.rs @@ -1017,7 +1017,7 @@ impl Env { max_queue_size: usize, callback: R, ) -> Result> { - ThreadsafeFunction::create(self.0, func, max_queue_size, callback) + ThreadsafeFunction::create(self.0, func.0.value, max_queue_size, callback) } #[cfg(all(feature = "tokio_rt", feature = "napi4"))] diff --git a/crates/napi/src/js_values/function.rs b/crates/napi/src/js_values/function.rs index 17db971d..360b8d36 100644 --- a/crates/napi/src/js_values/function.rs +++ b/crates/napi/src/js_values/function.rs @@ -2,6 +2,8 @@ use std::ptr; use super::Value; use crate::bindgen_runtime::TypeName; +#[cfg(feature = "napi4")] +use crate::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}; use crate::{check_status, ValueType}; use crate::{sys, Env, Error, JsObject, JsUnknown, NapiRaw, NapiValue, Result, Status}; @@ -95,8 +97,7 @@ impl JsFunction { /// https://nodejs.org/api/n-api.html#n_api_napi_new_instance /// /// This method is used to instantiate a new `JavaScript` value using a given `JsFunction` that represents the constructor for the object. - #[allow(clippy::new_ret_no_self)] - pub fn new(&self, args: &[V]) -> Result + pub fn new_instance(&self, args: &[V]) -> Result where V: NapiRaw, { @@ -117,4 +118,18 @@ impl JsFunction { })?; Ok(unsafe { JsObject::from_raw_unchecked(self.0.env, js_instance) }) } + + #[cfg(feature = "napi4")] + pub fn create_threadsafe_function( + &self, + max_queue_size: usize, + callback: F, + ) -> Result> + where + T: 'static, + V: NapiRaw, + F: 'static + Send + FnMut(ThreadSafeCallContext) -> Result>, + { + ThreadsafeFunction::create(self.0.env, self.0.value, max_queue_size, callback) + } } diff --git a/crates/napi/src/threadsafe_function.rs b/crates/napi/src/threadsafe_function.rs index f68a08d1..872db0b7 100644 --- a/crates/napi/src/threadsafe_function.rs +++ b/crates/napi/src/threadsafe_function.rs @@ -8,7 +8,7 @@ use std::ptr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; -use crate::{check_status, sys, Env, Error, JsError, JsFunction, NapiRaw, Result, Status}; +use crate::{check_status, sys, Env, Error, JsError, NapiRaw, Result, Status}; use sys::napi_threadsafe_function_call_mode; @@ -186,7 +186,7 @@ impl ThreadsafeFunction { R: 'static + Send + FnMut(ThreadSafeCallContext) -> Result>, >( env: sys::napi_env, - func: &JsFunction, + func: sys::napi_value, max_queue_size: usize, callback: R, ) -> Result { @@ -204,7 +204,7 @@ impl ThreadsafeFunction { check_status!(unsafe { sys::napi_create_threadsafe_function( env, - func.0.value, + func, ptr::null_mut(), async_resource_name, max_queue_size, diff --git a/examples/napi-compat-mode/src/class.rs b/examples/napi-compat-mode/src/class.rs index 51c5befc..7a735a26 100644 --- a/examples/napi-compat-mode/src/class.rs +++ b/examples/napi-compat-mode/src/class.rs @@ -65,7 +65,7 @@ fn new_test_class(ctx: CallContext) -> Result { .env .define_class("TestClass", test_class_constructor, properties.as_slice())?; - test_class.new(&[ctx.env.create_int32(42)?]) + test_class.new_instance(&[ctx.env.create_int32(42)?]) } pub fn register_js(exports: &mut JsObject) -> Result<()> { diff --git a/examples/napi-compat-mode/src/env.rs b/examples/napi-compat-mode/src/env.rs index 5f3afc38..e71b3eb3 100644 --- a/examples/napi-compat-mode/src/env.rs +++ b/examples/napi-compat-mode/src/env.rs @@ -49,7 +49,7 @@ pub fn throw_syntax_error(ctx: CallContext) -> Result { .env .get_global()? .get_named_property::("SyntaxError")?; - ctx.env.throw(syntax_error.new(&[message])?)?; + ctx.env.throw(syntax_error.new_instance(&[message])?)?; ctx.env.get_undefined() } diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index a5582548..3142411b 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -48,6 +48,8 @@ Generated by [AVA](https://avajs.dev). export function concatLatin1(s: string): string␊ export function withoutAbortController(a: number, b: number): Promise␊ export function withAbortController(a: number, b: number, signal: AbortSignal): Promise␊ + export function callThreadsafeFunction(callback: (...args: any[]) => any): void␊ + export function threadsafeFunctionThrowError(cb: (...args: any[]) => any): void␊ export function getBuffer(): Buffer␊ export class Animal {␊ readonly kind: Kind␊ diff --git a/examples/napi/__test__/typegen.spec.ts.snap b/examples/napi/__test__/typegen.spec.ts.snap index 3b1d6568b853ac5ec58e601cf870f81ddf4a146a..3fde6368903c6d9afcf9b3799747e5aac4a93387 100644 GIT binary patch literal 998 zcmVe)E0gaApWW5Nric-hT1t$G?Ai_uH3W{qxs{ z_rGri?)S%b5IhM!^Y+%K!RMd+x{Y^Zr7iRe$*oWl254U?otm%{Lsx4yeQK;0a&Wu~ z*(>rIP!@TDx(PHz?t!#!mayboS`87~Da2=TU%X$8f7eN(An&HGoZmbFGzzH&DnF`$A$G0WIuR%(Nnl zEL$sp!VXbid!H-a^WhAI92a(hX%_a@Yqo`!=box^p8G7ZwCv!ZMU;$0sSKHXl&70T z-De!(DtQg}+W@ysnTHmk!?i+W5pUzPNJDrbWD56irUvi~b|W}tmW;g!&I&#vYabpK z@TY~b$`#JJ8{NZ|#1c;_?cih#HM+Tj2rg8i$vTYSTHKLw6v4R|44uN)5%h%j{f1mn zI+9&jaYmB4QI`k%A;yd1?YC!#T1|X9g>6}qv+>o~S>Y|q;>U>j6k8fM>$t4K66C9J zT|V_iq9o&7gmOJN6UNegy}+pnRq{Fp`XIM!J6~DDcjK#g73Uoo3T>TG9gW9C`I`^5D{4FIcRH5eO5!O7&x=kXF^j$x62XuN88^! z+8q8JvqJ7|f}c<$P=)O#G=x~ImmZ9)}+5d-PU$D^F`CXG> zsbPJ*m`&Fogq@uox&}HpCC<;k_?AyrTP@oL*Ue)C;K~2yqx&ykaQ_N=tHSV%83SD@ za+dija>`0(Qy5VnjeR$zS-L;Ese)VDz~e%+P;(u^8B15peZU={hIPg6RR~4<@t$F) zR-LArT;`k%70bmUwv-_rYV%&GGu!IJ;=xWbHp2ar=giQ%;?K9VNt}iwoW?U?LgP-n z=+#0=$dYeq*`*lX*-EP(xTOG{G0Vb~KDBa~c@r>4s(`e1>SZ1GeMK}E%#lx5wZNKK UuMyd}K|vD#0*y4AjMWMN06#$Q`2YX_ literal 952 zcmV;p14sNpRzVxvYXc`nG`oz2OoeTuUSqqj(N}06 zqPL+YwddxXP%JRyVRoe%7D1P&OoZ-v}f*{xlp8WdlKY#!C^B=!Gd;aO)zyADv zCvd-~yFu_I_}be$Uk2ZN@zpNgO_a7UEM;Pal8~YOpmb_jFM__-eD=~+Irhy=CTzm(D1z!Fm*9DxgDZ;=Ifbpp3aq(tEUIuqC?xX@Z@_ytOt;!sFTL!gcQnwfTFndch? zP}mXb8}D3vJT*334c}^ zt6bw;xYa#eORVrz(hg3>NTYjq7{aA0HCaa?+=x3ejzhQ**~lrp2w@<+?|0;a(vj@J znixsuow_Gfnci)~JX*Kob#JZ}a;FIf#v)C=qqsNT-5?fif`?%`D3go+RQ#}nu ztRzno!Q^IeE{vu7dW|!~RQxdldSGHVPQI##$fRHDfu5g}NbIcOV0ecnUs7!q!2&M?zJcgqp@y&di!Zx8>G zTOs$i!B1%rsM7XhdV5BZX_ezc`{ z5Q@&@Jz1|&{ic4o$~he=UWjFEB||(kM!isPe$YX^0J4nT=W$B=H#y1h26*3IG6npU(*Z diff --git a/examples/napi/__test__/values.spec.ts b/examples/napi/__test__/values.spec.ts index c00f1c8c..f5f40870 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -38,6 +38,8 @@ import { bigintAdd, createBigInt, createBigIntI64, + callThreadsafeFunction, + threadsafeFunctionThrowError, } from '../' test('number', (t) => { @@ -201,9 +203,10 @@ test('async task without abort controller', async (t) => { t.is(await withoutAbortController(1, 2), 3) }) -const MaybeTest = typeof AbortController !== 'undefined' ? test : test.skip +const AbortSignalTest = + typeof AbortController !== 'undefined' ? test : test.skip -MaybeTest('async task with abort controller', async (t) => { +AbortSignalTest('async task with abort controller', async (t) => { const ctrl = new AbortController() const promise = withAbortController(1, 2, ctrl.signal) try { @@ -215,7 +218,7 @@ MaybeTest('async task with abort controller', async (t) => { } }) -MaybeTest('abort resolved task', async (t) => { +AbortSignalTest('abort resolved task', async (t) => { const ctrl = new AbortController() await withAbortController(1, 2, ctrl.signal).then(() => ctrl.abort()) t.pass('should not throw') @@ -234,3 +237,33 @@ BigIntTest('create BigInt', (t) => { BigIntTest('create BigInt i64', (t) => { t.is(createBigIntI64(), BigInt(100)) }) + +const ThreadsafeFunctionTest = + Number(process.versions.napi) >= 4 ? test : test.skip + +ThreadsafeFunctionTest('call thread safe function', (t) => { + let i = 0 + let value = 0 + return new Promise((resolve) => { + callThreadsafeFunction((err, v) => { + t.is(err, null) + i++ + value += v + if (i === 100) { + resolve() + t.is( + value, + Array.from({ length: 100 }, (_, i) => i + 1).reduce((a, b) => a + b), + ) + } + }) + }) +}) + +ThreadsafeFunctionTest('throw error from thread safe function', async (t) => { + const throwPromise = new Promise((_, reject) => { + threadsafeFunctionThrowError(reject) + }) + const err = await t.throwsAsync(throwPromise) + t.is(err.message, 'ThrowFromNative') +}) diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index a1e3c170..5dd08dfa 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -38,6 +38,8 @@ export function concatUtf16(s: string): string export function concatLatin1(s: string): string export function withoutAbortController(a: number, b: number): Promise export function withAbortController(a: number, b: number, signal: AbortSignal): Promise +export function callThreadsafeFunction(callback: (...args: any[]) => any): void +export function threadsafeFunctionThrowError(cb: (...args: any[]) => any): void export function getBuffer(): Buffer export class Animal { readonly kind: Kind diff --git a/examples/napi/src/lib.rs b/examples/napi/src/lib.rs index bece1e32..15f0d80d 100644 --- a/examples/napi/src/lib.rs +++ b/examples/napi/src/lib.rs @@ -18,4 +18,5 @@ mod object; mod serde; mod string; mod task; +mod threadsafe_function; mod typed_array; diff --git a/examples/napi/src/threadsafe_function.rs b/examples/napi/src/threadsafe_function.rs new file mode 100644 index 00000000..2115fd43 --- /dev/null +++ b/examples/napi/src/threadsafe_function.rs @@ -0,0 +1,37 @@ +use std::thread; + +use napi::{ + bindgen_prelude::*, + threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode}, +}; + +#[napi] +pub fn call_threadsafe_function(callback: JsFunction) -> Result<()> { + let tsfn = callback.create_threadsafe_function(0, |ctx: ThreadSafeCallContext| { + ctx.env.create_uint32(ctx.value + 1).map(|v| vec![v]) + })?; + for n in 0..100 { + let tsfn = tsfn.clone(); + thread::spawn(move || { + tsfn.call(Ok(n), ThreadsafeFunctionCallMode::Blocking); + }); + } + Ok(()) +} + +#[napi] +pub fn threadsafe_function_throw_error(cb: JsFunction) -> Result<()> { + let tsfn = cb.create_threadsafe_function(0, |ctx: ThreadSafeCallContext| { + ctx.env.get_boolean(ctx.value).map(|v| vec![v]) + })?; + thread::spawn(move || { + tsfn.call( + Err(Error::new( + Status::GenericFailure, + "ThrowFromNative".to_owned(), + )), + ThreadsafeFunctionCallMode::Blocking, + ); + }); + Ok(()) +}