use alloc::borrow::Cow;
use alloc::ffi::CString;
use alloc::format;
use core::ffi::CStr;
use core::mem;
use core::ptr::{self, NonNull};
use crate::encode::{Encode, Encoding};
use crate::runtime::{AnyClass, AnyObject, ClassBuilder, MessageReceiver, Sel};
use crate::{sel, ClassType, DefinedClass};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum DropFlag {
#[allow(dead_code)]
Allocated = 0x00,
InitializedIvars = 0x0f,
Finalized = 0xff,
}
unsafe impl Encode for DropFlag {
const ENCODING: Encoding = u8::ENCODING;
}
pub trait DefinedIvarsHelper {
const HAS_IVARS: bool;
const HAS_DROP_FLAG: bool;
}
impl<T: DefinedClass> DefinedIvarsHelper for T {
const HAS_IVARS: bool = {
mem::size_of::<T::Ivars>() > 0
|| mem::align_of::<T::Ivars>() > mem::align_of::<*mut AnyObject>()
};
const HAS_DROP_FLAG: bool = mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>();
}
#[inline]
unsafe fn ptr_to_ivar<T: ?Sized + DefinedClass>(ptr: NonNull<T>) -> NonNull<T::Ivars> {
unsafe { AnyObject::ivar_at_offset::<T::Ivars>(ptr.cast(), T::__ivars_offset()) }
}
#[inline]
unsafe fn ptr_to_drop_flag<T: DefinedClass>(ptr: NonNull<T>) -> *mut DropFlag {
debug_assert!(T::HAS_DROP_FLAG, "type did not have drop flag");
unsafe { AnyObject::ivar_at_offset::<DropFlag>(ptr.cast(), T::__drop_flag_offset()).as_ptr() }
}
pub(crate) fn setup_dealloc<T: DefinedClass>(builder: &mut ClassBuilder)
where
T::Super: ClassType,
{
if mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>() {
let func: unsafe extern "C-unwind" fn(_, _) = dealloc::<T>;
unsafe { builder.add_method(sel!(dealloc), func) };
} else {
}
}
unsafe extern "C-unwind" fn dealloc<T: DefinedClass>(this: NonNull<T>, cmd: Sel)
where
T::Super: ClassType,
{
#[inline]
#[cold]
fn cold_path() {}
let drop_flag = unsafe { *ptr_to_drop_flag(this) };
if mem::needs_drop::<T>() {
match drop_flag {
DropFlag::Allocated | DropFlag::InitializedIvars => cold_path(),
DropFlag::Finalized => unsafe { ptr::drop_in_place(this.as_ptr()) },
}
}
if mem::needs_drop::<T::Ivars>() {
match drop_flag {
DropFlag::Allocated => cold_path(),
DropFlag::InitializedIvars | DropFlag::Finalized => {
unsafe { ptr::drop_in_place(ptr_to_ivar(this).as_ptr()) };
}
}
}
unsafe {
MessageReceiver::send_super_message(
this,
<T as ClassType>::Super::class(),
cmd, (), )
}
}
#[inline]
pub(crate) fn ivar_drop_flag_names<T: DefinedClass>() -> (Cow<'static, CStr>, Cow<'static, CStr>) {
if cfg!(feature = "gnustep-1-7") {
(
CString::new(format!("{}_ivars", T::NAME)).unwrap().into(),
CString::new(format!("{}_drop_flag", T::NAME))
.unwrap()
.into(),
)
} else {
unsafe {
(
CStr::from_bytes_with_nul_unchecked(b"ivars\0").into(),
CStr::from_bytes_with_nul_unchecked(b"drop_flag\0").into(),
)
}
}
}
#[inline]
pub(crate) fn register_ivars<T: DefinedClass>(builder: &mut ClassBuilder, ivars_name: &CStr) {
if T::HAS_IVARS {
let ivar_encoding = Encoding::Array(
mem::size_of::<T::Ivars>() as u64,
match mem::align_of::<T::Ivars>() {
1 => &u8::ENCODING,
2 => &u16::ENCODING,
4 => &u32::ENCODING,
8 if mem::align_of::<u64>() == 8 => &u64::ENCODING,
alignment => panic!("unsupported alignment {alignment} for `{}::Ivars`", T::NAME),
},
);
unsafe { builder.add_ivar_inner::<T::Ivars>(ivars_name, &ivar_encoding) };
}
}
#[inline]
pub(crate) fn register_drop_flag<T: DefinedClass>(
builder: &mut ClassBuilder,
drop_flag_name: &CStr,
) {
if T::HAS_DROP_FLAG {
builder.add_ivar::<DropFlag>(drop_flag_name);
}
}
#[inline]
pub(crate) fn ivars_offset<T: DefinedClass>(cls: &AnyClass, ivars_name: &CStr) -> isize {
if T::HAS_IVARS {
fn get_ivar_failed() -> ! {
unreachable!("failed retrieving instance variable on newly defined class")
}
cls.instance_variable(ivars_name)
.unwrap_or_else(|| get_ivar_failed())
.offset()
} else {
0
}
}
#[inline]
pub(crate) fn drop_flag_offset<T: DefinedClass>(cls: &AnyClass, drop_flag_name: &CStr) -> isize {
if T::HAS_DROP_FLAG {
fn get_drop_flag_failed() -> ! {
unreachable!("failed retrieving drop flag instance variable on newly defined class")
}
cls.instance_variable(drop_flag_name)
.unwrap_or_else(|| get_drop_flag_failed())
.offset()
} else {
0
}
}
#[inline]
#[track_caller]
pub(crate) unsafe fn initialize_ivars<T: DefinedClass>(ptr: NonNull<T>, val: T::Ivars) {
if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
match unsafe { *ptr_to_drop_flag(ptr) } {
DropFlag::Allocated => {
}
DropFlag::InitializedIvars => {
panic!("tried to initialize ivars after they were already initialized")
}
DropFlag::Finalized => {
panic!("tried to initialize ivars on an already initialized object")
}
}
}
unsafe { ptr_to_ivar(ptr).as_ptr().write(val) };
if T::HAS_DROP_FLAG && (mem::needs_drop::<T::Ivars>() || cfg!(debug_assertions)) {
unsafe { ptr_to_drop_flag(ptr).write(DropFlag::InitializedIvars) }
}
}
#[inline]
#[track_caller]
pub(crate) unsafe fn set_finalized<T: DefinedClass>(ptr: NonNull<T>) {
if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
match unsafe { *ptr_to_drop_flag(ptr) } {
DropFlag::Allocated => {
panic!("tried to finalize an object that was not yet fully initialized")
}
DropFlag::InitializedIvars => {
}
DropFlag::Finalized => {
panic!("tried to finalize an already finalized object")
}
}
}
if T::HAS_DROP_FLAG && (mem::needs_drop::<T>() || cfg!(debug_assertions)) {
unsafe { ptr_to_drop_flag(ptr).write(DropFlag::Finalized) }
}
}
#[inline]
#[track_caller]
pub(crate) unsafe fn get_initialized_ivar_ptr<T: DefinedClass>(
ptr: NonNull<T>,
) -> NonNull<T::Ivars> {
if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
match unsafe { *ptr_to_drop_flag(ptr) } {
DropFlag::Allocated => {
panic!("tried to access uninitialized instance variable")
}
DropFlag::InitializedIvars => {
}
DropFlag::Finalized => {
}
}
}
unsafe { ptr_to_ivar(ptr) }
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use core::cell::Cell;
use std::sync::OnceLock;
use super::*;
use crate::rc::{Allocated, PartialInit, RcTestObject, Retained, ThreadTestData};
use crate::runtime::NSObject;
use crate::{define_class, msg_send, AnyThread, Message};
unsafe fn init_only_superclasses<T: DefinedClass>(obj: Allocated<T>) -> Retained<T>
where
T::Super: ClassType,
{
unsafe { Retained::from_raw(msg_send![super(Allocated::into_ptr(obj)), init]) }.unwrap()
}
unsafe fn init_no_finalize<T: DefinedClass>(obj: Allocated<T>) -> Retained<T>
where
T::Super: ClassType,
T::Ivars: Default,
{
let obj = obj.set_ivars(Default::default());
unsafe { Retained::from_raw(msg_send![super(PartialInit::into_ptr(obj)), init]) }.unwrap()
}
unsafe fn init<T: DefinedClass>(obj: Allocated<T>) -> Retained<T> {
unsafe { msg_send![obj, init] }
}
#[test]
fn assert_size() {
assert_eq!(mem::size_of::<DropFlag>(), 1);
}
#[test]
#[cfg(feature = "std")]
fn test_dealloc_and_dealloc_subclasses() {
use std::sync::Mutex;
#[derive(Debug, PartialEq)]
enum Operation {
DropIvar,
DropClass,
}
static OPERATIONS: Mutex<Vec<Operation>> = Mutex::new(Vec::new());
#[derive(Default)]
struct IvarThatImplsDrop;
impl Drop for IvarThatImplsDrop {
fn drop(&mut self) {
OPERATIONS.lock().unwrap().push(Operation::DropIvar);
}
}
#[track_caller]
fn check<const N: usize>(expected: [Operation; N]) {
let mut operations = OPERATIONS.lock().unwrap();
assert_eq!(&**operations, expected);
operations.clear();
}
define_class!(
#[unsafe(super(NSObject))]
#[ivars = ()]
struct ImplsDrop;
impl ImplsDrop {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send![super(this.set_ivars(())), init] }
}
}
);
impl Drop for ImplsDrop {
fn drop(&mut self) {
OPERATIONS.lock().unwrap().push(Operation::DropClass);
}
}
let _ = ImplsDrop::alloc();
check([]);
let _ = unsafe { init_only_superclasses(ImplsDrop::alloc()) };
check([]);
let _ = unsafe { init_no_finalize(ImplsDrop::alloc()) };
check([]);
let _ = unsafe { init(ImplsDrop::alloc()) };
check([Operation::DropClass]);
define_class!(
#[unsafe(super(ImplsDrop))]
#[ivars = IvarThatImplsDrop]
struct IvarsImplDrop;
impl IvarsImplDrop {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send![super(this.set_ivars(IvarThatImplsDrop)), init] }
}
}
);
let _ = IvarsImplDrop::alloc();
check([]);
let _ = unsafe { init_only_superclasses(IvarsImplDrop::alloc()) };
check([Operation::DropClass]);
let _ = unsafe { init_no_finalize(IvarsImplDrop::alloc()) };
check([Operation::DropIvar, Operation::DropClass]);
let _ = unsafe { init(IvarsImplDrop::alloc()) };
check([Operation::DropIvar, Operation::DropClass]);
define_class!(
#[unsafe(super(IvarsImplDrop))]
#[ivars = IvarThatImplsDrop]
struct BothIvarsAndTypeImplsDrop;
impl BothIvarsAndTypeImplsDrop {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send![super(this.set_ivars(IvarThatImplsDrop)), init] }
}
}
);
impl Drop for BothIvarsAndTypeImplsDrop {
fn drop(&mut self) {
OPERATIONS.lock().unwrap().push(Operation::DropClass);
}
}
let _ = BothIvarsAndTypeImplsDrop::alloc();
check([]);
let _ = unsafe { init_only_superclasses(BothIvarsAndTypeImplsDrop::alloc()) };
check([Operation::DropIvar, Operation::DropClass]);
let _ = unsafe { init_no_finalize(BothIvarsAndTypeImplsDrop::alloc()) };
check([
Operation::DropIvar,
Operation::DropIvar,
Operation::DropClass,
]);
let _ = unsafe { init(BothIvarsAndTypeImplsDrop::alloc()) };
check([
Operation::DropClass,
Operation::DropIvar,
Operation::DropIvar,
Operation::DropClass,
]);
}
#[test]
fn test_no_generated_dealloc_if_not_needed() {
#[allow(unused)]
struct Ivar {
field1: u8,
field2: bool,
}
define_class!(
#[unsafe(super(NSObject))]
#[ivars = Ivar]
struct IvarsNoDrop;
);
assert!(!mem::needs_drop::<IvarsNoDrop>());
assert!(!mem::needs_drop::<Ivar>());
assert_eq!(
IvarsNoDrop::class().instance_method(sel!(dealloc)),
NSObject::class().instance_method(sel!(dealloc)),
);
}
#[test]
fn zst_ivar() {
#[derive(Default, Debug, Clone, Copy)]
struct Ivar;
define_class!(
#[unsafe(super(NSObject))]
#[ivars = Cell<Ivar>]
struct IvarZst;
impl IvarZst {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send![super(this.set_ivars(Cell::new(Ivar))), init] }
}
}
);
assert_eq!(
IvarZst::class().instance_size(),
NSObject::class().instance_size(),
);
let ivar_name = if cfg!(feature = "gnustep-1-7") {
"IvarZst_ivars"
} else {
"ivars"
};
let ivar_name = CString::new(ivar_name).unwrap();
assert!(IvarZst::class().instance_variable(&ivar_name).is_none());
let obj = unsafe { init(IvarZst::alloc()) };
#[cfg(feature = "std")]
std::println!("{:?}", obj.ivars().get());
obj.ivars().set(Ivar);
}
#[test]
#[should_panic = "unsupported alignment 16 for `HasIvarWithHighAlignment::Ivars`"]
fn test_generate_ivar_high_alignment() {
#[repr(align(16))]
struct HighAlignment;
define_class!(
#[unsafe(super(NSObject))]
#[name = "HasIvarWithHighAlignment"]
#[ivars = HighAlignment]
struct HasIvarWithHighAlignment;
);
assert_eq!(HasIvarWithHighAlignment::class().instance_size(), 16);
let ivar_name = if cfg!(feature = "gnustep-1-7") {
"IvarZst_ivars"
} else {
"ivars"
};
let ivar_name = CString::new(ivar_name).unwrap();
let ivar = HasIvarWithHighAlignment::class()
.instance_variable(&ivar_name)
.unwrap();
assert_eq!(ivar.offset(), 16);
}
#[test]
fn test_ivar_access() {
define_class!(
#[unsafe(super(NSObject))]
#[ivars = Cell<Option<Retained<RcTestObject>>>]
struct RcIvar;
impl RcIvar {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
let this = this.set_ivars(Cell::new(Some(RcTestObject::new())));
unsafe { msg_send![super(this), init] }
}
}
);
let mut expected = ThreadTestData::current();
let _ = RcIvar::alloc();
expected.assert_current();
let _ = unsafe { init_only_superclasses(RcIvar::alloc()) };
expected.assert_current();
let obj = unsafe { init_no_finalize(RcIvar::alloc()) };
expected.assert_current();
obj.ivars().set(Some(RcTestObject::new()));
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
let obj = unsafe { init(RcIvar::alloc()) };
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
let ivar = unsafe { &*obj.ivars().as_ptr() }.clone();
obj.ivars().set(ivar);
expected.retain += 1;
expected.release += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
#[derive(Default)]
struct RcIvarSubclassIvars {
int: Cell<i32>,
obj: Cell<Retained<RcTestObject>>,
}
define_class!(
#[unsafe(super(RcIvar))]
#[ivars = RcIvarSubclassIvars]
struct RcIvarSubclass;
impl RcIvarSubclass {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
let this = this.set_ivars(RcIvarSubclassIvars {
int: Cell::new(42),
obj: Cell::new(RcTestObject::new()),
});
unsafe { msg_send![super(this), init] }
}
}
);
let obj = unsafe { init(RcIvarSubclass::alloc()) };
expected.alloc += 2;
expected.init += 2;
expected.assert_current();
assert_eq!(obj.ivars().int.get(), 42);
obj.ivars().int.set(obj.ivars().int.get() + 1);
assert_eq!(obj.ivars().int.get(), 43);
let ivar = unsafe { &*(**obj).ivars().as_ptr() }.clone().unwrap();
obj.ivars().obj.set(ivar);
expected.retain += 1;
expected.release += 1;
expected.drop += 1;
expected.assert_current();
(**obj).ivars().set(None);
expected.release += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
let obj = unsafe { init_only_superclasses(RcIvarSubclass::alloc()) };
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
#[cfg(feature = "std")]
std::println!("{:?}", unsafe { &*(**obj).ivars().as_ptr() });
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
}
#[test]
#[cfg_attr(not(debug_assertions), ignore = "only panics with debug assertions")]
#[should_panic = "tried to access uninitialized instance variable"]
fn access_invalid() {
define_class!(
#[unsafe(super(NSObject))]
#[ivars = Retained<NSObject>]
struct InvalidAccess;
);
let obj = unsafe { init_only_superclasses(InvalidAccess::alloc()) };
#[cfg(feature = "std")]
std::println!("{:?}", obj.ivars());
}
#[test]
#[should_panic = "panic in drop"]
#[ignore = "panicking in Drop requires that we actually implement `dealloc` as `C-unwind`"]
fn test_panic_in_drop() {
define_class!(
#[unsafe(super(NSObject))]
struct DropPanics;
);
impl Drop for DropPanics {
fn drop(&mut self) {
panic!("panic in drop");
}
}
let obj = DropPanics::alloc().set_ivars(());
let obj: Retained<DropPanics> = unsafe { msg_send![super(obj), init] };
drop(obj);
}
#[test]
#[should_panic = "panic in ivar drop"]
#[ignore = "panicking in Drop requires that we actually implement `dealloc` as `C-unwind`"]
fn test_panic_in_ivar_drop() {
struct DropPanics;
impl Drop for DropPanics {
fn drop(&mut self) {
panic!("panic in ivar drop");
}
}
define_class!(
#[unsafe(super(NSObject))]
#[ivars = DropPanics]
struct IvarDropPanics;
);
let obj = IvarDropPanics::alloc().set_ivars(DropPanics);
let obj: Retained<IvarDropPanics> = unsafe { msg_send![super(obj), init] };
drop(obj);
}
#[test]
fn test_retain_leak_in_drop() {
define_class!(
#[unsafe(super(NSObject))]
#[derive(Debug)]
struct DropRetainsAndLeaksSelf;
);
unsafe impl Send for DropRetainsAndLeaksSelf {}
unsafe impl Sync for DropRetainsAndLeaksSelf {}
static OBJ: OnceLock<Retained<DropRetainsAndLeaksSelf>> = OnceLock::new();
impl Drop for DropRetainsAndLeaksSelf {
fn drop(&mut self) {
fn inner(this: &DropRetainsAndLeaksSelf) {
OBJ.set(this.retain()).unwrap();
}
inner(self)
}
}
let obj = DropRetainsAndLeaksSelf::alloc().set_ivars(());
let obj: Retained<DropRetainsAndLeaksSelf> = unsafe { msg_send![super(obj), init] };
drop(obj);
let _ = OBJ.get().unwrap();
}
}