diff --git a/README.md b/README.md index 0398a0b458..e0f6671bf3 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ yarn test | Array | Array | 1 | v8.0.0 | | Vec | Array | 1 | v8.0.0 | | Buffer | Buffer | 1 | v8.0.0 | +| External | External | 1 | v8.0.0 | | | Null | null | 1 | v8.0.0 | | Undefined/() | undefined | 1 | v8.0.0 | | Result<()> | Error | 1 | v8.0.0 | diff --git a/cli/src/build.ts b/cli/src/build.ts index 8518c51310..d590ede1c0 100644 --- a/cli/src/build.ts +++ b/cli/src/build.ts @@ -344,7 +344,10 @@ async function processIntermediateTypeFile( .split('\n') .map((line) => line.trim()) .filter(Boolean) - let dts = '' + let dts = `export class ExternalObject { + private readonly __type: unique symbol; + [val: unique symbol]: T +}\n` const classes = new Map() const impls = new Map() diff --git a/crates/backend/src/typegen.rs b/crates/backend/src/typegen.rs index 12ff33da18..38bb14d5be 100644 --- a/crates/backend/src/typegen.rs +++ b/crates/backend/src/typegen.rs @@ -83,6 +83,7 @@ static KNOWN_TYPES: Lazy> = Lazy::new(|| { ("AbortSignal", "AbortSignal"), ("JsFunction", "(...args: any[]) => any"), ("JsGlobal", "typeof global"), + ("External", "ExternalObject<{}>"), ]); map diff --git a/crates/napi/src/bindgen_runtime/js_values.rs b/crates/napi/src/bindgen_runtime/js_values.rs index e8919656e7..8c6c0612ae 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 external; mod function; mod map; mod nil; @@ -26,6 +27,7 @@ pub use array::*; pub use bigint::*; pub use buffer::*; pub use either::*; +pub use external::*; #[cfg(feature = "napi4")] pub use function::*; pub use nil::*; diff --git a/crates/napi/src/bindgen_runtime/js_values/external.rs b/crates/napi/src/bindgen_runtime/js_values/external.rs new file mode 100644 index 0000000000..66f790ca56 --- /dev/null +++ b/crates/napi/src/bindgen_runtime/js_values/external.rs @@ -0,0 +1,110 @@ +use std::any::TypeId; + +use crate::{check_status, Error, Status, TaggedObject}; + +use super::{FromNapiValue, ToNapiValue}; + +pub struct External { + obj: *mut TaggedObject, + size_hint: usize, + pub adjusted_size: i64, +} + +impl External { + pub fn new(value: T) -> Self { + Self { + obj: Box::into_raw(Box::new(TaggedObject::new(value))), + size_hint: 0, + adjusted_size: 0, + } + } + + /// `size_hint` is a value to tell Node.js GC how much memory is used by this `External` object. + /// + /// If getting the exact `size_hint` is difficult, you can provide an approximate value, it's only effect to the GC. + /// + /// If your `External` object is not effect to GC, you can use `External::new` instead. + pub fn new_with_size_hint(value: T, size_hint: usize) -> Self { + Self { + obj: Box::into_raw(Box::new(TaggedObject::new(value))), + size_hint, + adjusted_size: 0, + } + } +} + +impl FromNapiValue for External { + unsafe fn from_napi_value( + env: napi_sys::napi_env, + napi_val: napi_sys::napi_value, + ) -> crate::Result { + let mut unknown_tagged_object = std::ptr::null_mut(); + check_status!( + napi_sys::napi_get_value_external(env, napi_val, &mut unknown_tagged_object), + "Failed to get external value" + )?; + + let type_id = unknown_tagged_object as *const TypeId; + if *type_id == TypeId::of::() { + let tagged_object = unknown_tagged_object as *mut TaggedObject; + Ok(Self { + obj: tagged_object, + size_hint: 0, + adjusted_size: 0, + }) + } else { + Err(Error::new( + Status::InvalidArg, + "T on `get_value_external` is not the type of wrapped object".to_owned(), + )) + } + } +} + +impl AsRef for External { + fn as_ref(&self) -> &T { + unsafe { Box::leak(Box::from_raw(self.obj)).object.as_ref().unwrap() } + } +} + +impl AsMut for External { + fn as_mut(&mut self) -> &mut T { + unsafe { Box::leak(Box::from_raw(self.obj)).object.as_mut().unwrap() } + } +} + +impl ToNapiValue for External { + unsafe fn to_napi_value( + env: napi_sys::napi_env, + mut val: Self, + ) -> crate::Result { + let mut napi_value = std::ptr::null_mut(); + check_status!( + napi_sys::napi_create_external( + env, + val.obj as *mut _, + Some(crate::raw_finalize::), + Box::into_raw(Box::new(Some(val.size_hint as i64))) as *mut _, + &mut napi_value + ), + "Create external value failed" + )?; + + let mut adjusted_external_memory_size = std::mem::MaybeUninit::new(0); + + if val.size_hint != 0 { + check_status!( + napi_sys::napi_adjust_external_memory( + env, + val.size_hint as i64, + adjusted_external_memory_size.as_mut_ptr() + ), + "Adjust external memory failed" + )?; + }; + + val.adjusted_size = adjusted_external_memory_size.assume_init(); + + Ok(napi_value) + } +} diff --git a/crates/napi/src/env.rs b/crates/napi/src/env.rs index e8f2b6866c..8cbc723de6 100644 --- a/crates/napi/src/env.rs +++ b/crates/napi/src/env.rs @@ -879,10 +879,12 @@ impl Env { ) })?; if let Some(changed) = size_hint { - let mut adjusted_value = 0i64; - check_status!(unsafe { - sys::napi_adjust_external_memory(self.0, changed, &mut adjusted_value) - })?; + if changed != 0 { + let mut adjusted_value = 0i64; + check_status!(unsafe { + sys::napi_adjust_external_memory(self.0, changed, &mut adjusted_value) + })?; + } }; Ok(unsafe { JsExternal::from_raw_unchecked(self.0, object_value) }) } @@ -1260,12 +1262,14 @@ pub(crate) unsafe extern "C" fn raw_finalize( if !finalize_hint.is_null() { let size_hint = *Box::from_raw(finalize_hint as *mut Option); if let Some(changed) = size_hint { - let mut adjusted = 0i64; - let status = sys::napi_adjust_external_memory(env, -changed, &mut adjusted); - debug_assert!( - status == sys::Status::napi_ok, - "Calling napi_adjust_external_memory failed" - ); + if changed != 0 { + let mut adjusted = 0i64; + let status = sys::napi_adjust_external_memory(env, -changed, &mut adjusted); + debug_assert!( + status == sys::Status::napi_ok, + "Calling napi_adjust_external_memory failed" + ); + } }; } } diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index d46afc27ed..7dc78975c8 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -8,7 +8,11 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - `export const DEFAULT_COST: number␊ + `export class ExternalObject {␊ + private readonly __type: unique symbol;␊ + [val: unique symbol]: T␊ + }␊ + export const DEFAULT_COST: number␊ export function getWords(): Array␊ export function getNums(): Array␊ export function sumNums(nums: Array): number␊ @@ -30,6 +34,10 @@ Generated by [AVA](https://avajs.dev). export enum CustomNumEnum { One = 1, Two = 2, Three = 3, Four = 4, Six = 6, Eight = 8, Nine = 9, Ten = 10 }␊ export function enumToI32(e: CustomNumEnum): number␊ export function throwError(): void␊ + export function createExternal(size: number): ExternalObject␊ + export function createExternalString(content: string): ExternalObject␊ + export function getExternal(external: ExternalObject): number␊ + export function mutateExternal(external: ExternalObject, newVal: number): void␊ export function mapOption(val?: number | undefined | null): number | undefined | null␊ export function add(a: number, b: number): number␊ export function fibonacci(n: number): number␊ diff --git a/examples/napi/__test__/typegen.spec.ts.snap b/examples/napi/__test__/typegen.spec.ts.snap index 717c673875..f471018bb2 100644 Binary files a/examples/napi/__test__/typegen.spec.ts.snap and b/examples/napi/__test__/typegen.spec.ts.snap differ diff --git a/examples/napi/__test__/values.spec.ts b/examples/napi/__test__/values.spec.ts index 01a9e92b27..64c285f6e0 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -48,6 +48,10 @@ import { setSymbolInObj, createSymbol, threadsafeFunctionFatalMode, + createExternal, + getExternal, + mutateExternal, + createExternalString, } from '../' test('export const', (t) => { @@ -237,13 +241,27 @@ test('either4', (t) => { t.is(either4({ v: 'world' }), 'world'.length) }) -test('async task without abort controller', async (t) => { - t.is(await withoutAbortController(1, 2), 3) +test('external', (t) => { + const FX = 42 + const ext = createExternal(FX) + t.is(getExternal(ext), FX) + mutateExternal(ext, FX + 1) + t.is(getExternal(ext), FX + 1) + // @ts-expect-error + t.throws(() => getExternal({})) + const ext2 = createExternalString('wtf') + // @ts-expect-error + const e = t.throws(() => getExternal(ext2)) + t.is(e.message, 'T on `get_value_external` is not the type of wrapped object') }) const AbortSignalTest = typeof AbortController !== 'undefined' ? test : test.skip +AbortSignalTest('async task without abort controller', async (t) => { + t.is(await withoutAbortController(1, 2), 3) +}) + AbortSignalTest('async task with abort controller', async (t) => { const ctrl = new AbortController() const promise = withAbortController(1, 2, ctrl.signal) diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index d5476cc4b7..cad9c2dc8c 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -1,3 +1,7 @@ +export class ExternalObject { + private readonly __type: unique symbol; + [val: unique symbol]: T +} export const DEFAULT_COST: number export function getWords(): Array export function getNums(): Array @@ -20,6 +24,10 @@ export enum Kind { Dog = 0, Cat = 1, Duck = 2 } export enum CustomNumEnum { One = 1, Two = 2, Three = 3, Four = 4, Six = 6, Eight = 8, Nine = 9, Ten = 10 } export function enumToI32(e: CustomNumEnum): number export function throwError(): void +export function createExternal(size: number): ExternalObject +export function createExternalString(content: string): ExternalObject +export function getExternal(external: ExternalObject): number +export function mutateExternal(external: ExternalObject, newVal: number): void export function mapOption(val?: number | undefined | null): number | undefined | null export function add(a: number, b: number): number export function fibonacci(n: number): number diff --git a/examples/napi/src/external.rs b/examples/napi/src/external.rs new file mode 100644 index 0000000000..699a8f80df --- /dev/null +++ b/examples/napi/src/external.rs @@ -0,0 +1,21 @@ +use napi::bindgen_prelude::*; + +#[napi] +pub fn create_external(size: u32) -> External { + External::new(size) +} + +#[napi] +pub fn create_external_string(content: String) -> External { + External::new(content) +} + +#[napi] +pub fn get_external(external: External) -> u32 { + *external.as_ref() +} + +#[napi] +pub fn mutate_external(mut external: External, new_val: u32) { + *external.as_mut() = new_val; +} diff --git a/examples/napi/src/lib.rs b/examples/napi/src/lib.rs index a06b769557..22e44b6617 100644 --- a/examples/napi/src/lib.rs +++ b/examples/napi/src/lib.rs @@ -15,6 +15,7 @@ mod class_factory; mod either; mod r#enum; mod error; +mod external; mod nullable; mod number; mod object;