use std::{
any::TypeId,
collections::HashMap,
marker::PhantomData,
ops::{Deref, DerefMut},
os::raw::c_uchar,
panic::AssertUnwindSafe,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex, PoisonError, Weak,
},
};
use crate::{
error::Error,
maybe_send_sync::{MaybeSend, MaybeSync},
};
type CleanupFn = Box<dyn FnMut() + Send>;
type TakenEntry = (usize, Wrapper);
#[derive(Clone, Copy, PartialEq, Debug)]
enum Wrapper {
Boxed,
Arced,
}
struct EntryInner {
real_addr: usize,
type_id: TypeId,
wrapper: Wrapper,
borrow_state: AtomicUsize,
cleanup: Mutex<Option<CleanupFn>>,
owner_pid: u32,
}
#[cfg(not(target_arch = "wasm32"))]
fn current_pid() -> u32 {
std::process::id()
}
#[cfg(target_arch = "wasm32")]
fn current_pid() -> u32 {
0
}
const EXCLUSIVE: usize = usize::MAX;
const WRITER_PENDING: usize = 1 << (usize::BITS - 1);
#[cfg(not(target_arch = "wasm32"))]
const WRITER_WAIT: std::time::Duration = std::time::Duration::from_millis(10);
#[cfg(not(target_arch = "wasm32"))]
const READER_WAIT: std::time::Duration = std::time::Duration::from_millis(1);
impl EntryInner {
fn try_borrow_shared(&self) -> bool {
#[cfg(not(target_arch = "wasm32"))]
let deadline = std::time::Instant::now() + READER_WAIT;
let mut current = self.borrow_state.load(Ordering::Acquire);
loop {
if current == EXCLUSIVE || current & WRITER_PENDING != 0 {
#[cfg(target_arch = "wasm32")]
return false;
#[cfg(not(target_arch = "wasm32"))]
{
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::yield_now();
current = self.borrow_state.load(Ordering::Acquire);
continue;
}
}
match self.borrow_state.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(observed) => current = observed,
}
}
}
fn try_borrow_exclusive(&self) -> bool {
if self
.borrow_state
.compare_exchange(0, EXCLUSIVE, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return true;
}
#[cfg(target_arch = "wasm32")]
{
false
}
#[cfg(not(target_arch = "wasm32"))]
{
self.try_borrow_exclusive_slow()
}
}
#[cfg(not(target_arch = "wasm32"))]
fn try_borrow_exclusive_slow(&self) -> bool {
let deadline = std::time::Instant::now() + WRITER_WAIT;
let mut owns_pending = false;
let mut current = self.borrow_state.load(Ordering::Acquire);
loop {
if owns_pending {
if current == WRITER_PENDING {
match self.borrow_state.compare_exchange_weak(
WRITER_PENDING,
EXCLUSIVE,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(observed) => {
current = observed;
continue;
}
}
}
} else if current == 0 {
match self.borrow_state.compare_exchange_weak(
0,
EXCLUSIVE,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(observed) => {
current = observed;
continue;
}
}
} else if current != EXCLUSIVE && current & WRITER_PENDING == 0 {
match self.borrow_state.compare_exchange_weak(
current,
current | WRITER_PENDING,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
owns_pending = true;
current |= WRITER_PENDING;
continue;
}
Err(observed) => {
current = observed;
continue;
}
}
}
if std::time::Instant::now() >= deadline {
if owns_pending {
self.borrow_state
.fetch_and(!WRITER_PENDING, Ordering::AcqRel);
}
return false;
}
std::thread::yield_now();
current = self.borrow_state.load(Ordering::Acquire);
}
}
fn cancel_cleanup(&self) {
self.cleanup
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
}
fn cancel_cleanup_mut(&mut self) {
self.cleanup
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.take();
}
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>, hide_address: bool) -> std::fmt::Result {
let borrow_state = self.borrow_state.load(Ordering::Relaxed);
let mut out = f.debug_struct("EntryInner");
if hide_address {
out.field("real_addr", &format_args!("<hidden>"));
} else {
out.field("real_addr", &format_args!("0x{:x}", self.real_addr));
}
out.field("type_id", &self.type_id)
.field("wrapper", &self.wrapper)
.field(
"borrow_state",
&match borrow_state {
0 => "free".to_string(),
EXCLUSIVE => "exclusive".to_string(),
n if n & WRITER_PENDING != 0 => {
format!("{} shared (writer pending)", n & !WRITER_PENDING)
}
n => format!("{n} shared"),
},
)
.field(
"cleanup",
&match self.cleanup.try_lock() {
Ok(guard) => {
if guard.is_some() {
"armed"
} else {
"defused"
}
}
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
if poisoned.get_ref().is_some() {
"armed"
} else {
"defused"
}
}
Err(std::sync::TryLockError::WouldBlock) => "locked",
},
)
.field("owner_pid", &self.owner_pid)
.finish()
}
}
impl std::fmt::Debug for EntryInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.debug_fmt(f, !cfg!(debug_assertions))
}
}
impl Drop for EntryInner {
fn drop(&mut self) {
if self.owner_pid != current_pid() {
self.cancel_cleanup_mut();
return;
}
if let Some(mut cleanup) = self
.cleanup
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
if std::panic::catch_unwind(AssertUnwindSafe(&mut cleanup)).is_err() {
eprintln!("cimpl: panic while freeing a tracked pointer, leaking pointer");
}
}
}
}
pub struct SharedCheckout {
entry: Arc<EntryInner>,
}
impl Drop for SharedCheckout {
fn drop(&mut self) {
self.entry.borrow_state.fetch_sub(1, Ordering::AcqRel);
}
}
pub struct ExclusiveCheckout {
entry: Arc<EntryInner>,
}
impl Drop for ExclusiveCheckout {
fn drop(&mut self) {
self.entry.borrow_state.store(0, Ordering::Release);
}
}
pub struct TypedShared<T> {
inner: SharedCheckout,
_marker: PhantomData<*const T>,
}
unsafe impl<T: Sync> Send for TypedShared<T> {}
unsafe impl<T: Sync> Sync for TypedShared<T> {}
pub struct TypedExclusive<T> {
inner: ExclusiveCheckout,
_marker: PhantomData<*const T>,
}
unsafe impl<T: Send> Send for TypedExclusive<T> {}
unsafe impl<T: Sync> Sync for TypedExclusive<T> {}
impl<T> Deref for TypedShared<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*std::ptr::with_exposed_provenance::<T>(self.inner.entry.real_addr) }
}
}
impl<T> Deref for TypedExclusive<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*std::ptr::with_exposed_provenance::<T>(self.inner.entry.real_addr) }
}
}
impl<T> DerefMut for TypedExclusive<T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *std::ptr::with_exposed_provenance_mut::<T>(self.inner.entry.real_addr) }
}
}
#[cfg(target_pointer_width = "64")]
const ID_MULTIPLIER: usize = 0x9e3779b97f4a7c15;
#[cfg(target_pointer_width = "32")]
const ID_MULTIPLIER: usize = 0x9e3779b9;
#[cfg(not(any(target_pointer_width = "64", target_pointer_width = "32")))]
compile_error!("PointerRegistry's handle scrambling needs a 32- or 64-bit usize");
fn scramble_to_odd_id(counter: usize) -> usize {
let odd = counter.wrapping_mul(2).wrapping_add(1);
odd.wrapping_mul(ID_MULTIPLIER)
}
pub(crate) struct PointerRegistry {
tracked: Mutex<HashMap<usize, Arc<EntryInner>>>,
next_id: AtomicUsize,
id_space_exhausted: AtomicBool,
owner_pid: u32,
}
impl PointerRegistry {
fn new() -> Self {
Self {
tracked: Mutex::new(HashMap::new()),
next_id: AtomicUsize::new(0),
id_space_exhausted: AtomicBool::new(false),
owner_pid: current_pid(),
}
}
#[must_use = "On None value: caller still owns pointer (and must free it)"]
fn track_by_id(
&self,
real_addr: usize,
type_id: TypeId,
wrapper: Wrapper,
cleanup: CleanupFn,
) -> Option<usize> {
if real_addr == 0 {
return None;
}
if self.check_same_process().is_err() {
Error::foreign_process().set_last();
return None;
}
let counter = self.next_id.fetch_add(1, Ordering::Relaxed);
if counter >= (usize::MAX >> 1) || self.id_space_exhausted.load(Ordering::Relaxed) {
self.id_space_exhausted.store(true, Ordering::Relaxed);
eprintln!("cimpl: PointerRegistry id space exhausted");
Error::tracking_refused("handle id space exhausted").set_last();
return None;
}
let id = scramble_to_odd_id(counter);
let entry = Arc::new(EntryInner {
real_addr,
type_id,
wrapper,
borrow_state: AtomicUsize::new(0),
cleanup: Mutex::new(Some(cleanup)),
owner_pid: current_pid(),
});
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
if let Some(previous) = tracked.insert(id, entry) {
previous.cancel_cleanup();
eprintln!("cimpl: handle id 0x{id:x} was minted twice, leaking the displaced object");
}
Some(id)
}
fn track_by_address(&self, real_addr: usize, type_id: TypeId, cleanup: CleanupFn) -> bool {
if self.check_same_process().is_err() {
Error::foreign_process().set_last();
return false;
}
if real_addr != 0 {
if !real_addr.is_multiple_of(2) {
eprintln!("cimpl: odd address can not be tracked");
Error::tracking_refused("buffer address could collide with a handle id").set_last();
return false;
}
let entry = Arc::new(EntryInner {
real_addr,
type_id,
wrapper: Wrapper::Boxed,
borrow_state: AtomicUsize::new(0),
cleanup: Mutex::new(Some(cleanup)),
owner_pid: current_pid(),
});
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
if let Some(previous) = tracked.insert(real_addr, entry) {
previous.cancel_cleanup();
eprintln!("cimpl: attempt to retrack an already tracked address");
}
return true;
}
true
}
fn ensure_trackable(&self) -> Result<(), Error> {
self.check_same_process()?;
if self.id_space_exhausted.load(Ordering::Relaxed)
|| self.next_id.load(Ordering::Relaxed) >= (usize::MAX >> 1)
{
return Err(Error::tracking_refused("id space of handles exhausted"));
}
drop(self.tracked.lock().unwrap_or_else(|e| e.into_inner()));
Ok(())
}
fn check_same_process(&self) -> Result<(), Error> {
if self.owner_pid != current_pid() {
return Err(Error::foreign_process());
}
Ok(())
}
fn lookup(&self, id: usize, expected_type: TypeId) -> Result<Arc<EntryInner>, Error> {
self.check_same_process()?;
if id == 0 {
return Err(Error::null_parameter("pointer"));
}
let tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.get(&id) {
Some(entry) if entry.type_id == expected_type => Ok(Arc::clone(entry)),
Some(_) => Err(Error::wrong_pointer_type(id as u64)),
None => Err(Error::untracked_pointer(id as u64)),
}
}
#[must_use = "the borrow ends when the returned guard is dropped"]
fn checkout_shared(&self, id: usize, expected_type: TypeId) -> Result<SharedCheckout, Error> {
let entry = self.lookup(id, expected_type)?;
if entry.try_borrow_shared() {
Ok(SharedCheckout { entry })
} else {
Err(Error::pointer_in_use())
}
}
#[must_use = "the borrow ends when the returned guard is dropped"]
fn checkout_exclusive(
&self,
id: usize,
expected_type: TypeId,
) -> Result<ExclusiveCheckout, Error> {
let entry = self.lookup(id, expected_type)?;
if entry.try_borrow_exclusive() {
Ok(ExclusiveCheckout { entry })
} else {
Err(Error::pointer_in_use())
}
}
#[must_use = "dropping the returned address leaks the allocation"]
fn untrack(
&self,
id: usize,
expected_type: TypeId,
required: Option<Wrapper>,
) -> Result<(usize, Wrapper), Error> {
self.check_same_process()?;
if id == 0 {
return Err(Error::null_parameter("pointer"));
}
let entry = {
let tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.get(&id) {
Some(entry) if entry.type_id == expected_type => {
if required.is_some_and(|required| required != entry.wrapper) {
return Err(Error::wrong_wrapper_kind());
}
Arc::clone(entry)
}
Some(_) => return Err(Error::wrong_pointer_type(id as u64)),
None => return Err(Error::untracked_pointer(id as u64)),
}
};
if !entry.try_borrow_exclusive() {
return Err(Error::pointer_in_use());
}
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.get(&id) {
Some(current) if Arc::ptr_eq(current, &entry) => {}
_ => {
drop(tracked);
entry.borrow_state.store(0, Ordering::Release);
return Err(Error::untracked_pointer(id as u64));
}
}
let removed = tracked.remove(&id).expect("checked Some above");
let real_addr = removed.real_addr;
let wrapper = removed.wrapper;
removed.cancel_cleanup();
drop(tracked);
drop(removed);
drop(entry);
Ok((real_addr, wrapper))
}
#[must_use = "dropping the returned addresses leaks both allocations"]
fn untrack_pair(
&self,
first: usize,
second: usize,
expected_type: TypeId,
required: Option<Wrapper>,
) -> Result<(TakenEntry, TakenEntry), Error> {
self.check_same_process()?;
if first == 0 || second == 0 {
return Err(Error::null_parameter("pointer"));
}
if first == second {
return Err(Error::tracking_refused(
"the same handle was passed for two parameters",
));
}
let (first_entry, second_entry) = {
let tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
for id in [first, second] {
match tracked.get(&id) {
Some(entry) if entry.type_id == expected_type => {
if required.is_some_and(|required| required != entry.wrapper) {
return Err(Error::wrong_wrapper_kind());
}
}
Some(_) => return Err(Error::wrong_pointer_type(id as u64)),
None => return Err(Error::untracked_pointer(id as u64)),
}
}
(
Arc::clone(tracked.get(&first).expect("validated above")),
Arc::clone(tracked.get(&second).expect("validated above")),
)
};
let (lower_entry, higher_entry) = if first < second {
(&first_entry, &second_entry)
} else {
(&second_entry, &first_entry)
};
if !lower_entry.try_borrow_exclusive() {
return Err(Error::pointer_in_use());
}
if !higher_entry.try_borrow_exclusive() {
lower_entry.borrow_state.store(0, Ordering::Release);
return Err(Error::pointer_in_use());
}
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
for (id, entry) in [(first, &first_entry), (second, &second_entry)] {
match tracked.get(&id) {
Some(current) if Arc::ptr_eq(current, entry) => {}
_ => {
drop(tracked);
first_entry.borrow_state.store(0, Ordering::Release);
second_entry.borrow_state.store(0, Ordering::Release);
return Err(Error::untracked_pointer(id as u64));
}
}
}
let first_removed = tracked.remove(&first).expect("checked above");
first_removed.cancel_cleanup();
let second_removed = tracked.remove(&second).expect("checked above");
second_removed.cancel_cleanup();
let taken = (
(first_removed.real_addr, first_removed.wrapper),
(second_removed.real_addr, second_removed.wrapper),
);
drop(tracked);
drop(first_removed);
drop(second_removed);
drop(first_entry);
drop(second_entry);
Ok(taken)
}
pub fn free(&self, key: usize) -> Result<(), Error> {
self.check_same_process()?;
if key == 0 {
return Ok(()); }
let entry = {
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.remove(&key) {
Some(entry) => entry,
None => return Err(Error::untracked_pointer(key as u64)),
}
};
drop(entry);
Ok(())
}
fn free_if_still_tracked_entry(
&self,
key: usize,
expected: &Arc<EntryInner>,
) -> Result<(), Error> {
self.check_same_process()?;
if key == 0 {
return Ok(());
}
let entry = {
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.get(&key) {
Some(current) if Arc::ptr_eq(current, expected) => {
tracked.remove(&key).expect("checked Some above")
}
_ => return Ok(()),
}
};
drop(entry);
Ok(())
}
}
impl Drop for PointerRegistry {
fn drop(&mut self) {
if self.owner_pid != current_pid() {
return;
}
let leaked = {
let mut tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *tracked)
};
if !leaked.is_empty() {
eprintln!(
"\n⚠️ WARNING: {} pointer(s) were not freed at shutdown!",
leaked.len()
);
eprintln!("This indicates C code did not properly free all allocated pointers.");
eprintln!("Each pointer should be freed exactly once with cimpl_free().\n");
}
for (_, entry) in leaked {
entry.cancel_cleanup();
}
}
}
pub(crate) fn get_registry() -> &'static PointerRegistry {
use std::sync::OnceLock;
static REGISTRY: OnceLock<PointerRegistry> = OnceLock::new();
REGISTRY.get_or_init(PointerRegistry::new)
}
pub fn track_box<T: 'static + MaybeSend>(ptr: *mut T) -> *mut T {
let ptr_val = ptr.expose_provenance(); let cleanup = move || unsafe {
drop(Box::from_raw(std::ptr::with_exposed_provenance_mut::<T>(
ptr_val,
)));
};
match get_registry().track_by_id(
ptr_val,
TypeId::of::<T>(),
Wrapper::Boxed,
Box::new(cleanup),
) {
Some(id) => id as *mut T,
None if ptr.is_null() => std::ptr::null_mut(),
None => {
unsafe {
drop(Box::from_raw(std::ptr::with_exposed_provenance_mut::<T>(
ptr_val,
)))
};
std::ptr::null_mut()
}
}
}
pub fn track_arc<T: 'static + MaybeSend>(ptr: *mut T) -> *mut T {
let ptr_val = ptr.expose_provenance(); let cleanup = move || unsafe {
drop(Arc::from_raw(std::ptr::with_exposed_provenance::<T>(
ptr_val,
)));
};
match get_registry().track_by_id(
ptr_val,
TypeId::of::<T>(),
Wrapper::Arced,
Box::new(cleanup),
) {
Some(id) => id as *mut T,
None if ptr.is_null() => std::ptr::null_mut(),
None => {
unsafe {
drop(Arc::from_raw(std::ptr::with_exposed_provenance::<T>(
ptr_val,
)))
};
std::ptr::null_mut()
}
}
}
pub fn track_arc_mutex<T: 'static + MaybeSend>(ptr: *mut Mutex<T>) -> *mut Mutex<T> {
let ptr_val = ptr.expose_provenance(); let cleanup = move || unsafe {
drop(Arc::from_raw(
std::ptr::with_exposed_provenance::<Mutex<T>>(ptr_val),
));
};
match get_registry().track_by_id(
ptr_val,
TypeId::of::<Mutex<T>>(),
Wrapper::Arced,
Box::new(cleanup),
) {
Some(id) => id as *mut Mutex<T>,
None if ptr.is_null() => std::ptr::null_mut(),
None => {
unsafe {
drop(Arc::from_raw(
std::ptr::with_exposed_provenance::<Mutex<T>>(ptr_val),
))
};
std::ptr::null_mut()
}
}
}
#[must_use = "the borrow ends when the returned guard is dropped"]
pub fn checkout_shared<T: 'static + MaybeSync>(ptr: *mut T) -> Result<TypedShared<T>, Error> {
let inner = get_registry().checkout_shared(ptr as usize, TypeId::of::<T>())?;
Ok(TypedShared {
inner,
_marker: PhantomData,
})
}
#[must_use = "the borrow ends when the returned guard is dropped"]
pub fn checkout_exclusive<T: 'static + MaybeSend>(ptr: *mut T) -> Result<TypedExclusive<T>, Error> {
let inner = get_registry().checkout_exclusive(ptr as usize, TypeId::of::<T>())?;
Ok(TypedExclusive {
inner,
_marker: PhantomData,
})
}
pub fn untrack_owned_pair<T: 'static>(first: *mut T, second: *mut T) -> Result<(T, T), Error> {
let ((first_addr, _), (second_addr, _)) = get_registry().untrack_pair(
first as usize,
second as usize,
TypeId::of::<T>(),
Some(Wrapper::Boxed),
)?;
Ok((
*unsafe { Box::from_raw(std::ptr::with_exposed_provenance_mut::<T>(first_addr)) },
*unsafe { Box::from_raw(std::ptr::with_exposed_provenance_mut::<T>(second_addr)) },
))
}
pub fn untrack_owned<T: 'static>(ptr: *mut T) -> Result<T, Error> {
let (real_addr, _) =
get_registry().untrack(ptr as usize, TypeId::of::<T>(), Some(Wrapper::Boxed))?;
Ok(*unsafe { Box::from_raw(std::ptr::with_exposed_provenance_mut::<T>(real_addr)) })
}
pub fn cimpl_free(ptr: *mut std::ffi::c_void) -> i32 {
match get_registry().free(ptr as usize) {
Ok(()) => 0,
Err(error) => {
#[cfg(test)]
{
if ptr as usize != 0 {
eprintln!(
"\n⚠️ ERROR: cimpl_free failed for pointer 0x{:x}: {}\n\
This usually means:\n\
1. The pointer was not allocated with box_tracked!/track_box\n\
2. The pointer was already freed (double-free)\n\
3. The pointer is invalid/corrupted\n",
ptr as usize, error
);
}
}
error.set_last();
-1
}
}
}
pub fn is_safe_buffer_size(size: usize, ptr: *const c_uchar) -> bool {
if size == 0 || size > isize::MAX as usize {
return false;
}
ptr.addr().checked_add(size).is_some()
}
pub unsafe fn safe_slice_from_raw_parts(
ptr: *const c_uchar,
len: usize,
param_name: &str,
) -> Result<&[u8], Error> {
if ptr.is_null() {
return Err(Error::null_parameter(param_name));
}
if !is_safe_buffer_size(len, ptr) {
return Err(Error::invalid_buffer_size(len, param_name));
}
Ok(std::slice::from_raw_parts(ptr, len))
}
const C_BUFFER_ALIGN: usize = 2;
struct CStringBuffer;
struct ByteBuffer;
struct StringArray;
pub fn ensure_trackable() -> Result<(), Error> {
get_registry().ensure_trackable()
}
pub fn track_string_array(
ptrs: Vec<*mut std::os::raw::c_char>,
) -> *const *const std::os::raw::c_char {
if ptrs.is_empty() {
return std::ptr::null();
}
let len = ptrs.len();
let raw = Box::into_raw(ptrs.into_boxed_slice()) as *mut *mut std::os::raw::c_char;
let addr = raw.expose_provenance();
let rebuild = move || unsafe {
Box::from_raw(std::ptr::slice_from_raw_parts_mut(
std::ptr::with_exposed_provenance_mut::<*mut std::os::raw::c_char>(addr),
len,
))
};
let members: Vec<(usize, Weak<EntryInner>)> = {
let strings = rebuild();
let recorded = strings
.iter()
.map(|s| {
let key = s.expose_provenance();
let weak = get_registry()
.tracked
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
.map(Arc::downgrade)
.unwrap_or_default();
(key, weak)
})
.collect();
std::mem::forget(strings);
recorded
};
fn free_members(members: &[(usize, Weak<EntryInner>)]) {
for (key, weak) in members {
if let Some(entry) = weak.upgrade() {
let _ = get_registry().free_if_still_tracked_entry(*key, &entry);
}
}
}
let cleanup_members = members.clone();
let cleanup = move || {
let strings = rebuild();
free_members(&cleanup_members);
drop(strings);
};
if !get_registry().track_by_address(addr, TypeId::of::<StringArray>(), Box::new(cleanup)) {
free_members(&members);
drop(unsafe {
Box::from_raw(std::ptr::slice_from_raw_parts_mut(
std::ptr::with_exposed_provenance_mut::<*mut std::os::raw::c_char>(addr),
len,
))
});
return std::ptr::null();
}
raw as *const *const std::os::raw::c_char
}
fn alloc_even_buffer(bytes: &[u8], nul_terminated: bool) -> *mut u8 {
let total = bytes.len() + usize::from(nul_terminated);
if total == 0 {
return std::ptr::null_mut();
}
let Ok(layout) = std::alloc::Layout::from_size_align(total, C_BUFFER_ALIGN) else {
return std::ptr::null_mut();
};
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() {
return ptr;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
if nul_terminated {
ptr.add(bytes.len()).write(0);
}
}
ptr
}
unsafe fn dealloc_even_buffer(addr: usize, total: usize) {
let layout = std::alloc::Layout::from_size_align(total, C_BUFFER_ALIGN)
.expect("the layout was valid when the buffer was allocated");
std::alloc::dealloc(std::ptr::with_exposed_provenance_mut(addr), layout);
}
pub fn to_c_string(s: String) -> *mut std::os::raw::c_char {
if s.as_bytes().contains(&0) {
Error::other("NUL byte in string").set_last();
return std::ptr::null_mut();
}
let total = s.len() + 1;
let ptr = alloc_even_buffer(s.as_bytes(), true);
if ptr.is_null() {
Error::other("string buffer could not be allocated").set_last();
return std::ptr::null_mut();
}
let ptr_val = ptr.expose_provenance();
let tracked = get_registry().track_by_address(
ptr_val,
TypeId::of::<CStringBuffer>(),
Box::new(move || unsafe { dealloc_even_buffer(ptr_val, total) }),
);
if !tracked {
unsafe { dealloc_even_buffer(ptr_val, total) };
return std::ptr::null_mut();
}
ptr as *mut std::os::raw::c_char
}
pub fn to_c_bytes(bytes: Vec<u8>) -> *const c_uchar {
let len = bytes.len();
if len == 0 {
return std::ptr::null();
}
let ptr = alloc_even_buffer(&bytes, false);
if ptr.is_null() {
Error::other("could not allocate the byte buffer").set_last();
return std::ptr::null();
}
drop(bytes);
let ptr_val = ptr.expose_provenance();
let tracked = get_registry().track_by_address(
ptr_val,
TypeId::of::<ByteBuffer>(),
Box::new(move || unsafe { dealloc_even_buffer(ptr_val, len) }),
);
if !tracked {
unsafe { dealloc_even_buffer(ptr_val, len) };
return std::ptr::null();
}
ptr
}
#[cfg(test)]
mod tests {
use super::*;
impl PointerRegistry {
fn resolve_typed<T: 'static>(&self, ptr: *mut T) -> Result<*mut T, Error> {
Ok(std::ptr::with_exposed_provenance_mut(
self.resolve(ptr as usize, TypeId::of::<T>())?,
))
}
fn untrack_typed<T: 'static>(&self, ptr: *mut T) -> Result<*mut T, Error> {
let (real_addr, _wrapper) = self.untrack(ptr as usize, TypeId::of::<T>(), None)?;
Ok(std::ptr::with_exposed_provenance_mut(real_addr))
}
#[must_use = "the id passed in is not a usable pointer, only the returned address is"]
fn resolve(&self, id: usize, expected_type: TypeId) -> Result<usize, Error> {
self.check_same_process()?;
if id == 0 {
return Err(Error::null_parameter("pointer"));
}
let tracked = self.tracked.lock().unwrap_or_else(|e| e.into_inner());
match tracked.get(&id) {
Some(entry) if entry.type_id == expected_type => Ok(entry.real_addr),
Some(_) => Err(Error::wrong_pointer_type(id as u64)),
None => Err(Error::untracked_pointer(id as u64)),
}
}
}
#[test]
fn test_allocation_tracking_double_free_string() {
use std::ffi::CString;
let test_string = CString::new("test allocation tracking").unwrap();
let c_string = to_c_string(test_string.to_str().unwrap().to_string());
assert!(!c_string.is_null());
let result1 = cimpl_free(c_string as *mut std::ffi::c_void);
assert_eq!(result1, 0);
let result2 = cimpl_free(c_string as *mut std::ffi::c_void);
assert_eq!(result2, -1);
}
#[test]
fn test_to_c_string_basic() {
let rust_string = "Hello, C!".to_string();
let c_string = to_c_string(rust_string);
assert!(!c_string.is_null());
cimpl_free(c_string as *mut std::ffi::c_void);
}
#[test]
fn test_to_c_bytes_basic() {
let bytes = vec![1, 2, 3, 4, 5];
let ptr = to_c_bytes(bytes);
assert!(!ptr.is_null());
cimpl_free(ptr as *mut std::ffi::c_void);
}
#[test]
fn test_to_c_string_with_null_byte() {
let bad_string = "Hello\0World".to_string();
let c_string = to_c_string(bad_string);
assert!(c_string.is_null());
}
#[test]
fn test_untrack_removes_from_registry() {
let ptr = track_box(Box::into_raw(Box::new(42i32)));
assert!(get_registry().resolve_typed::<i32>(ptr).is_ok());
let real_ptr = get_registry().untrack_typed::<i32>(ptr).unwrap();
let result = cimpl_free(ptr as *mut std::ffi::c_void);
assert_eq!(result, -1);
unsafe { drop(Box::from_raw(real_ptr)) };
}
#[test]
fn test_untrack_wrong_type_fails() {
let ptr = track_box(Box::into_raw(Box::new(42i32)));
let result = get_registry().untrack_typed::<u64>(ptr as *mut u64);
assert!(result.is_err());
assert!(get_registry().resolve_typed::<i32>(ptr).is_ok());
cimpl_free(ptr as *mut std::ffi::c_void);
}
#[test]
fn test_untrack_null_pointer_fails() {
let result = get_registry().untrack_typed::<i32>(std::ptr::null_mut());
assert!(result.is_err());
}
#[test]
fn test_untrack_unregistered_pointer_fails() {
let ptr = Box::into_raw(Box::new(42i32));
let result = get_registry().untrack_typed::<i32>(ptr);
assert!(result.is_err());
unsafe { drop(Box::from_raw(ptr)) };
}
#[test]
fn test_untrack_already_untracked_fails() {
let ptr = track_box(Box::into_raw(Box::new(42i32)));
let real_ptr = get_registry().untrack_typed::<i32>(ptr).unwrap();
let result = get_registry().untrack_typed::<i32>(ptr);
assert!(result.is_err());
unsafe { drop(Box::from_raw(real_ptr)) };
}
#[test]
fn test_untrack_refuses_while_a_borrow_is_pending() {
for _ in 0..10 {
let ptr = track_box(Box::into_raw(Box::new(1234i32)));
let guard = checkout_shared::<i32>(ptr).expect("fresh handle");
assert!(untrack_owned::<i32>(ptr).is_err());
drop(guard);
assert_eq!(untrack_owned::<i32>(ptr).expect("borrow released"), 1234);
}
}
#[test]
fn test_untrack_takes_the_borrow_rather_than_observing_it() {
let ptr = track_box(Box::into_raw(Box::new(1234i32)));
let addr = ptr as usize;
let entry = get_registry()
.lookup(addr, TypeId::of::<i32>())
.expect("fresh handle");
assert_eq!(
untrack_owned::<i32>(addr as *mut i32).expect("nothing borrowed yet"),
1234
);
assert_eq!(entry.borrow_state.load(Ordering::Acquire), EXCLUSIVE);
assert!(
entry
.borrow_state
.compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
.is_err(),
"a shared borrow was still available after ownership moved out"
);
}
#[test]
fn test_untrack_releases_the_registry_entry() {
let ptr = track_box(Box::into_raw(Box::new(77i32)));
let weak = get_registry()
.tracked
.lock()
.expect("registry lock")
.get(&(ptr as usize))
.map(Arc::downgrade)
.expect("entry tracked");
assert_eq!(untrack_owned::<i32>(ptr).expect("nothing borrowed yet"), 77);
assert!(weak.upgrade().is_none(), "registry entry still here");
}
#[test]
fn test_untrack_leaves_a_rejected_entry_borrowable() {
let arc = Arc::new(5i32);
let ptr = track_arc(Arc::into_raw(arc) as *mut i32);
assert!(
untrack_owned::<i32>(ptr).is_err(),
"Arc entry must be refused"
);
assert!(checkout_shared::<i32>(ptr).is_ok());
assert_eq!(cimpl_free(ptr as *mut std::ffi::c_void), 0);
}
#[test]
fn test_untrack_refused_while_checked_out() {
let ptr = track_box(Box::into_raw(Box::new(42i32)));
let guard = checkout_shared::<i32>(ptr).unwrap();
assert!(
untrack_owned::<i32>(ptr).is_err(),
"ownership must not move while borrowed"
);
assert!(get_registry().resolve_typed::<i32>(ptr).is_ok());
drop(guard);
assert_eq!(untrack_owned::<i32>(ptr).unwrap(), 42);
}
#[test]
fn test_untrack_owned_rejects_arc_tracked_entry() {
let arc = Arc::new(5i32);
let ptr = track_arc(Arc::into_raw(arc) as *mut i32);
let err = untrack_owned::<i32>(ptr).unwrap_err();
assert!(
err.to_string().starts_with("WrongWrapperKind:"),
"expected WrongWrapperKind, got {err}"
);
assert_eq!(cimpl_free(ptr as *mut std::ffi::c_void), 0);
}
#[test]
fn test_free_while_checked_out_defers_cleanup() {
static DROPPED: AtomicBool = AtomicBool::new(false);
struct Tracked;
impl Drop for Tracked {
fn drop(&mut self) {
DROPPED.store(true, Ordering::SeqCst);
}
}
DROPPED.store(false, Ordering::SeqCst);
let ptr = track_box(Box::into_raw(Box::new(Tracked)));
let guard = checkout_shared::<Tracked>(ptr).unwrap();
assert_eq!(cimpl_free(ptr as *mut std::ffi::c_void), 0);
assert!(!DROPPED.load(Ordering::SeqCst), "freed while borrowed");
drop(guard);
assert!(
DROPPED.load(Ordering::SeqCst),
"not freed after last borrow"
);
}
#[test]
fn test_reentering_cleanup_does_not_deadlock() {
let inner = track_box(Box::into_raw(Box::new(7i32)));
let inner_addr = inner as usize;
struct Reenter(usize);
impl Drop for Reenter {
fn drop(&mut self) {
cimpl_free(self.0 as *mut std::ffi::c_void);
}
}
let outer = track_box(Box::into_raw(Box::new(Reenter(inner_addr))));
assert_eq!(cimpl_free(outer as *mut std::ffi::c_void), 0);
assert!(get_registry().resolve_typed::<i32>(inner).is_err());
}
#[test]
fn test_second_exclusive_checkout_refused_then_released() {
let ptr = track_box(Box::into_raw(Box::new(1i32)));
let first = checkout_exclusive::<i32>(ptr).unwrap();
assert!(
checkout_exclusive::<i32>(ptr).is_err(),
"two exclusive borrows must not coexist"
);
drop(first);
assert!(
checkout_exclusive::<i32>(ptr).is_ok(),
"the borrow must be released on drop"
);
cimpl_free(ptr as *mut std::ffi::c_void);
}
#[test]
fn test_nested_checkout_of_different_ids_succeed() {
let outer = track_box(Box::into_raw(Box::new(1i32)));
let inner = track_box(Box::into_raw(Box::new(2i32)));
let outer_guard = checkout_exclusive::<i32>(outer).unwrap();
let inner_guard = checkout_exclusive::<i32>(inner).unwrap();
assert_eq!(*outer_guard, 1);
assert_eq!(*inner_guard, 2);
drop(inner_guard);
drop(outer_guard);
cimpl_free(outer as *mut std::ffi::c_void);
cimpl_free(inner as *mut std::ffi::c_void);
}
#[test]
#[cfg(not(target_os = "wasi"))] fn test_concurrent_shared_checkouts_succeed() {
let ptr = track_box(Box::into_raw(Box::new(42i32)));
let addr = ptr as usize;
let failures = std::sync::Arc::new(AtomicUsize::new(0));
std::thread::scope(|scope| {
for _ in 0..4 {
let failures = std::sync::Arc::clone(&failures);
scope.spawn(move || {
for _ in 0..5_000 {
match checkout_shared::<i32>(addr as *mut i32) {
Ok(guard) => assert_eq!(*guard, 42),
Err(_) => {
failures.fetch_add(1, Ordering::Relaxed);
}
}
}
});
}
});
assert_eq!(failures.load(Ordering::Relaxed), 0);
cimpl_free(ptr as *mut std::ffi::c_void);
}
#[test]
fn test_guards_are_send() {
fn assert_send<T: Send>() {}
assert_send::<TypedShared<i32>>();
assert_send::<TypedExclusive<i32>>();
}
#[test]
#[cfg(not(target_os = "wasi"))] fn test_guard_cleans_up_once() {
static CLEANUPS: AtomicUsize = AtomicUsize::new(0);
struct Counted;
impl Drop for Counted {
fn drop(&mut self) {
CLEANUPS.fetch_add(1, Ordering::SeqCst);
}
}
CLEANUPS.store(0, Ordering::SeqCst);
let ptr = track_box(Box::into_raw(Box::new(Counted)));
let addr = ptr as usize;
let guard = checkout_shared::<Counted>(ptr).unwrap();
std::thread::spawn(move || {
cimpl_free(addr as *mut std::ffi::c_void);
})
.join()
.unwrap();
std::thread::spawn(move || drop(guard)).join().unwrap();
assert_eq!(CLEANUPS.load(Ordering::SeqCst), 1);
}
#[test]
#[cfg(panic = "unwind")]
fn test_panicking_cleanup_is_handled() {
struct Panics;
impl Drop for Panics {
fn drop(&mut self) {
panic!("cleanup panic");
}
}
let ptr = track_box(Box::into_raw(Box::new(Panics)));
assert_eq!(cimpl_free(ptr as *mut std::ffi::c_void), 0);
let after = track_box(Box::into_raw(Box::new(5i32)));
assert!(get_registry().resolve_typed::<i32>(after).is_ok());
cimpl_free(after as *mut std::ffi::c_void);
}
#[test]
fn test_stale_handle_does_not_alias_new_allocation() {
let ptr1 = track_box(Box::into_raw(Box::new(1i32)));
assert!(get_registry().resolve_typed::<i32>(ptr1).is_ok());
assert_eq!(cimpl_free(ptr1 as *mut std::ffi::c_void), 0);
let ptr2 = track_box(Box::into_raw(Box::new(2i32)));
assert_ne!(ptr1, ptr2);
assert!(get_registry().resolve_typed::<i32>(ptr1).is_err());
assert!(get_registry().resolve_typed::<i32>(ptr2).is_ok());
cimpl_free(ptr2 as *mut std::ffi::c_void);
}
#[test]
fn test_untrack_pair_refuses_the_same_handle_twice() {
let ptr = track_box(Box::into_raw(Box::new(11i32)));
assert!(untrack_owned_pair::<i32>(ptr, ptr).is_err());
assert_eq!(
untrack_owned::<i32>(ptr).expect("still tracked after the refusal"),
11
);
}
#[test]
fn test_untrack_pair_takes_all_or_nothing() {
let first = track_box(Box::into_raw(Box::new(44i32)));
let second = track_box(Box::into_raw(Box::new(55i32)));
let (a, b) = untrack_owned_pair::<i32>(first, second).expect("both tracked");
assert_eq!((a, b), (44, 55), "values came back in argument order");
assert!(untrack_owned::<i32>(first).is_err());
assert!(untrack_owned::<i32>(second).is_err());
}
#[test]
fn test_c_buffers_have_even_addresses() {
for i in 0..8 {
let s = to_c_string(format!("even {i}"));
assert!(!s.is_null(), "tracking must not refuse the address");
assert_eq!(s as usize % 2, 0, "string buffer address must be even");
assert_eq!(cimpl_free(s as *mut std::ffi::c_void), 0);
let b = to_c_bytes(vec![i as u8; 3]);
assert!(!b.is_null(), "tracking must not refuse the address");
assert_eq!(b as usize % 2, 0, "byte buffer address must be even");
assert_eq!(cimpl_free(b as *mut std::ffi::c_void), 0);
}
}
#[test]
#[cfg(not(target_os = "wasi"))] fn test_registry_stays_usable_after_the_lock_is_poisoned() {
use std::sync::atomic::AtomicBool;
let registry = PointerRegistry::new();
std::thread::scope(|scope| {
let poisoner = scope.spawn(|| {
let _guard = registry.tracked.lock().expect("lock not yet poisoned");
panic!("poison the registry lock");
});
assert!(poisoner.join().is_err(), "the poisoning thread must panic");
});
assert!(registry.tracked.is_poisoned());
let freed = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&freed);
let id = registry
.track_by_id(
0x1000,
TypeId::of::<u8>(),
Wrapper::Boxed,
Box::new(move || flag.store(true, Ordering::SeqCst)),
)
.expect("tracking must survive a poisoned lock");
assert!(registry.lookup(id, TypeId::of::<u8>()).is_ok());
assert!(registry.free(id).is_ok());
assert!(
freed.load(Ordering::SeqCst),
"cleanup must run when a handle is freed after poisoning"
);
}
}