use alloc::borrow::Cow;
use alloc::format;
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, DeclaredClass};
#[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 DeclaredIvarsHelper {
const HAS_IVARS: bool;
const HAS_DROP_FLAG: bool;
}
impl<T: DeclaredClass> DeclaredIvarsHelper 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 + DeclaredClass>(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: DeclaredClass>(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: DeclaredClass>(builder: &mut ClassBuilder)
where
T::Super: ClassType,
{
if mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>() {
let func: unsafe extern "C" fn(_, _) = dealloc::<T>;
unsafe { builder.add_method(sel!(dealloc), func) };
} else {
}
}
unsafe extern "C" fn dealloc<T: DeclaredClass>(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 register_with_ivars<T: DeclaredClass>(
mut builder: ClassBuilder,
) -> (&'static AnyClass, isize, isize) {
let (ivar_name, drop_flag_name): (Cow<'static, str>, Cow<'static, str>) = {
if cfg!(feature = "gnustep-1-7") {
(
format!("{}_ivars", T::NAME).into(),
format!("{}_drop_flag", T::NAME).into(),
)
} else {
("ivars".into(), "drop_flag".into())
}
};
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>(&ivar_name, &ivar_encoding) };
}
if T::HAS_DROP_FLAG {
builder.add_ivar::<DropFlag>(&drop_flag_name);
}
let cls = builder.register();
let ivars_offset = if T::HAS_IVARS {
fn get_ivar_failed() -> ! {
unreachable!("failed retrieving instance variable on newly declared class")
}
cls.instance_variable(&ivar_name)
.unwrap_or_else(|| get_ivar_failed())
.offset()
} else {
0
};
let drop_flag_offset = if T::HAS_DROP_FLAG {
fn get_drop_flag_failed() -> ! {
unreachable!("failed retrieving drop flag instance variable on newly declared class")
}
cls.instance_variable(&drop_flag_name)
.unwrap_or_else(|| get_drop_flag_failed())
.offset()
} else {
0
};
(cls, ivars_offset, drop_flag_offset)
}
#[inline]
#[track_caller]
pub(crate) unsafe fn initialize_ivars<T: DeclaredClass>(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: DeclaredClass>(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: DeclaredClass>(
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 std::println;
use std::sync::Mutex;
use alloc::vec::Vec;
use super::*;
use crate::mutability::{InteriorMutable, Mutable};
use crate::rc::{Allocated, PartialInit, RcTestObject, Retained, ThreadTestData};
use crate::runtime::NSObject;
use crate::{declare_class, msg_send, msg_send_id};
unsafe fn init_only_superclasses<T: DeclaredClass>(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: DeclaredClass>(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: DeclaredClass>(obj: Allocated<T>) -> Retained<T> {
unsafe { msg_send_id![obj, init] }
}
#[test]
fn assert_size() {
assert_eq!(mem::size_of::<DropFlag>(), 1);
}
#[test]
fn test_dealloc_and_dealloc_subclasses() {
#[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();
}
declare_class!(
struct ImplsDrop;
unsafe impl ClassType for ImplsDrop {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "ImplsDrop";
}
impl DeclaredClass for ImplsDrop {
type Ivars = ();
}
unsafe impl ImplsDrop {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send_id![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]);
declare_class!(
struct IvarsImplDrop;
unsafe impl ClassType for IvarsImplDrop {
type Super = ImplsDrop;
type Mutability = InteriorMutable;
const NAME: &'static str = "IvarsImplDrop";
}
impl DeclaredClass for IvarsImplDrop {
type Ivars = IvarThatImplsDrop;
}
unsafe impl IvarsImplDrop {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send_id![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]);
declare_class!(
struct BothIvarsAndTypeImplsDrop;
unsafe impl ClassType for BothIvarsAndTypeImplsDrop {
type Super = IvarsImplDrop;
type Mutability = InteriorMutable;
const NAME: &'static str = "BothIvarsAndTypeImplsDrop";
}
impl DeclaredClass for BothIvarsAndTypeImplsDrop {
type Ivars = IvarThatImplsDrop;
}
unsafe impl BothIvarsAndTypeImplsDrop {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send_id![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,
}
declare_class!(
struct IvarsNoDrop;
unsafe impl ClassType for IvarsNoDrop {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "IvarsNoDrop";
}
impl DeclaredClass for IvarsNoDrop {
type Ivars = Ivar;
}
);
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)]
struct Ivar;
declare_class!(
struct IvarZst;
unsafe impl ClassType for IvarZst {
type Super = NSObject;
type Mutability = Mutable;
const NAME: &'static str = "IvarZst";
}
impl DeclaredClass for IvarZst {
type Ivars = Ivar;
}
unsafe impl IvarZst {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
unsafe { msg_send_id![super(this.set_ivars(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"
};
assert!(IvarZst::class().instance_variable(ivar_name).is_none());
let mut obj = unsafe { init(IvarZst::alloc()) };
println!("{:?}", obj.ivars());
*obj.ivars_mut() = Ivar;
}
#[test]
#[should_panic = "unsupported alignment 16 for `HasIvarWithHighAlignment::Ivars`"]
fn test_generate_ivar_high_alignment() {
#[repr(align(16))]
struct HighAlignment;
declare_class!(
struct HasIvarWithHighAlignment;
unsafe impl ClassType for HasIvarWithHighAlignment {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "HasIvarWithHighAlignment";
}
impl DeclaredClass for HasIvarWithHighAlignment {
type Ivars = HighAlignment;
}
);
assert_eq!(HasIvarWithHighAlignment::class().instance_size(), 16);
let ivar_name = if cfg!(feature = "gnustep-1-7") {
"IvarZst_ivars"
} else {
"ivars"
};
let ivar = HasIvarWithHighAlignment::class()
.instance_variable(ivar_name)
.unwrap();
assert_eq!(ivar.offset(), 16);
}
#[test]
#[allow(clippy::assigning_clones)]
fn test_ivar_access() {
declare_class!(
struct RcIvar;
unsafe impl ClassType for RcIvar {
type Super = NSObject;
type Mutability = Mutable;
const NAME: &'static str = "RcIvar";
}
impl DeclaredClass for RcIvar {
type Ivars = Option<Retained<RcTestObject>>;
}
unsafe impl RcIvar {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
let this = this.set_ivars(Some(RcTestObject::new()));
unsafe { msg_send_id![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 mut obj = unsafe { init_no_finalize(RcIvar::alloc()) };
expected.assert_current();
*obj.ivars_mut() = Some(RcTestObject::new());
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
let mut obj = unsafe { init(RcIvar::alloc()) };
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
*obj.ivars_mut() = obj.ivars().clone();
expected.retain += 1;
expected.release += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
#[derive(Default, Debug, PartialEq, Eq)]
struct RcIvarSubclassIvars {
int: i32,
obj: Retained<RcTestObject>,
}
declare_class!(
struct RcIvarSubclass;
unsafe impl ClassType for RcIvarSubclass {
type Super = RcIvar;
type Mutability = Mutable;
const NAME: &'static str = "RcIvarSubclass";
}
impl DeclaredClass for RcIvarSubclass {
type Ivars = RcIvarSubclassIvars;
}
unsafe impl RcIvarSubclass {
#[method_id(init)]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
let this = this.set_ivars(RcIvarSubclassIvars {
int: 42,
obj: RcTestObject::new(),
});
unsafe { msg_send_id![super(this), init] }
}
}
);
let mut obj = unsafe { init(RcIvarSubclass::alloc()) };
expected.alloc += 2;
expected.init += 2;
expected.assert_current();
assert_eq!(obj.ivars().int, 42);
obj.ivars_mut().int += 1;
assert_eq!(obj.ivars().int, 43);
obj.ivars_mut().obj = (**obj).ivars().clone().unwrap();
expected.retain += 1;
expected.release += 1;
expected.drop += 1;
expected.assert_current();
*(**obj).ivars_mut() = 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();
println!("{:?}", (**obj).ivars());
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() {
declare_class!(
struct InvalidAccess;
unsafe impl ClassType for InvalidAccess {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "InvalidAccess";
}
impl DeclaredClass for InvalidAccess {
type Ivars = Retained<NSObject>;
}
);
let obj = unsafe { init_only_superclasses(InvalidAccess::alloc()) };
println!("{:?}", obj.ivars());
}
#[test]
#[should_panic = "panic in drop"]
#[cfg_attr(
any(feature = "unstable-c-unwind", target_arch = "x86"),
ignore = "panicking in Drop requires that we actually implement `dealloc` as `C-unwind`"
)]
fn test_panic_in_drop() {
declare_class!(
struct DropPanics;
unsafe impl ClassType for DropPanics {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "DropPanics";
}
impl DeclaredClass for 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_id![super(obj), init] };
drop(obj);
}
#[test]
#[should_panic = "panic in ivar drop"]
#[cfg_attr(
any(feature = "unstable-c-unwind", target_arch = "x86"),
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");
}
}
declare_class!(
struct IvarDropPanics;
unsafe impl ClassType for IvarDropPanics {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "IvarDropPanics";
}
impl DeclaredClass for IvarDropPanics {
type Ivars = DropPanics;
}
);
let obj = IvarDropPanics::alloc().set_ivars(DropPanics);
let obj: Retained<IvarDropPanics> = unsafe { msg_send_id![super(obj), init] };
drop(obj);
}
}