Skip to content

Commit

Permalink
feat(napi): implement external value
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Nov 21, 2021
1 parent d1a5f84 commit bdfb150
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ yarn test
| Array | Array<any> | 1 | v8.0.0 |
| Vec<T> | Array<T> | 1 | v8.0.0 |
| Buffer | Buffer | 1 | v8.0.0 |
| External<T> | External<T> | 1 | v8.0.0 | |
| Null | null | 1 | v8.0.0 |
| Undefined/() | undefined | 1 | v8.0.0 |
| Result<()> | Error | 1 | v8.0.0 |
Expand Down
5 changes: 4 additions & 1 deletion cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,10 @@ async function processIntermediateTypeFile(
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
let dts = ''
let dts = `export class ExternalObject<T> {
private readonly __type: unique symbol;
[val: unique symbol]: T
}\n`
const classes = new Map<string, string>()
const impls = new Map<string, string>()

Expand Down
1 change: 1 addition & 0 deletions crates/backend/src/typegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ static KNOWN_TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
("AbortSignal", "AbortSignal"),
("JsFunction", "(...args: any[]) => any"),
("JsGlobal", "typeof global"),
("External", "ExternalObject<{}>"),
]);

map
Expand Down
2 changes: 2 additions & 0 deletions crates/napi/src/bindgen_runtime/js_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod bigint;
mod boolean;
mod buffer;
mod either;
mod external;
mod function;
mod map;
mod nil;
Expand All @@ -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::*;
Expand Down
110 changes: 110 additions & 0 deletions crates/napi/src/bindgen_runtime/js_values/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::any::TypeId;

use crate::{check_status, Error, Status, TaggedObject};

use super::{FromNapiValue, ToNapiValue};

pub struct External<T: 'static> {
obj: *mut TaggedObject<T>,
size_hint: usize,
pub adjusted_size: i64,
}

impl<T: 'static> External<T> {
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<T: 'static> FromNapiValue for External<T> {
unsafe fn from_napi_value(
env: napi_sys::napi_env,
napi_val: napi_sys::napi_value,
) -> crate::Result<Self> {
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::<T>() {
let tagged_object = unknown_tagged_object as *mut TaggedObject<T>;
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<T: 'static> AsRef<T> for External<T> {
fn as_ref(&self) -> &T {
unsafe { Box::leak(Box::from_raw(self.obj)).object.as_ref().unwrap() }
}
}

impl<T: 'static> AsMut<T> for External<T> {
fn as_mut(&mut self) -> &mut T {
unsafe { Box::leak(Box::from_raw(self.obj)).object.as_mut().unwrap() }
}
}

impl<T: 'static> ToNapiValue for External<T> {
unsafe fn to_napi_value(
env: napi_sys::napi_env,
mut val: Self,
) -> crate::Result<napi_sys::napi_value> {
let mut napi_value = std::ptr::null_mut();
check_status!(
napi_sys::napi_create_external(
env,
val.obj as *mut _,
Some(crate::raw_finalize::<T>),
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)
}
}
24 changes: 14 additions & 10 deletions crates/napi/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) })
}
Expand Down Expand Up @@ -1260,12 +1262,14 @@ pub(crate) unsafe extern "C" fn raw_finalize<T>(
if !finalize_hint.is_null() {
let size_hint = *Box::from_raw(finalize_hint as *mut Option<i64>);
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"
);
}
};
}
}
Expand Down
10 changes: 9 additions & 1 deletion examples/napi/__test__/typegen.spec.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ Generated by [AVA](https://avajs.dev).

> Snapshot 1
`export const DEFAULT_COST: number␊
`export class ExternalObject<T> {␊
private readonly __type: unique symbol;␊
[val: unique symbol]: T␊
}␊
export const DEFAULT_COST: number␊
export function getWords(): Array<string>␊
export function getNums(): Array<number>␊
export function sumNums(nums: Array<number>): number␊
Expand All @@ -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<number>␊
export function createExternalString(content: string): ExternalObject<string>␊
export function getExternal(external: ExternalObject<number>): number␊
export function mutateExternal(external: ExternalObject<number>, 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␊
Expand Down
Binary file modified examples/napi/__test__/typegen.spec.ts.snap
Binary file not shown.
22 changes: 20 additions & 2 deletions examples/napi/__test__/values.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import {
setSymbolInObj,
createSymbol,
threadsafeFunctionFatalMode,
createExternal,
getExternal,
mutateExternal,
createExternalString,
} from '../'

test('export const', (t) => {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions examples/napi/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export class ExternalObject<T> {
private readonly __type: unique symbol;
[val: unique symbol]: T
}
export const DEFAULT_COST: number
export function getWords(): Array<string>
export function getNums(): Array<number>
Expand All @@ -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<number>
export function createExternalString(content: string): ExternalObject<string>
export function getExternal(external: ExternalObject<number>): number
export function mutateExternal(external: ExternalObject<number>, 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
Expand Down
21 changes: 21 additions & 0 deletions examples/napi/src/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use napi::bindgen_prelude::*;

#[napi]
pub fn create_external(size: u32) -> External<u32> {
External::new(size)
}

#[napi]
pub fn create_external_string(content: String) -> External<String> {
External::new(content)
}

#[napi]
pub fn get_external(external: External<u32>) -> u32 {
*external.as_ref()
}

#[napi]
pub fn mutate_external(mut external: External<u32>, new_val: u32) {
*external.as_mut() = new_val;
}
1 change: 1 addition & 0 deletions examples/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod class_factory;
mod either;
mod r#enum;
mod error;
mod external;
mod nullable;
mod number;
mod object;
Expand Down

0 comments on commit bdfb150

Please sign in to comment.