use core::ffi::c_void;
use core::sync::atomic::{AtomicUsize, Ordering};
use azul_css::AzString;
use crate::refany::RefAny;
pub const AZ_HOST_HANDLE_RTTI_ID: u64 = 0xA20A_4853_5448_5F44;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct HostHandlePayload {
pub id: u64,
}
#[repr(C)]
#[derive(Debug)]
pub struct InvokerSlot {
fn_ptr: AtomicUsize,
}
impl InvokerSlot {
#[must_use] pub const fn new() -> Self {
Self {
fn_ptr: AtomicUsize::new(0),
}
}
pub fn set(&self, ptr: usize) {
self.fn_ptr.store(ptr, Ordering::SeqCst);
}
pub fn get(&self) -> usize {
self.fn_ptr.load(Ordering::SeqCst)
}
}
impl Default for InvokerSlot {
fn default() -> Self {
Self::new()
}
}
pub static HOST_HANDLE_RELEASER: InvokerSlot = InvokerSlot::new();
pub static GENERIC_INVOKER: InvokerSlot = InvokerSlot::new();
pub type AzGenericInvoker = extern "C" fn(
handle: u64,
kind: *const core::ffi::c_char,
args: *const *const c_void,
n_args: usize,
ret: *mut c_void,
);
#[no_mangle]
pub extern "C" fn AzApp_setGenericInvoker(invoker: AzGenericInvoker) {
GENERIC_INVOKER.set(invoker as usize);
}
#[no_mangle]
pub extern "C" fn AzApp_setHostHandleReleaser(releaser: extern "C" fn(u64)) {
HOST_HANDLE_RELEASER.set(releaser as usize);
}
extern "C" fn host_handle_destructor(ptr: *mut c_void) {
if ptr.is_null() {
return;
}
let payload = unsafe { &*(ptr as *const HostHandlePayload) };
let releaser_addr = HOST_HANDLE_RELEASER.get();
if releaser_addr == 0 {
return;
}
let releaser: extern "C" fn(u64) = unsafe { core::mem::transmute(releaser_addr) };
#[cfg(feature = "std")]
{
drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| releaser(payload.id))));
}
#[cfg(not(feature = "std"))]
{
releaser(payload.id);
}
}
pub fn host_handle_to_refany(id: u64) -> RefAny {
let payload = HostHandlePayload { id };
let type_name: AzString = "AzHostHandle".into();
RefAny::new_c(
&raw const payload as *const c_void,
size_of::<HostHandlePayload>(),
align_of::<HostHandlePayload>(),
AZ_HOST_HANDLE_RTTI_ID,
type_name,
host_handle_destructor,
0,
0,
)
}
#[must_use] pub fn refany_to_host_handle(refany: &RefAny) -> Option<u64> {
if !refany.is_type(AZ_HOST_HANDLE_RTTI_ID) {
return None;
}
let ptr = refany.get_data_ptr() as *const HostHandlePayload;
if ptr.is_null() {
return None;
}
Some(unsafe { (*ptr).id })
}
#[no_mangle]
pub extern "C" fn AzRefAny_newHostHandle(id: u64) -> RefAny {
host_handle_to_refany(id)
}
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn AzRefAny_getHostHandle(refany: *const RefAny) -> u64 {
if refany.is_null() {
return 0;
}
let r = unsafe { &*refany };
refany_to_host_handle(r).unwrap_or(0)
}
#[macro_export]
macro_rules! impl_managed_callback {
(
wrapper: $wrapper:ty,
info_ty: $info_ty:ty,
return_ty: $ret:ty,
default_ret: $default:expr,
invoker_static: $invoker_static:ident,
invoker_ty: $invoker_ty:ident,
thunk_fn: $thunk_fn:ident,
setter_fn: $setter_fn:ident,
from_handle_fn: $from_handle_fn:ident,
) => {
$crate::impl_managed_callback! {
wrapper: $wrapper,
info_ty: $info_ty,
return_ty: $ret,
default_ret: $default,
invoker_static: $invoker_static,
invoker_ty: $invoker_ty,
thunk_fn: $thunk_fn,
setter_fn: $setter_fn,
from_handle_fn: $from_handle_fn,
extra_args: [],
}
};
(
wrapper: $wrapper:ty,
info_ty: $info_ty:ty,
return_ty: $ret:ty,
default_ret: $default:expr,
invoker_static: $invoker_static:ident,
invoker_ty: $invoker_ty:ident,
thunk_fn: $thunk_fn:ident,
setter_fn: $setter_fn:ident,
from_handle_fn: $from_handle_fn:ident,
extra_args: [ $( $extra_name:ident : $extra_ty:ty ),* $(,)? ] $(,)?
) => {
pub static $invoker_static: $crate::host_invoker::InvokerSlot =
$crate::host_invoker::InvokerSlot::new();
pub type $invoker_ty = extern "C" fn(
handle: u64,
data: *const $crate::refany::RefAny,
info: *const $info_ty,
$( $extra_name : *const $extra_ty , )*
out: *mut $ret,
);
#[no_mangle]
pub extern "C" fn $setter_fn(invoker: $invoker_ty) {
$invoker_static.set(invoker as usize);
}
extern "C" fn $thunk_fn(
data: $crate::refany::RefAny,
info: $info_ty,
$( $extra_name : $extra_ty , )*
) -> $ret {
const KIND_STR: &str = concat!(stringify!($wrapper), "\0");
let body = move || -> $ret {
let ctx = info.get_ctx();
let handle = match ctx {
$crate::refany::OptionRefAny::Some(ref refany) => {
match $crate::host_invoker::refany_to_host_handle(refany) {
Some(id) => id,
None => return $default,
}
}
_ => return $default,
};
let invoker_addr = $invoker_static.get();
if invoker_addr == 0 {
let generic_addr = $crate::host_invoker::GENERIC_INVOKER.get();
if generic_addr == 0 {
return $default;
}
let generic: $crate::host_invoker::AzGenericInvoker =
unsafe { core::mem::transmute(generic_addr) };
let args = [
&raw const data as *const core::ffi::c_void,
&raw const info as *const core::ffi::c_void,
$( & $extra_name as *const _ as *const core::ffi::c_void , )*
];
let mut out: $ret = $default;
generic(
handle,
KIND_STR.as_ptr() as *const core::ffi::c_char,
args.as_ptr(),
args.len(),
&raw mut out as *mut core::ffi::c_void,
);
return out;
}
let invoker: $invoker_ty = unsafe { core::mem::transmute(invoker_addr) };
let mut out: $ret = $default;
invoker(
handle,
&raw const data,
&raw const info,
$( & $extra_name as *const $extra_ty , )*
&raw mut out,
);
out
};
#[cfg(feature = "std")]
{
std::panic::catch_unwind(std::panic::AssertUnwindSafe(body))
.unwrap_or($default)
}
#[cfg(not(feature = "std"))]
{
body()
}
}
impl $wrapper {
#[must_use] pub fn create_from_host_handle(handle: u64) -> Self {
Self {
cb: $thunk_fn,
ctx: $crate::refany::OptionRefAny::Some(
$crate::host_invoker::host_handle_to_refany(handle),
),
}
}
}
#[no_mangle]
pub extern "C" fn $from_handle_fn(handle: u64) -> $wrapper {
<$wrapper>::create_from_host_handle(handle)
}
};
}
#[cfg(all(test, feature = "std"))]
#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] mod tests {
use core::sync::atomic::{AtomicU64, Ordering as AtOrdering};
use std::sync::Mutex;
use super::*;
pub(super) static TEST_LOCK: Mutex<()> = Mutex::new(());
static LAST_RELEASED: AtomicU64 = AtomicU64::new(0);
extern "C" fn recording_releaser(id: u64) {
LAST_RELEASED.store(id, AtOrdering::SeqCst);
}
#[test]
fn destructor_transmutes_and_invokes_releaser() {
let _g = TEST_LOCK.lock().unwrap();
LAST_RELEASED.store(0, AtOrdering::SeqCst);
AzApp_setHostHandleReleaser(recording_releaser);
let mut payload = HostHandlePayload { id: 0xABCD_1234 };
host_handle_destructor((&raw mut payload).cast::<c_void>());
assert_eq!(LAST_RELEASED.load(AtOrdering::SeqCst), 0xABCD_1234);
HOST_HANDLE_RELEASER.set(0);
}
#[test]
fn destructor_null_ptr_is_noop() {
host_handle_destructor(core::ptr::null_mut());
}
#[test]
fn host_handle_roundtrips_through_refany() {
let _g = TEST_LOCK.lock().unwrap();
HOST_HANDLE_RELEASER.set(0);
let refany = host_handle_to_refany(0x55);
assert_eq!(refany_to_host_handle(&refany), Some(0x55));
}
#[derive(PartialEq, Debug)]
struct FakeRet(u32);
struct FakeInfo;
impl FakeInfo {
fn get_ctx(&self) -> crate::refany::OptionRefAny {
panic!("boom from get_ctx");
}
}
struct FakeWrapper {
#[allow(dead_code)]
cb: extern "C" fn(crate::refany::RefAny, FakeInfo) -> FakeRet,
#[allow(dead_code)]
ctx: crate::refany::OptionRefAny,
}
crate::impl_managed_callback! {
wrapper: FakeWrapper,
info_ty: FakeInfo,
return_ty: FakeRet,
default_ret: FakeRet(99),
invoker_static: AZ_TEST_FAKE_INVOKER,
invoker_ty: AzTestFakeInvoker,
thunk_fn: az_test_fake_thunk,
setter_fn: az_test_fake_set_invoker,
from_handle_fn: az_test_fake_from_handle,
}
#[test]
fn thunk_contains_panic_and_returns_default() {
let _g = TEST_LOCK.lock().unwrap();
HOST_HANDLE_RELEASER.set(0);
let data = host_handle_to_refany(1);
let out = az_test_fake_thunk(data, FakeInfo);
assert_eq!(out, FakeRet(99));
}
}
#[cfg(all(test, feature = "std"))]
#[allow(
clippy::items_after_statements,
clippy::redundant_clone,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
trivial_casts,
clippy::borrow_as_ptr,
clippy::cast_ptr_alignment,
clippy::unused_self,
unused_qualifications,
unreachable_pub,
private_interfaces,
improper_ctypes_definitions,
missing_debug_implementations,
missing_copy_implementations
)] mod autotest_generated {
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering as AtOrdering};
use std::{ffi::CStr, sync::PoisonError};
use super::{tests::TEST_LOCK, *};
use crate::refany::OptionRefAny;
fn lock_slots() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
}
fn clear_all_slots() {
HOST_HANDLE_RELEASER.set(0);
GENERIC_INVOKER.set(0);
AZ_AUTOTEST_INVOKER.set(0);
}
const BOUNDARY_IDS: [u64; 11] = [
0,
1,
2,
u32::MAX as u64,
u32::MAX as u64 + 1, 0xDEAD_BEEF_CAFE_BABE,
1 << 63,
(1 << 53) + 1, u64::MAX - 1,
u64::MAX,
AZ_HOST_HANDLE_RTTI_ID,
];
#[test]
fn slot_new_and_default_start_unregistered() {
assert_eq!(InvokerSlot::new().get(), 0);
assert_eq!(InvokerSlot::default().get(), 0);
}
#[test]
fn slot_new_is_const_usable_in_static() {
static SLOT: InvokerSlot = InvokerSlot::new();
assert_eq!(SLOT.get(), 0);
SLOT.set(0x1234);
assert_eq!(SLOT.get(), 0x1234);
SLOT.set(0); }
#[test]
fn slot_set_get_roundtrips_at_usize_boundaries() {
let slot = InvokerSlot::new();
for ptr in [
0usize,
1,
2,
usize::MAX,
usize::MAX - 1,
usize::MAX / 2,
1usize << (usize::BITS - 1), usize::try_from(u32::MAX).unwrap(),
0xDEAD_BEEF,
] {
slot.set(ptr);
assert_eq!(slot.get(), ptr, "set/get must round-trip {ptr:#x} exactly");
}
}
#[test]
fn slot_set_is_last_write_wins_and_zero_clears() {
let slot = InvokerSlot::new();
slot.set(usize::MAX);
slot.set(0x42);
assert_eq!(slot.get(), 0x42);
slot.set(0);
assert_eq!(slot.get(), 0);
}
#[test]
fn slot_get_is_idempotent() {
let slot = InvokerSlot::new();
slot.set(0xABCD);
assert_eq!(slot.get(), 0xABCD);
assert_eq!(slot.get(), 0xABCD);
assert_eq!(slot.get(), 0xABCD);
}
#[test]
fn slot_concurrent_writes_never_tear() {
let slot = InvokerSlot::new();
let written: [usize; 4] = [0, 1, usize::MAX, 1usize << (usize::BITS - 1)];
let slot_ref = &slot;
std::thread::scope(|s| {
for &w in &written {
s.spawn(move || {
for _ in 0..200 {
slot_ref.set(w);
let seen = slot_ref.get();
assert!(
written.contains(&seen),
"torn/garbage value observed in slot: {seen:#x}"
);
}
});
}
});
assert!(written.contains(&slot.get()));
}
#[test]
fn rtti_id_matches_documented_constant() {
assert_eq!(AZ_HOST_HANDLE_RTTI_ID, 0xA20A_4853_5448_5F44);
assert_ne!(AZ_HOST_HANDLE_RTTI_ID, 0);
}
#[test]
fn host_handle_payload_layout_is_a_bare_u64() {
assert_eq!(size_of::<HostHandlePayload>(), size_of::<u64>());
assert_eq!(align_of::<HostHandlePayload>(), align_of::<u64>());
}
#[test]
fn host_handle_roundtrips_at_every_u64_boundary() {
let _g = lock_slots();
clear_all_slots(); for id in BOUNDARY_IDS {
let refany = host_handle_to_refany(id);
assert_eq!(
refany_to_host_handle(&refany),
Some(id),
"encode/decode must be lossless for id {id:#x}"
);
}
}
#[test]
fn host_handle_refany_carries_the_expected_rtti_metadata() {
let _g = lock_slots();
clear_all_slots();
let refany = host_handle_to_refany(9);
assert!(refany.is_type(AZ_HOST_HANDLE_RTTI_ID));
assert_eq!(refany.get_type_id(), AZ_HOST_HANDLE_RTTI_ID);
assert_eq!(refany.get_type_name().as_str(), "AzHostHandle");
assert_eq!(refany.get_data_len(), size_of::<HostHandlePayload>());
assert_eq!(refany.get_ref_count(), 1);
assert!(!refany.get_data_ptr().is_null());
}
#[test]
fn host_handle_id_survives_cloning() {
let _g = lock_slots();
clear_all_slots();
let refany = host_handle_to_refany(u64::MAX);
let clone = refany.clone();
assert_eq!(refany.get_ref_count(), 2);
assert_eq!(refany_to_host_handle(&clone), Some(u64::MAX));
assert_eq!(refany_to_host_handle(&refany), Some(u64::MAX));
}
#[test]
fn refany_to_host_handle_rejects_foreign_refanys() {
assert_eq!(refany_to_host_handle(&RefAny::new(0u64)), None);
assert_eq!(refany_to_host_handle(&RefAny::new(u64::MAX)), None);
assert_eq!(refany_to_host_handle(&RefAny::new(())), None);
assert_eq!(refany_to_host_handle(&RefAny::new([0xFFu8; 64])), None);
let same_shape = RefAny::new(HostHandlePayload { id: 0x1111 });
assert_ne!(same_shape.get_type_id(), AZ_HOST_HANDLE_RTTI_ID);
assert_eq!(refany_to_host_handle(&same_shape), None);
}
extern "C" fn noop_destructor(_ptr: *mut c_void) {}
#[test]
fn refany_to_host_handle_trusts_the_rtti_id_alone() {
let _g = lock_slots();
clear_all_slots();
let payload = HostHandlePayload {
id: 0x1234_5678_9ABC_DEF0,
};
let spoofed = RefAny::new_c(
(&raw const payload).cast::<c_void>(),
size_of::<HostHandlePayload>(),
align_of::<HostHandlePayload>(),
AZ_HOST_HANDLE_RTTI_ID,
"NotAHostHandle".into(),
noop_destructor,
0,
0,
);
assert_eq!(refany_to_host_handle(&spoofed), Some(0x1234_5678_9ABC_DEF0));
}
#[test]
fn c_abi_new_and_get_host_handle_roundtrip() {
let _g = lock_slots();
clear_all_slots();
for id in BOUNDARY_IDS {
let refany = AzRefAny_newHostHandle(id);
assert_eq!(refany_to_host_handle(&refany), Some(id));
assert_eq!(AzRefAny_getHostHandle(&raw const refany), id);
}
}
#[test]
fn c_abi_get_host_handle_null_returns_zero() {
assert_eq!(AzRefAny_getHostHandle(core::ptr::null()), 0);
}
#[test]
fn c_abi_get_host_handle_foreign_refany_returns_zero() {
let foreign = RefAny::new(0xDEAD_BEEF_u64);
assert_eq!(AzRefAny_getHostHandle(&raw const foreign), 0);
}
#[test]
fn c_abi_get_host_handle_cannot_distinguish_id_zero_from_failure() {
let _g = lock_slots();
clear_all_slots();
let zero_handle = AzRefAny_newHostHandle(0);
assert_eq!(AzRefAny_getHostHandle(&raw const zero_handle), 0);
assert_eq!(AzRefAny_getHostHandle(core::ptr::null()), 0);
assert_eq!(refany_to_host_handle(&zero_handle), Some(0));
}
static RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0);
static RELEASED_ID: AtomicU64 = AtomicU64::new(0);
extern "C" fn counting_releaser(id: u64) {
RELEASED_ID.store(id, AtOrdering::SeqCst);
RELEASE_COUNT.fetch_add(1, AtOrdering::SeqCst);
}
static OTHER_RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0);
extern "C" fn other_releaser(_id: u64) {
OTHER_RELEASE_COUNT.fetch_add(1, AtOrdering::SeqCst);
}
fn reset_release_recorder() {
RELEASE_COUNT.store(0, AtOrdering::SeqCst);
RELEASED_ID.store(0, AtOrdering::SeqCst);
OTHER_RELEASE_COUNT.store(0, AtOrdering::SeqCst);
}
#[test]
fn set_releaser_stores_the_fn_address_and_replaces_it() {
let _g = lock_slots();
clear_all_slots();
let expected: extern "C" fn(u64) = counting_releaser;
AzApp_setHostHandleReleaser(counting_releaser);
assert_eq!(HOST_HANDLE_RELEASER.get(), expected as usize);
assert_ne!(HOST_HANDLE_RELEASER.get(), 0);
let replacement: extern "C" fn(u64) = other_releaser;
AzApp_setHostHandleReleaser(other_releaser);
assert_eq!(HOST_HANDLE_RELEASER.get(), replacement as usize);
clear_all_slots();
}
#[test]
fn releaser_fires_exactly_once_on_the_last_drop() {
let _g = lock_slots();
clear_all_slots();
reset_release_recorder();
AzApp_setHostHandleReleaser(counting_releaser);
let refany = host_handle_to_refany(0xABC_DEF);
let clone_a = refany.clone();
let clone_b = refany.clone();
drop(clone_a);
drop(clone_b);
assert_eq!(RELEASE_COUNT.load(AtOrdering::SeqCst), 0);
drop(refany);
assert_eq!(RELEASE_COUNT.load(AtOrdering::SeqCst), 1);
assert_eq!(RELEASED_ID.load(AtOrdering::SeqCst), 0xABC_DEF);
clear_all_slots();
}
#[test]
fn releaser_receives_boundary_ids_verbatim() {
let _g = lock_slots();
clear_all_slots();
reset_release_recorder();
AzApp_setHostHandleReleaser(counting_releaser);
for (n, id) in BOUNDARY_IDS.into_iter().enumerate() {
RELEASED_ID.store(0, AtOrdering::SeqCst);
drop(host_handle_to_refany(id));
assert_eq!(
RELEASE_COUNT.load(AtOrdering::SeqCst),
n + 1,
"one release per dropped handle"
);
assert_eq!(
RELEASED_ID.load(AtOrdering::SeqCst),
id,
"releaser must see id {id:#x} unmangled (no truncation/saturation)"
);
}
clear_all_slots();
}
#[test]
fn dropping_a_handle_with_no_releaser_registered_is_a_noop() {
let _g = lock_slots();
clear_all_slots();
reset_release_recorder();
drop(host_handle_to_refany(1));
drop(host_handle_to_refany(u64::MAX));
assert_eq!(RELEASE_COUNT.load(AtOrdering::SeqCst), 0);
assert_eq!(HOST_HANDLE_RELEASER.get(), 0);
}
#[test]
fn re_registering_the_releaser_retires_the_old_one() {
let _g = lock_slots();
clear_all_slots();
reset_release_recorder();
AzApp_setHostHandleReleaser(counting_releaser);
let live = host_handle_to_refany(7);
AzApp_setHostHandleReleaser(other_releaser);
drop(live);
assert_eq!(RELEASE_COUNT.load(AtOrdering::SeqCst), 0);
assert_eq!(OTHER_RELEASE_COUNT.load(AtOrdering::SeqCst), 1);
clear_all_slots();
}
#[test]
fn destructor_on_null_payload_is_a_noop_even_with_a_releaser() {
let _g = lock_slots();
clear_all_slots();
reset_release_recorder();
AzApp_setHostHandleReleaser(counting_releaser);
host_handle_destructor(core::ptr::null_mut());
assert_eq!(
RELEASE_COUNT.load(AtOrdering::SeqCst),
0,
"a null payload must not be deref'd, nor reported as id 0"
);
clear_all_slots();
}
#[repr(C)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
struct AutoRet(u32);
const DEFAULT_RET: AutoRet = AutoRet(0xDEAD);
#[repr(C)]
#[derive(Debug)]
struct AutoInfo {
ctx: OptionRefAny,
}
impl AutoInfo {
fn get_ctx(&self) -> OptionRefAny {
self.ctx.clone()
}
}
#[repr(C)]
#[derive(Debug)]
struct AutoWrapper {
cb: extern "C" fn(RefAny, AutoInfo) -> AutoRet,
ctx: OptionRefAny,
}
crate::impl_managed_callback! {
wrapper: AutoWrapper,
info_ty: AutoInfo,
return_ty: AutoRet,
default_ret: DEFAULT_RET,
invoker_static: AZ_AUTOTEST_INVOKER,
invoker_ty: AzAutotestInvoker,
thunk_fn: az_autotest_thunk,
setter_fn: az_autotest_set_invoker,
from_handle_fn: az_autotest_from_handle,
}
static GENERIC_CALLS: AtomicUsize = AtomicUsize::new(0);
static GENERIC_HANDLE: AtomicU64 = AtomicU64::new(0);
static GENERIC_NARGS: AtomicUsize = AtomicUsize::new(0);
static GENERIC_KIND_OK: AtomicBool = AtomicBool::new(false);
static GENERIC_ARG0_ID: AtomicU64 = AtomicU64::new(0);
static PERKIND_CALLS: AtomicUsize = AtomicUsize::new(0);
static PERKIND_HANDLE: AtomicU64 = AtomicU64::new(0);
fn reset_invoker_recorders() {
GENERIC_CALLS.store(0, AtOrdering::SeqCst);
GENERIC_HANDLE.store(0, AtOrdering::SeqCst);
GENERIC_NARGS.store(0, AtOrdering::SeqCst);
GENERIC_KIND_OK.store(false, AtOrdering::SeqCst);
GENERIC_ARG0_ID.store(0, AtOrdering::SeqCst);
PERKIND_CALLS.store(0, AtOrdering::SeqCst);
PERKIND_HANDLE.store(0, AtOrdering::SeqCst);
}
extern "C" fn recording_generic(
handle: u64,
kind: *const core::ffi::c_char,
args: *const *const c_void,
n_args: usize,
ret: *mut c_void,
) {
GENERIC_CALLS.fetch_add(1, AtOrdering::SeqCst);
GENERIC_HANDLE.store(handle, AtOrdering::SeqCst);
GENERIC_NARGS.store(n_args, AtOrdering::SeqCst);
let kind_ok = !kind.is_null()
&& unsafe { CStr::from_ptr(kind) }.to_str() == Ok("AutoWrapper");
GENERIC_KIND_OK.store(kind_ok, AtOrdering::SeqCst);
if !args.is_null() && n_args == 2 {
let arg0 = unsafe { *args };
if !arg0.is_null() {
let data = unsafe { &*(arg0.cast::<RefAny>()) };
GENERIC_ARG0_ID.store(
refany_to_host_handle(data).unwrap_or(0),
AtOrdering::SeqCst,
);
}
}
if kind_ok && !ret.is_null() {
unsafe { ret.cast::<AutoRet>().write(AutoRet(0x2222)) };
}
}
extern "C" fn recording_perkind(
handle: u64,
_data: *const RefAny,
_info: *const AutoInfo,
out: *mut AutoRet,
) {
PERKIND_CALLS.fetch_add(1, AtOrdering::SeqCst);
PERKIND_HANDLE.store(handle, AtOrdering::SeqCst);
if !out.is_null() {
unsafe { out.write(AutoRet(0x1111)) };
}
}
extern "C" fn silent_perkind(
_handle: u64,
_data: *const RefAny,
_info: *const AutoInfo,
_out: *mut AutoRet,
) {
PERKIND_CALLS.fetch_add(1, AtOrdering::SeqCst);
}
fn info_with_ctx(ctx: OptionRefAny) -> AutoInfo {
AutoInfo { ctx }
}
#[test]
fn set_generic_invoker_stores_the_fn_address_and_replaces_it() {
let _g = lock_slots();
clear_all_slots();
let expected: AzGenericInvoker = recording_generic;
AzApp_setGenericInvoker(recording_generic);
assert_eq!(GENERIC_INVOKER.get(), expected as usize);
assert_ne!(GENERIC_INVOKER.get(), 0);
AzApp_setGenericInvoker(recording_generic); assert_eq!(GENERIC_INVOKER.get(), expected as usize);
clear_all_slots();
}
#[test]
fn create_from_host_handle_wires_the_thunk_and_ctx() {
let _g = lock_slots();
clear_all_slots();
for id in BOUNDARY_IDS {
let wrapper = AutoWrapper::create_from_host_handle(id);
let expected: extern "C" fn(RefAny, AutoInfo) -> AutoRet = az_autotest_thunk;
assert_eq!(wrapper.cb as usize, expected as usize);
match &wrapper.ctx {
OptionRefAny::Some(refany) => {
assert_eq!(refany_to_host_handle(refany), Some(id));
}
OptionRefAny::None => panic!("ctx must carry the host handle for id {id:#x}"),
}
let from_c = az_autotest_from_handle(id);
assert_eq!(from_c.cb as usize, expected as usize);
}
}
#[test]
fn thunk_returns_default_when_ctx_is_none() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
AzApp_setGenericInvoker(recording_generic);
az_autotest_set_invoker(recording_perkind);
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(OptionRefAny::None));
assert_eq!(out, DEFAULT_RET);
assert_eq!(GENERIC_CALLS.load(AtOrdering::SeqCst), 0);
assert_eq!(PERKIND_CALLS.load(AtOrdering::SeqCst), 0);
clear_all_slots();
}
#[test]
fn thunk_returns_default_when_ctx_is_not_a_host_handle() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
AzApp_setGenericInvoker(recording_generic);
az_autotest_set_invoker(recording_perkind);
let ctx = OptionRefAny::Some(RefAny::new(0xFFFF_FFFF_FFFF_FFFF_u64));
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(ctx));
assert_eq!(out, DEFAULT_RET);
assert_eq!(GENERIC_CALLS.load(AtOrdering::SeqCst), 0);
assert_eq!(PERKIND_CALLS.load(AtOrdering::SeqCst), 0);
clear_all_slots();
}
#[test]
fn thunk_returns_default_when_nothing_is_registered() {
let _g = lock_slots();
clear_all_slots();
let ctx = OptionRefAny::Some(host_handle_to_refany(5));
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(ctx));
assert_eq!(out, DEFAULT_RET);
}
#[test]
fn thunk_falls_back_to_the_generic_invoker() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
AzApp_setGenericInvoker(recording_generic);
let data = host_handle_to_refany(0xDA7A);
let ctx = OptionRefAny::Some(host_handle_to_refany(0xC7_C7_C7));
let out = az_autotest_thunk(data, info_with_ctx(ctx));
assert_eq!(GENERIC_CALLS.load(AtOrdering::SeqCst), 1);
assert_eq!(GENERIC_HANDLE.load(AtOrdering::SeqCst), 0xC7_C7_C7);
assert!(GENERIC_KIND_OK.load(AtOrdering::SeqCst), "kind must be \"AutoWrapper\\0\"");
assert_eq!(GENERIC_NARGS.load(AtOrdering::SeqCst), 2, "data + info");
assert_eq!(GENERIC_ARG0_ID.load(AtOrdering::SeqCst), 0xDA7A);
assert_eq!(out, AutoRet(0x2222));
clear_all_slots();
}
#[test]
fn thunk_prefers_the_per_kind_invoker_over_the_generic_one() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
AzApp_setGenericInvoker(recording_generic);
az_autotest_set_invoker(recording_perkind);
let ctx = OptionRefAny::Some(host_handle_to_refany(0x99));
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(ctx));
assert_eq!(out, AutoRet(0x1111));
assert_eq!(PERKIND_CALLS.load(AtOrdering::SeqCst), 1);
assert_eq!(PERKIND_HANDLE.load(AtOrdering::SeqCst), 0x99);
assert_eq!(
GENERIC_CALLS.load(AtOrdering::SeqCst),
0,
"generic is a fallback only — it must not also fire"
);
clear_all_slots();
}
#[test]
fn thunk_returns_default_when_the_host_ignores_the_out_pointer() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
az_autotest_set_invoker(silent_perkind);
let ctx = OptionRefAny::Some(host_handle_to_refany(3));
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(ctx));
assert_eq!(PERKIND_CALLS.load(AtOrdering::SeqCst), 1);
assert_eq!(out, DEFAULT_RET);
clear_all_slots();
}
#[test]
fn thunk_dispatches_boundary_handles_without_truncation() {
let _g = lock_slots();
clear_all_slots();
reset_invoker_recorders();
az_autotest_set_invoker(recording_perkind);
for id in BOUNDARY_IDS {
let ctx = OptionRefAny::Some(host_handle_to_refany(id));
let out = az_autotest_thunk(RefAny::new(1u32), info_with_ctx(ctx));
assert_eq!(out, AutoRet(0x1111));
assert_eq!(
PERKIND_HANDLE.load(AtOrdering::SeqCst),
id,
"handle {id:#x} must reach the host invoker unmangled"
);
}
clear_all_slots();
}
}