fix(napi): asan caught memory safety issue

This commit is contained in:
LongYinan 2023-11-03 12:09:06 +08:00
parent afd3395bb4
commit 0dc1ef738b
14 changed files with 205 additions and 261 deletions

View file

@ -33,7 +33,7 @@ build_and_test: &BUILD_AND_TEST
freebsd_task: freebsd_task:
name: FreeBSD name: FreeBSD
freebsd_instance: freebsd_instance:
image: freebsd-13-1-release-amd64 image: freebsd-13-2-release-amd64
env: env:
RUSTUP_HOME: /usr/local/rustup RUSTUP_HOME: /usr/local/rustup
CARGO_HOME: /usr/local/cargo CARGO_HOME: /usr/local/cargo

View file

@ -41,7 +41,7 @@ impl NapiConst {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), not(target_arch = "wasm32")))] #[cfg(all(not(test), not(feature = "noop"), not(target_os = "wasi")))]
#[napi::bindgen_prelude::ctor] #[napi::bindgen_prelude::ctor]
fn #register_name() { fn #register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #cb_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #cb_name);
@ -49,7 +49,7 @@ impl NapiConst {
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), target_arch = "wasm32"))] #[cfg(all(not(test), not(feature = "noop"), target_os = "wasi"))]
#[no_mangle] #[no_mangle]
unsafe extern "C" fn #register_name() { unsafe extern "C" fn #register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #cb_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #cb_name);

View file

@ -158,14 +158,14 @@ impl NapiEnum {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), not(target_arch = "wasm32")))] #[cfg(all(not(test), not(feature = "noop"), not(target_os = "wasi")))]
#[napi::bindgen_prelude::ctor] #[napi::bindgen_prelude::ctor]
fn #register_name() { fn #register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #callback_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #callback_name);
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), target_arch = "wasm32"))] #[cfg(all(not(test), not(feature = "noop"), target_os = "wasi"))]
#[no_mangle] #[no_mangle]
extern "C" fn #register_name() { extern "C" fn #register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #callback_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name_lit, #callback_name);

View file

@ -589,7 +589,7 @@ impl NapiFn {
#[allow(clippy::all)] #[allow(clippy::all)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[cfg(all(not(test), not(feature = "noop"), not(target_arch = "wasm32")))] #[cfg(all(not(test), not(feature = "noop"), not(target_os = "wasi")))]
#[napi::bindgen_prelude::ctor] #[napi::bindgen_prelude::ctor]
fn #module_register_name() { fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);
@ -597,7 +597,7 @@ impl NapiFn {
#[allow(clippy::all)] #[allow(clippy::all)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[cfg(all(not(test), not(feature = "noop"), target_arch = "wasm32"))] #[cfg(all(not(test), not(feature = "noop"), target_os = "wasi"))]
#[no_mangle] #[no_mangle]
extern "C" fn #module_register_name() { extern "C" fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name); napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);

View file

@ -778,7 +778,7 @@ impl NapiStruct {
quote! { quote! {
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), not(target_arch = "wasm32")))] #[cfg(all(not(test), not(feature = "noop"), not(target_os = "wasi")))]
#[napi::bindgen_prelude::ctor] #[napi::bindgen_prelude::ctor]
fn #struct_register_name() { fn #struct_register_name() {
napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]); napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]);
@ -786,7 +786,7 @@ impl NapiStruct {
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[allow(clippy::all)] #[allow(clippy::all)]
#[cfg(all(not(test), not(feature = "noop"), target_arch = "wasm32"))] #[cfg(all(not(test), not(feature = "noop"), target_os = "wasi"))]
#[no_mangle] #[no_mangle]
extern "C" fn #struct_register_name() { extern "C" fn #struct_register_name() {
napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]); napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]);
@ -901,13 +901,13 @@ impl NapiImpl {
use super::*; use super::*;
#(#methods)* #(#methods)*
#[cfg(all(not(test), not(feature = "noop"), not(target_arch = "wasm32")))] #[cfg(all(not(test), not(feature = "noop"), not(target_os = "wasi")))]
#[napi::bindgen_prelude::ctor] #[napi::bindgen_prelude::ctor]
fn #register_name() { fn #register_name() {
napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]); napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props),*]);
} }
#[cfg(all(not(test), not(feature = "noop"), target_arch = "wasm32"))] #[cfg(all(not(test), not(feature = "noop"), target_os = "wasi"))]
#[no_mangle] #[no_mangle]
extern "C" fn #register_name() { extern "C" fn #register_name() {
napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props_wasm),*]); napi::__private::register_class(#name_str, #js_mod_ident, #js_name, vec![#(#props_wasm),*]);

View file

@ -151,7 +151,7 @@ pub fn module_exports(_attr: TokenStream, input: TokenStream) -> TokenStream {
}; };
let register = quote! { let register = quote! {
#[cfg_attr(not(target_arch = "wasm32"), napi::bindgen_prelude::ctor)] #[cfg_attr(not(target_os = "wasi"), napi::bindgen_prelude::ctor)]
fn __napi__explicit_module_register() { fn __napi__explicit_module_register() {
unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> {
use napi::{Env, JsObject, NapiValue}; use napi::{Env, JsObject, NapiValue};

View file

@ -8,7 +8,7 @@ use std::sync::{
}; };
#[cfg(all(feature = "napi4", not(target_os = "wasi")))] #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
use crate::bindgen_prelude::{CUSTOM_GC_TSFN, THREADS_CAN_ACCESS_ENV, THREAD_DESTROYED}; use crate::bindgen_prelude::{CUSTOM_GC_TSFN, CUSTOM_GC_TSFN_DESTROYED, THREADS_CAN_ACCESS_ENV};
pub use crate::js_values::TypedArrayType; pub use crate::js_values::TypedArrayType;
use crate::{check_status, sys, Error, Result, Status}; use crate::{check_status, sys, Error, Result, Status};
@ -77,14 +77,17 @@ macro_rules! impl_typed_array {
fn drop(&mut self) { fn drop(&mut self) {
if Arc::strong_count(&self.drop_in_vm) == 1 { if Arc::strong_count(&self.drop_in_vm) == 1 {
if let Some((ref_, env)) = self.raw { if let Some((ref_, env)) = self.raw {
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] if ref_.is_null() {
return;
}
#[cfg(not(target_os = "wasi"))]
if CUSTOM_GC_TSFN_DESTROYED.load(Ordering::SeqCst) {
return;
}
#[cfg(all(feature = "napi4", not(target_os = "wasi")))]
{ {
if THREAD_DESTROYED.with(|closed| closed.load(std::sync::atomic::Ordering::Relaxed)) {
return;
}
if !THREADS_CAN_ACCESS_ENV if !THREADS_CAN_ACCESS_ENV
.get_or_init(Default::default) .borrow_mut(|m| m.get(&std::thread::current().id()).is_some())
.contains(&std::thread::current().id())
{ {
let status = unsafe { let status = unsafe {
sys::napi_call_threadsafe_function( sys::napi_call_threadsafe_function(
@ -94,18 +97,23 @@ macro_rules! impl_typed_array {
) )
}; };
assert!( assert!(
status == sys::Status::napi_ok, status == sys::Status::napi_ok || status == sys::Status::napi_closing,
"Call custom GC in ArrayBuffer::drop failed {:?}", "Call custom GC in ArrayBuffer::drop failed {}",
Status::from(status) Status::from(status)
); );
return; return;
} }
} }
let mut ref_count = 0;
crate::check_status_or_throw!( crate::check_status_or_throw!(
env, env,
unsafe { sys::napi_reference_unref(env, ref_, &mut 0) }, unsafe { sys::napi_reference_unref(env, ref_, &mut ref_count) },
"Failed to unref ArrayBuffer reference in drop" "Failed to unref ArrayBuffer reference in drop"
); );
debug_assert!(
ref_count == 0,
"ArrayBuffer reference count in ArrayBuffer::drop is not zero"
);
crate::check_status_or_throw!( crate::check_status_or_throw!(
env, env,
unsafe { sys::napi_delete_reference(env, ref_) }, unsafe { sys::napi_delete_reference(env, ref_) },
@ -344,13 +352,21 @@ macro_rules! impl_typed_array {
} }
impl ToNapiValue for $name { impl ToNapiValue for $name {
unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> { unsafe fn to_napi_value(env: sys::napi_env, mut val: Self) -> Result<sys::napi_value> {
if let Some((ref_, _)) = val.raw { if let Some((ref_, _)) = val.raw {
let mut napi_value = std::ptr::null_mut(); let mut napi_value = std::ptr::null_mut();
check_status!( check_status!(
unsafe { sys::napi_get_reference_value(env, ref_, &mut napi_value) }, unsafe { sys::napi_get_reference_value(env, ref_, &mut napi_value) },
"Failed to delete reference from Buffer" "Failed to get reference from ArrayBuffer"
)?; )?;
// fast path for ArrayBuffer::drop
if Arc::strong_count(&val.drop_in_vm) == 1 {
check_status!(
unsafe { sys::napi_delete_reference(env, ref_) },
"Failed to delete reference in ArrayBuffer::to_napi_value"
)?;
val.raw = Some((ptr::null_mut(), ptr::null_mut()));
}
return Ok(napi_value); return Ok(napi_value);
} }
let mut arraybuffer_value = ptr::null_mut(); let mut arraybuffer_value = ptr::null_mut();
@ -422,7 +438,7 @@ macro_rules! impl_typed_array {
let mut napi_value = std::ptr::null_mut(); let mut napi_value = std::ptr::null_mut();
check_status!( check_status!(
unsafe { sys::napi_get_reference_value(env, ref_, &mut napi_value) }, unsafe { sys::napi_get_reference_value(env, ref_, &mut napi_value) },
"Failed to delete reference from Buffer" "Failed to get reference from ArrayBuffer"
)?; )?;
Ok(napi_value) Ok(napi_value)
} else { } else {

View file

@ -9,8 +9,8 @@ use std::sync::Arc;
#[cfg(all(debug_assertions, not(windows)))] #[cfg(all(debug_assertions, not(windows)))]
use std::sync::Mutex; use std::sync::Mutex;
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
use crate::bindgen_prelude::{CUSTOM_GC_TSFN, THREADS_CAN_ACCESS_ENV, THREAD_DESTROYED}; use crate::bindgen_prelude::{CUSTOM_GC_TSFN, CUSTOM_GC_TSFN_DESTROYED, THREADS_CAN_ACCESS_ENV};
use crate::{bindgen_prelude::*, check_status, sys, Result, ValueType}; use crate::{bindgen_prelude::*, check_status, sys, Result, ValueType};
#[cfg(all(debug_assertions, not(windows)))] #[cfg(all(debug_assertions, not(windows)))]
@ -34,15 +34,16 @@ impl Drop for Buffer {
fn drop(&mut self) { fn drop(&mut self) {
if Arc::strong_count(&self.ref_count) == 1 { if Arc::strong_count(&self.ref_count) == 1 {
if let Some((ref_, env)) = self.raw { if let Some((ref_, env)) = self.raw {
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] if ref_.is_null() {
return;
}
#[cfg(not(target_os = "wasi"))]
if CUSTOM_GC_TSFN_DESTROYED.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
#[cfg(all(feature = "napi4", not(target_os = "wasi")))]
{ {
if THREAD_DESTROYED.with(|closed| closed.load(std::sync::atomic::Ordering::Relaxed)) { if !THREADS_CAN_ACCESS_ENV.borrow_mut(|m| m.get(&std::thread::current().id()).is_some()) {
return;
}
if !THREADS_CAN_ACCESS_ENV
.get_or_init(Default::default)
.contains(&std::thread::current().id())
{
let status = unsafe { let status = unsafe {
sys::napi_call_threadsafe_function( sys::napi_call_threadsafe_function(
CUSTOM_GC_TSFN.load(std::sync::atomic::Ordering::SeqCst), CUSTOM_GC_TSFN.load(std::sync::atomic::Ordering::SeqCst),
@ -51,8 +52,8 @@ impl Drop for Buffer {
) )
}; };
assert!( assert!(
status == sys::Status::napi_ok, status == sys::Status::napi_ok || status == sys::Status::napi_closing,
"Call custom GC in ArrayBuffer::drop failed {:?}", "Call custom GC in Buffer::drop failed {}",
Status::from(status) Status::from(status)
); );
return; return;
@ -64,6 +65,10 @@ impl Drop for Buffer {
unsafe { sys::napi_reference_unref(env, ref_, &mut ref_count) }, unsafe { sys::napi_reference_unref(env, ref_, &mut ref_count) },
"Failed to unref Buffer reference in drop" "Failed to unref Buffer reference in drop"
); );
debug_assert!(
ref_count == 0,
"Buffer reference count in Buffer::drop is not zero"
);
check_status_or_throw!( check_status_or_throw!(
env, env,
unsafe { sys::napi_delete_reference(env, ref_) }, unsafe { sys::napi_delete_reference(env, ref_) },
@ -217,7 +222,7 @@ impl FromNapiValue for Buffer {
} }
impl ToNapiValue for Buffer { impl ToNapiValue for Buffer {
unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> { unsafe fn to_napi_value(env: sys::napi_env, mut val: Self) -> Result<sys::napi_value> {
// From Node.js value, not from `Vec<u8>` // From Node.js value, not from `Vec<u8>`
if let Some((ref_, _)) = val.raw { if let Some((ref_, _)) = val.raw {
let mut buf = ptr::null_mut(); let mut buf = ptr::null_mut();
@ -225,6 +230,14 @@ impl ToNapiValue for Buffer {
unsafe { sys::napi_get_reference_value(env, ref_, &mut buf) }, unsafe { sys::napi_get_reference_value(env, ref_, &mut buf) },
"Failed to get Buffer value from reference" "Failed to get Buffer value from reference"
)?; )?;
// fast path for Buffer::drop
if Arc::strong_count(&val.ref_count) == 1 {
check_status!(
unsafe { sys::napi_delete_reference(env, ref_) },
"Failed to delete Buffer reference in Buffer::to_napi_value"
)?;
val.raw = Some((ptr::null_mut(), ptr::null_mut()));
}
return Ok(buf); return Ok(buf);
} }
let len = val.len; let len = val.len;

View file

@ -1,11 +1,14 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::ffi::CStr; use std::ffi::CStr;
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))]
use std::hash::Hash;
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
use std::ops::Deref;
use std::ptr; use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; #[cfg(all(
any(target_os = "windows", target_os = "freebsd"),
feature = "napi4",
feature = "tokio_rt"
))]
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use std::sync::RwLock;
use std::thread::ThreadId; use std::thread::ThreadId;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -18,109 +21,13 @@ pub type ExportRegisterCallback = unsafe fn(sys::napi_env) -> Result<sys::napi_v
pub type ModuleExportsCallback = pub type ModuleExportsCallback =
unsafe fn(env: sys::napi_env, exports: sys::napi_value) -> Result<()>; unsafe fn(env: sys::napi_env, exports: sys::napi_value) -> Result<()>;
struct PersistedPerInstanceVec<T> { #[repr(transparent)]
inner: AtomicPtr<T>, pub(crate) struct PersistedPerInstanceHashMap<K, V>(RwLock<HashMap<K, V>>);
length: AtomicUsize,
}
impl<T> Default for PersistedPerInstanceVec<T> {
fn default() -> Self {
let mut vec: Vec<T> = Vec::with_capacity(1);
let ret = Self {
inner: AtomicPtr::new(vec.as_mut_ptr()),
length: AtomicUsize::new(0),
};
std::mem::forget(vec);
ret
}
}
impl<T> PersistedPerInstanceVec<T> {
#[cfg(not(feature = "noop"))]
#[allow(clippy::mut_from_ref)]
fn borrow_mut<F>(&self, f: F)
where
F: FnOnce(&mut [T]),
{
let length = self.length.load(Ordering::Relaxed);
if length == 0 {
f(&mut []);
} else {
let inner = self.inner.load(Ordering::Relaxed);
let mut temp =
std::mem::ManuallyDrop::new(unsafe { Vec::from_raw_parts(inner, length, length) });
f(temp.as_mut_slice());
// Inner Vec has been reallocated, so we need to update the pointer
if temp.as_mut_ptr() != inner {
self.inner.store(temp.as_mut_ptr(), Ordering::Relaxed);
}
self.length.store(temp.len(), Ordering::Relaxed);
}
}
fn push(&self, item: T) {
let length = self.length.load(Ordering::Relaxed);
let inner = self.inner.load(Ordering::Relaxed);
let mut temp =
std::mem::ManuallyDrop::new(unsafe { Vec::from_raw_parts(inner, length, length) });
temp.push(item);
// Inner Vec has been reallocated, so we need to update the pointer
if temp.as_mut_ptr() != inner {
self.inner.store(temp.as_mut_ptr(), Ordering::Relaxed);
}
self.length.fetch_add(1, Ordering::Relaxed);
}
}
unsafe impl<T: Send> Send for PersistedPerInstanceVec<T> {}
unsafe impl<T: Sync> Sync for PersistedPerInstanceVec<T> {}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
pub(crate) struct PersistedPerInstanceHashSet<T: 'static> {
inner: *mut HashSet<T>,
}
#[cfg(not(feature = "noop"))]
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
impl<T: 'static + PartialEq + Eq + Hash> PersistedPerInstanceHashSet<T> {
pub(crate) fn insert(&self, item: T) {
Box::leak(unsafe { Box::from_raw(self.inner) }).insert(item);
}
fn remove(&self, item: &T) {
Box::leak(unsafe { Box::from_raw(self.inner) }).remove(item);
}
}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
impl<T: 'static> Deref for PersistedPerInstanceHashSet<T> {
type Target = HashSet<T>;
fn deref(&self) -> &Self::Target {
Box::leak(unsafe { Box::from_raw(self.inner) })
}
}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
impl<T: 'static> Default for PersistedPerInstanceHashSet<T> {
fn default() -> Self {
Self {
inner: Box::leak(Box::default()),
}
}
}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
unsafe impl<T: Send> Send for PersistedPerInstanceHashSet<T> {}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
unsafe impl<T: Sync> Sync for PersistedPerInstanceHashSet<T> {}
pub(crate) struct PersistedPerInstanceHashMap<K, V>(*mut HashMap<K, V>);
impl<K, V> PersistedPerInstanceHashMap<K, V> { impl<K, V> PersistedPerInstanceHashMap<K, V> {
#[cfg(not(feature = "noop"))] #[cfg(not(feature = "noop"))]
pub(crate) fn from_hashmap(hashmap: HashMap<K, V>) -> Self { pub(crate) fn from_hashmap(hashmap: HashMap<K, V>) -> Self {
Self(Box::into_raw(Box::new(hashmap))) Self(RwLock::new(hashmap))
} }
#[allow(clippy::mut_from_ref)] #[allow(clippy::mut_from_ref)]
@ -128,19 +35,19 @@ impl<K, V> PersistedPerInstanceHashMap<K, V> {
where where
F: FnOnce(&mut HashMap<K, V>) -> R, F: FnOnce(&mut HashMap<K, V>) -> R,
{ {
f(unsafe { Box::leak(Box::from_raw(self.0)) }) let mut write_lock = self.0.write().unwrap();
f(&mut *write_lock)
} }
} }
impl<K, V> Default for PersistedPerInstanceHashMap<K, V> { impl<K, V> Default for PersistedPerInstanceHashMap<K, V> {
fn default() -> Self { fn default() -> Self {
let map = Default::default(); Self(RwLock::new(HashMap::default()))
Self(Box::into_raw(Box::new(map)))
} }
} }
type ModuleRegisterCallback = type ModuleRegisterCallback =
PersistedPerInstanceVec<(Option<&'static str>, (&'static str, ExportRegisterCallback))>; RwLock<Vec<(Option<&'static str>, (&'static str, ExportRegisterCallback))>>;
type ModuleClassProperty = PersistedPerInstanceHashMap< type ModuleClassProperty = PersistedPerInstanceHashMap<
&'static str, &'static str,
@ -162,28 +69,23 @@ static IS_FIRST_MODULE: AtomicBool = AtomicBool::new(true);
static FIRST_MODULE_REGISTERED: AtomicBool = AtomicBool::new(false); static FIRST_MODULE_REGISTERED: AtomicBool = AtomicBool::new(false);
static REGISTERED_CLASSES: Lazy<RegisteredClassesMap> = Lazy::new(Default::default); static REGISTERED_CLASSES: Lazy<RegisteredClassesMap> = Lazy::new(Default::default);
static FN_REGISTER_MAP: Lazy<FnRegisterMap> = Lazy::new(Default::default); static FN_REGISTER_MAP: Lazy<FnRegisterMap> = Lazy::new(Default::default);
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
pub(crate) static CUSTOM_GC_TSFN: AtomicPtr<sys::napi_threadsafe_function__> = pub(crate) static CUSTOM_GC_TSFN: AtomicPtr<sys::napi_threadsafe_function__> =
AtomicPtr::new(ptr::null_mut()); AtomicPtr::new(ptr::null_mut());
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
thread_local! { pub(crate) static CUSTOM_GC_TSFN_DESTROYED: AtomicBool = AtomicBool::new(false);
// CustomGC ThreadsafeFunction may be deleted during the process exit. #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
// And there may still some Buffer alive after that.
pub(crate) static THREAD_DESTROYED: AtomicBool = AtomicBool::new(false);
}
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
// Store thread id of the thread that created the CustomGC ThreadsafeFunction. // Store thread id of the thread that created the CustomGC ThreadsafeFunction.
pub(crate) static THREADS_CAN_ACCESS_ENV: once_cell::sync::OnceCell< pub(crate) static THREADS_CAN_ACCESS_ENV: once_cell::sync::Lazy<
PersistedPerInstanceHashSet<ThreadId>, PersistedPerInstanceHashMap<ThreadId, bool>,
> = once_cell::sync::OnceCell::new(); > = once_cell::sync::Lazy::new(Default::default);
type RegisteredClasses = type RegisteredClasses =
PersistedPerInstanceHashMap</* export name */ String, /* constructor */ sys::napi_ref>; PersistedPerInstanceHashMap</* export name */ String, /* constructor */ sys::napi_ref>;
#[cfg(all(feature = "compat-mode", not(feature = "noop")))] #[cfg(all(feature = "compat-mode", not(feature = "noop")))]
// compatibility for #[module_exports] // compatibility for #[module_exports]
static MODULE_EXPORTS: Lazy<PersistedPerInstanceVec<ModuleExportsCallback>> = static MODULE_EXPORTS: Lazy<RwLock<Vec<ModuleExportsCallback>>> = Lazy::new(Default::default);
Lazy::new(Default::default);
#[cfg(not(feature = "noop"))] #[cfg(not(feature = "noop"))]
#[inline] #[inline]
@ -207,7 +109,10 @@ pub fn get_class_constructor(js_name: &'static str) -> Option<sys::napi_ref> {
#[cfg(all(feature = "compat-mode", not(feature = "noop")))] #[cfg(all(feature = "compat-mode", not(feature = "noop")))]
// compatibility for #[module_exports] // compatibility for #[module_exports]
pub fn register_module_exports(callback: ModuleExportsCallback) { pub fn register_module_exports(callback: ModuleExportsCallback) {
MODULE_EXPORTS.push(callback); MODULE_EXPORTS
.write()
.expect("Register module exports failed")
.push(callback);
} }
#[doc(hidden)] #[doc(hidden)]
@ -216,7 +121,10 @@ pub fn register_module_export(
name: &'static str, name: &'static str,
cb: ExportRegisterCallback, cb: ExportRegisterCallback,
) { ) {
MODULE_REGISTER_CALLBACK.push((js_mod, (name, cb))); MODULE_REGISTER_CALLBACK
.write()
.expect("Register module export failed")
.push((js_mod, (name, cb)));
} }
#[doc(hidden)] #[doc(hidden)]
@ -338,7 +246,7 @@ fn load_host() {
} }
} }
#[cfg(all(target_arch = "wasm32", not(feature = "noop")))] #[cfg(all(target_os = "wasi", not(feature = "noop")))]
#[no_mangle] #[no_mangle]
unsafe extern "C" fn napi_register_wasm_v1( unsafe extern "C" fn napi_register_wasm_v1(
env: sys::napi_env, env: sys::napi_env,
@ -366,8 +274,12 @@ pub unsafe extern "C" fn napi_register_module_v1(
wait_first_thread_registered(); wait_first_thread_registered();
} }
let mut exports_objects: HashSet<String> = HashSet::default(); let mut exports_objects: HashSet<String> = HashSet::default();
MODULE_REGISTER_CALLBACK.borrow_mut(|inner| {
inner {
let mut register_callback = MODULE_REGISTER_CALLBACK
.write()
.expect("Write MODULE_REGISTER_CALLBACK in napi_register_module_v1 failed");
register_callback
.iter_mut() .iter_mut()
.fold( .fold(
HashMap::<Option<&'static str>, Vec<(&'static str, ExportRegisterCallback)>>::new(), HashMap::<Option<&'static str>, Vec<(&'static str, ExportRegisterCallback)>>::new(),
@ -437,8 +349,8 @@ pub unsafe extern "C" fn napi_register_module_v1(
} }
} }
} }
}) });
}); }
let mut registered_classes = HashMap::new(); let mut registered_classes = HashMap::new();
@ -536,15 +448,20 @@ pub unsafe extern "C" fn napi_register_module_v1(
}); });
#[cfg(feature = "compat-mode")] #[cfg(feature = "compat-mode")]
MODULE_EXPORTS.borrow_mut(|inner| { {
inner.iter().for_each(|callback| unsafe { let module_exports = MODULE_EXPORTS.read().expect("Read MODULE_EXPORTS failed");
module_exports.iter().for_each(|callback| unsafe {
if let Err(e) = callback(env, exports) { if let Err(e) = callback(env, exports) {
JsError::from(e).throw_into(env); JsError::from(e).throw_into(env);
} }
}) })
}); }
#[cfg(all(windows, feature = "napi4", feature = "tokio_rt"))] #[cfg(all(
any(target_os = "windows", target_os = "freebsd"),
feature = "napi4",
feature = "tokio_rt"
))]
{ {
crate::tokio_runtime::ensure_runtime(); crate::tokio_runtime::ensure_runtime();
@ -560,7 +477,7 @@ pub unsafe extern "C" fn napi_register_module_v1(
) )
}; };
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))] #[cfg(all(feature = "napi4", not(target_os = "wasi")))]
create_custom_gc(env); create_custom_gc(env);
FIRST_MODULE_REGISTERED.store(true, Ordering::SeqCst); FIRST_MODULE_REGISTERED.store(true, Ordering::SeqCst);
exports exports
@ -584,67 +501,69 @@ pub(crate) unsafe extern "C" fn noop(
ptr::null_mut() ptr::null_mut()
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))] #[cfg(all(feature = "napi4", not(target_os = "wasi"), not(feature = "noop")))]
fn create_custom_gc(env: sys::napi_env) { fn create_custom_gc(env: sys::napi_env) {
use std::os::raw::c_char; use std::os::raw::c_char;
let mut custom_gc_fn = ptr::null_mut(); if !FIRST_MODULE_REGISTERED.load(Ordering::SeqCst) {
check_status_or_throw!( let mut custom_gc_fn = ptr::null_mut();
env, check_status_or_throw!(
unsafe { env,
sys::napi_create_function( unsafe {
env, sys::napi_create_function(
"custom_gc".as_ptr() as *const c_char, env,
9, "custom_gc".as_ptr().cast(),
Some(empty), 9,
ptr::null_mut(), Some(empty),
&mut custom_gc_fn, ptr::null_mut(),
) &mut custom_gc_fn,
}, )
"Create Custom GC Function in napi_register_module_v1 failed" },
); "Create Custom GC Function in napi_register_module_v1 failed"
let mut async_resource_name = ptr::null_mut(); );
check_status_or_throw!( let mut async_resource_name = ptr::null_mut();
env, check_status_or_throw!(
unsafe { env,
sys::napi_create_string_utf8( unsafe {
env, sys::napi_create_string_utf8(
"CustomGC".as_ptr() as *const c_char, env,
8, "CustomGC".as_ptr() as *const c_char,
&mut async_resource_name, 8,
) &mut async_resource_name,
}, )
"Create async resource string in napi_register_module_v1" },
); "Create async resource string in napi_register_module_v1"
let mut custom_gc_tsfn = ptr::null_mut(); );
check_status_or_throw!( let mut custom_gc_tsfn = ptr::null_mut();
env, check_status_or_throw!(
unsafe { env,
sys::napi_create_threadsafe_function( unsafe {
env, sys::napi_create_threadsafe_function(
custom_gc_fn, env,
ptr::null_mut(), custom_gc_fn,
async_resource_name, ptr::null_mut(),
0, async_resource_name,
1, 0,
ptr::null_mut(), 1,
Some(custom_gc_finalize), ptr::null_mut(),
ptr::null_mut(), Some(custom_gc_finalize),
Some(custom_gc), ptr::null_mut(),
&mut custom_gc_tsfn, Some(custom_gc),
) &mut custom_gc_tsfn,
}, )
"Create Custom GC ThreadsafeFunction in napi_register_module_v1 failed" },
); "Create Custom GC ThreadsafeFunction in napi_register_module_v1 failed"
check_status_or_throw!( );
env, check_status_or_throw!(
unsafe { sys::napi_unref_threadsafe_function(env, custom_gc_tsfn) }, env,
"Unref Custom GC ThreadsafeFunction in napi_register_module_v1 failed" unsafe { sys::napi_unref_threadsafe_function(env, custom_gc_tsfn) },
); "Unref Custom GC ThreadsafeFunction in napi_register_module_v1 failed"
CUSTOM_GC_TSFN.store(custom_gc_tsfn, Ordering::Relaxed); );
let threads = THREADS_CAN_ACCESS_ENV.get_or_init(Default::default); CUSTOM_GC_TSFN.store(custom_gc_tsfn, Ordering::Relaxed);
}
let current_thread_id = std::thread::current().id(); let current_thread_id = std::thread::current().id();
threads.insert(current_thread_id); THREADS_CAN_ACCESS_ENV.borrow_mut(|m| m.insert(current_thread_id, true));
check_status_or_throw!( check_status_or_throw!(
env, env,
unsafe { unsafe {
@ -658,33 +577,29 @@ fn create_custom_gc(env: sys::napi_env) {
); );
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))] #[cfg(all(feature = "napi4", not(target_os = "wasi"), not(feature = "noop")))]
unsafe extern "C" fn remove_thread_id(id: *mut std::ffi::c_void) { unsafe extern "C" fn remove_thread_id(id: *mut std::ffi::c_void) {
let thread_id = unsafe { Box::from_raw(id.cast::<ThreadId>()) }; let thread_id = unsafe { Box::from_raw(id.cast::<ThreadId>()) };
THREADS_CAN_ACCESS_ENV THREADS_CAN_ACCESS_ENV.borrow_mut(|m| m.insert(*thread_id, false));
.get_or_init(Default::default)
.remove(&*thread_id);
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))] #[cfg(all(feature = "napi4", not(target_os = "wasi"), not(feature = "noop")))]
#[allow(unused)] #[allow(unused)]
unsafe extern "C" fn empty(env: sys::napi_env, info: sys::napi_callback_info) -> sys::napi_value { unsafe extern "C" fn empty(env: sys::napi_env, info: sys::napi_callback_info) -> sys::napi_value {
ptr::null_mut() ptr::null_mut()
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))] #[cfg(all(feature = "napi4", not(target_os = "wasi"), not(feature = "noop")))]
#[allow(unused)] #[allow(unused_variables)]
unsafe extern "C" fn custom_gc_finalize( unsafe extern "C" fn custom_gc_finalize(
env: sys::napi_env, env: sys::napi_env,
finalize_data: *mut std::ffi::c_void, finalize_data: *mut std::ffi::c_void,
finalize_hint: *mut std::ffi::c_void, finalize_hint: *mut std::ffi::c_void,
) { ) {
THREAD_DESTROYED.with(|closed| { CUSTOM_GC_TSFN_DESTROYED.store(true, Ordering::SeqCst);
closed.store(true, Ordering::Relaxed);
});
} }
#[cfg(all(feature = "napi4", not(target_arch = "wasm32"), not(feature = "noop")))] #[cfg(all(feature = "napi4", not(target_os = "wasi"), not(feature = "noop")))]
// recycle the ArrayBuffer/Buffer Reference if the ArrayBuffer/Buffer is not dropped on the main thread // recycle the ArrayBuffer/Buffer Reference if the ArrayBuffer/Buffer is not dropped on the main thread
extern "C" fn custom_gc( extern "C" fn custom_gc(
env: sys::napi_env, env: sys::napi_env,
@ -692,9 +607,14 @@ extern "C" fn custom_gc(
_context: *mut std::ffi::c_void, _context: *mut std::ffi::c_void,
data: *mut std::ffi::c_void, data: *mut std::ffi::c_void,
) { ) {
// current thread was destroyed
if THREADS_CAN_ACCESS_ENV.borrow_mut(|m| m.get(&std::thread::current().id()) == Some(&false)) {
return;
}
let mut reference = 0;
check_status_or_throw!( check_status_or_throw!(
env, env,
unsafe { sys::napi_delete_reference(env, data as sys::napi_ref) }, unsafe { sys::napi_reference_unref(env, data.cast(), &mut reference) },
"Failed to delete Buffer reference in Custom GC" "Failed to delete Buffer reference in Custom GC"
); );
} }

View file

@ -3,8 +3,6 @@ use std::marker::PhantomData;
use std::os::raw::c_void; use std::os::raw::c_void;
use std::ptr; use std::ptr;
#[cfg(all(feature = "napi4", not(target_arch = "wasm32")))]
use crate::bindgen_prelude::THREAD_DESTROYED;
use crate::bindgen_runtime::ToNapiValue; use crate::bindgen_runtime::ToNapiValue;
use crate::{check_status, JsObject, Value}; use crate::{check_status, JsObject, Value};
use crate::{sys, Env, Error, Result}; use crate::{sys, Env, Error, Result};
@ -208,12 +206,6 @@ extern "C" fn napi_resolve_deferred<Data: ToNapiValue, Resolver: FnOnce(Env) ->
context: *mut c_void, context: *mut c_void,
data: *mut c_void, data: *mut c_void,
) { ) {
#[cfg(not(target_arch = "wasm32"))]
{
if THREAD_DESTROYED.with(|closed| closed.load(std::sync::atomic::Ordering::Relaxed)) {
return;
}
}
let deferred = context.cast(); let deferred = context.cast();
let deferred_data: Box<DeferredData<Data, Resolver>> = unsafe { Box::from_raw(data.cast()) }; let deferred_data: Box<DeferredData<Data, Resolver>> = unsafe { Box::from_raw(data.cast()) };
let result = deferred_data let result = deferred_data

View file

@ -23,14 +23,14 @@ fn create_runtime() -> Option<Runtime> {
pub(crate) static RT: Lazy<RwLock<Option<Runtime>>> = Lazy::new(|| RwLock::new(create_runtime())); pub(crate) static RT: Lazy<RwLock<Option<Runtime>>> = Lazy::new(|| RwLock::new(create_runtime()));
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "freebsd"))]
static RT_REFERENCE_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); static RT_REFERENCE_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
/// Ensure that the Tokio runtime is initialized. /// Ensure that the Tokio runtime is initialized.
/// In windows the Tokio runtime will be dropped when Node env exits. /// In windows the Tokio runtime will be dropped when Node env exits.
/// But in Electron renderer process, the Node env will exits and recreate when the window reloads. /// But in Electron renderer process, the Node env will exits and recreate when the window reloads.
/// So we need to ensure that the Tokio runtime is initialized when the Node env is created. /// So we need to ensure that the Tokio runtime is initialized when the Node env is created.
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "freebsd"))]
pub(crate) fn ensure_runtime() { pub(crate) fn ensure_runtime() {
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -42,7 +42,7 @@ pub(crate) fn ensure_runtime() {
RT_REFERENCE_COUNT.fetch_add(1, Ordering::Relaxed); RT_REFERENCE_COUNT.fetch_add(1, Ordering::Relaxed);
} }
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "freebsd"))]
pub(crate) unsafe extern "C" fn drop_runtime(_arg: *mut std::ffi::c_void) { pub(crate) unsafe extern "C" fn drop_runtime(_arg: *mut std::ffi::c_void) {
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -104,10 +104,10 @@ pub fn execute_tokio_future<
} }
}; };
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_os = "wasi"))]
spawn(inner); spawn(inner);
#[cfg(target_arch = "wasm32")] #[cfg(target_os = "wasi")]
block_on(inner); block_on(inner);
Ok(promise.0.value) Ok(promise.0.value)

View file

@ -8,14 +8,19 @@ const { Animal, Kind, DEFAULT_COST } = (await import('../index.js')).default
const __dirname = join(fileURLToPath(import.meta.url), '..') const __dirname = join(fileURLToPath(import.meta.url), '..')
// aarch64-unknown-linux-gnu is extremely slow in CI, skip it or it will timeout
const t = const t =
// aarch64-unknown-linux-gnu is extremely slow in CI, skip it or it will timeout
(process.arch === 'arm64' && process.platform === 'linux') || (process.arch === 'arm64' && process.platform === 'linux') ||
process.env.WASI_TEST process.env.WASI_TEST
? test.skip ? test.skip
: test : test
const concurrency = process.platform === 'win32' || process.platform === 'darwin' || (process.platform === 'linux' && process.arch === 'x64') ? 50 : 10 const concurrency =
process.platform === 'win32' ||
process.platform === 'darwin' ||
(process.platform === 'linux' && process.arch === 'x64')
? 50
: 10
t('should be able to require in worker thread', async (t) => { t('should be able to require in worker thread', async (t) => {
await Promise.all( await Promise.all(

View file

@ -1,13 +1,13 @@
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_os = "wasi"))]
use futures::prelude::*; use futures::prelude::*;
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use napi::tokio; use napi::tokio;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_os = "wasi"))]
use napi::tokio::fs; use napi::tokio::fs;
#[napi] #[napi]
async fn read_file_async(path: String) -> Result<Buffer> { async fn read_file_async(path: String) -> Result<Buffer> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_os = "wasi"))]
{ {
fs::read(path) fs::read(path)
.map(|r| match r { .map(|r| match r {
@ -19,7 +19,7 @@ async fn read_file_async(path: String) -> Result<Buffer> {
}) })
.await .await
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_os = "wasi")]
{ {
let conetent = std::fs::read(path)?; let conetent = std::fs::read(path)?;
Ok(conetent.into()) Ok(conetent.into())

View file

@ -1,5 +1,3 @@
use napi::bindgen_prelude::*;
/// default enum values are continuos i32s start from 0 /// default enum values are continuos i32s start from 0
#[napi] #[napi]
pub enum Kind { pub enum Kind {