use std::ffi::c_void;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
use std::{
any::TypeId,
collections::HashMap,
num::NonZeroU32,
sync::{LazyLock, Mutex},
};
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
use rustc_hash::FxBuildHasher;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
pub(crate) type ClassId = Option<NonZeroU32>;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
const MAX_CLASS_IDS: usize = 65_536;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[derive(Default)]
struct ClassRegistry {
type_ids: Vec<TypeId>,
indices: HashMap<TypeId, NonZeroU32, FxBuildHasher>,
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
static CLASS_REGISTRY: LazyLock<Mutex<ClassRegistry>> =
LazyLock::new(|| Mutex::new(ClassRegistry::default()));
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn class_registry_full() -> crate::Error {
crate::Error::new(
crate::Status::GenericFailure,
"N-API class identity registry is full",
)
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn class_registry_allocation_failure() -> crate::Error {
crate::Error::from_status(crate::Status::GenericFailure)
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn intern_class_id(
registry: &Mutex<ClassRegistry>,
type_id: TypeId,
capacity: usize,
) -> crate::Result<ClassId> {
let mut registry = registry.lock().unwrap();
if let Some(class_id) = registry.indices.get(&type_id) {
return Ok(Some(*class_id));
}
if registry.type_ids.len() >= capacity {
return Err(class_registry_full());
}
registry
.type_ids
.try_reserve(1)
.map_err(|_| class_registry_allocation_failure())?;
registry
.indices
.try_reserve(1)
.map_err(|_| class_registry_allocation_failure())?;
let class_id =
NonZeroU32::new(u32::try_from(registry.type_ids.len() + 1).map_err(|_| class_registry_full())?)
.ok_or_else(class_registry_full)?;
registry.type_ids.push(type_id);
registry.indices.insert(type_id, class_id);
Ok(Some(class_id))
}
#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[derive(Clone, Copy, Default)]
pub(crate) struct ClassId;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline(always)]
pub(crate) fn class_id_of<T: 'static>() -> crate::Result<ClassId> {
intern_class_id(&CLASS_REGISTRY, TypeId::of::<T>(), MAX_CLASS_IDS)
}
#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[inline(always)]
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) fn class_id_of<T: 'static>() -> crate::Result<ClassId> {
Ok(ClassId)
}
pub(crate) fn wrong_class_error(class_name: &str) -> crate::Error {
crate::Error::new(
crate::Status::InvalidArg,
format!("Value is not an instance of class `{class_name}`"),
)
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline(always)]
fn type_id_of<T: ?Sized>() -> TypeId {
trait NonStaticAny {
fn get_type_id(&self) -> TypeId
where
Self: 'static;
}
impl<T: ?Sized> NonStaticAny for std::marker::PhantomData<T> {
#[inline(always)]
fn get_type_id(&self) -> TypeId
where
Self: 'static,
{
TypeId::of::<T>()
}
}
let phantom_data = std::marker::PhantomData::<T>;
NonStaticAny::get_type_id(unsafe {
std::mem::transmute::<&dyn NonStaticAny, &(dyn NonStaticAny + 'static)>(&phantom_data)
})
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline]
fn check_class_identity_with_registry<T>(
registry: &Mutex<ClassRegistry>,
wrapped: *mut c_void,
class_name: fn() -> &'static str,
) -> crate::Result<()> {
if std::mem::size_of::<T>() == 0 {
return Ok(());
}
let recorded = super::REFERENCE_MAP
.with(|cell| cell.borrow_mut(|map| map.get(&wrapped).map(|entry| entry.class_id)));
let is_t = match recorded {
Some(Some(class_id)) => registry
.lock()
.unwrap()
.type_ids
.get(class_id.get() as usize - 1)
.is_some_and(|registered| *registered == type_id_of::<T>()),
_ => {
let addr = wrapped as usize;
addr >= 4096 && addr.is_multiple_of(std::mem::align_of::<T>())
}
};
if is_t {
Ok(())
} else {
Err(wrong_class_error(class_name()))
}
}
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline]
pub(crate) fn check_class_identity<T>(
wrapped: *mut c_void,
class_name: fn() -> &'static str,
) -> crate::Result<()> {
check_class_identity_with_registry::<T>(&CLASS_REGISTRY, wrapped, class_name)
}
#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[inline(always)]
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) fn check_class_identity<T>(
_wrapped: *mut c_void,
_class_name: fn() -> &'static str,
) -> crate::Result<()> {
Ok(())
}
pub(crate) fn short_type_name<T>() -> &'static str {
let name = std::any::type_name::<T>();
let name = name.split('<').next().unwrap_or(name);
name.rsplit("::").next().unwrap_or(name)
}
#[cfg(all(test, not(all(feature = "napi8", not(target_family = "wasm")))))]
mod tests {
use std::{any::TypeId, ffi::c_void, sync::Mutex};
use super::{
check_class_identity, check_class_identity_with_registry, class_id_of, intern_class_id,
short_type_name, ClassId, ClassRegistry,
};
use crate::bindgen_runtime::{RefEntry, REFERENCE_MAP};
use crate::Status;
struct ClassA(#[allow(dead_code)] u64);
struct ClassB(#[allow(dead_code)] u64);
struct WithLifetime<'a>(#[allow(dead_code)] &'a u64);
struct Empty;
fn record(wrapped: *mut c_void, class_id: ClassId) {
let info = (wrapped, std::ptr::null_mut(), std::ptr::null());
REFERENCE_MAP
.with(|cell| cell.borrow_mut(|map| map.insert(wrapped, RefEntry { info, class_id })));
}
fn forget(wrapped: *mut c_void) {
REFERENCE_MAP.with(|cell| cell.borrow_mut(|map| map.remove(&wrapped)));
}
fn name() -> &'static str {
"ClassB"
}
#[test]
fn recorded_class_must_match() {
let mut value = ClassA(1);
let wrapped: *mut c_void = (&mut value as *mut ClassA).cast();
record(wrapped, class_id_of::<ClassA>().unwrap());
assert!(check_class_identity::<ClassA>(wrapped, name).is_ok());
let err = check_class_identity::<ClassB>(wrapped, name).unwrap_err();
assert_eq!(err.status, Status::InvalidArg);
assert_eq!(err.reason, "Value is not an instance of class `ClassB`");
assert!(check_class_identity::<Empty>(wrapped, name).is_ok());
record(wrapped, class_id_of::<WithLifetime<'static>>().unwrap());
assert!(check_class_identity::<WithLifetime<'_>>(wrapped, name).is_ok());
assert!(check_class_identity::<ClassA>(wrapped, name).is_err());
forget(wrapped);
}
#[test]
fn unrecorded_class_is_accepted_unless_it_cannot_be_a_value() {
let mut value = ClassB(1);
let wrapped: *mut c_void = (&mut value as *mut ClassB).cast();
assert!(check_class_identity::<ClassB>(wrapped, name).is_ok());
record(wrapped, None);
assert!(check_class_identity::<ClassB>(wrapped, name).is_ok());
forget(wrapped);
let dangling = std::ptr::NonNull::<Empty>::dangling().as_ptr().cast();
assert!(check_class_identity::<ClassB>(dangling, name).is_err());
assert!(check_class_identity::<ClassB>(wrapped.wrapping_byte_add(1), name).is_err());
}
#[test]
fn class_ids_are_compact_and_stable() {
assert_eq!(std::mem::size_of::<ClassId>(), 4);
assert_eq!(
class_id_of::<ClassA>().unwrap(),
class_id_of::<ClassA>().unwrap()
);
assert_ne!(
class_id_of::<ClassA>().unwrap(),
class_id_of::<ClassB>().unwrap()
);
}
#[test]
fn capacity_failure_leaves_registry_usable() {
let registry = Mutex::new(ClassRegistry::default());
let mut value = ClassA(1);
let wrapped: *mut c_void = (&mut value as *mut ClassA).cast();
let class_a = intern_class_id(®istry, TypeId::of::<ClassA>(), 1).unwrap();
record(wrapped, class_a);
let err = intern_class_id(®istry, TypeId::of::<ClassB>(), 1).unwrap_err();
assert_eq!(err.status, Status::GenericFailure);
assert_eq!(err.reason, "N-API class identity registry is full");
assert!(registry.lock().is_ok());
assert_eq!(
intern_class_id(®istry, TypeId::of::<ClassA>(), 1).unwrap(),
class_a
);
assert!(check_class_identity_with_registry::<ClassA>(®istry, wrapped, name).is_ok());
assert!(check_class_identity_with_registry::<ClassB>(®istry, wrapped, name).is_err());
forget(wrapped);
}
#[test]
fn short_type_name_drops_the_module_path() {
assert_eq!(short_type_name::<ClassA>(), "ClassA");
assert_eq!(short_type_name::<WithLifetime<'static>>(), "WithLifetime");
}
}