objc2/__macro_helpers/
defined_ivars.rs

1//! # Supporting code for instance variables on defined classes.
2//!
3//! Adding instance variables to Objective-C classes is fairly simple, it can
4//! be done using `ClassBuilder::add_ivar`.
5//!
6//! However, things become more complicated once we have to handle `Drop`,
7//! deallocation and unwind safety; remember, `dealloc` may be called even on
8//! newly, non-initialized instances.
9//!
10//! Note that Swift [doesn't handle this][swift-deinit-unsound], but that
11//! doesn't mean we can simply stick our heads in the sand.
12//!
13//! Basically, instead of storing the ivars directly, we store it as the
14//! following tagged enum:
15//! ```
16//! #[repr(u8)]
17//! enum ActualIvar<T: objc2::DefinedClass> {
18//!     Allocated = 0,
19//!     PartialInit(T::Ivars),
20//!     Finalized(T::Ivars),
21//! }
22//! ```
23//!
24//! For performance reasons, we unfortunately can't write it that cleanly: we
25//! want the data and the drop flag as two separate ivars instead of combining
26//! them into one, since that will give the layout algorithm in the
27//! Objective-C runtime more information to work with, and it allows us to
28//! selectively omit the drop flag or the data storage when either is not
29//! needed.
30//!
31//! Ideally, we'd be able to somehow statically detect when the ivars have a
32//! zero niche, which would allow us to know if the type is safe to drop when
33//! zero-initialized:
34//! ```ignore
35//! None::<T::Ivars>.is_all_zeroes_bitpattern()
36//! ```
37//!
38//! However, detecting if the `None` is all zeroes requires reading the bytes,
39//! which is [unsound for types that may have padding][unsound-read-padding],
40//! since that padding is uninitialized.
41//!
42//! So this is an optimization that we don't yet do, but that may be possible
43//! in the future using something like `bytemuck::ZeroableInOption`.
44//!
45//! [swift-deinit-unsound]: https://github.com/apple/swift/issues/68734
46//! [unsound-read-padding]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ea068e8d9e55801aa9520ea914eb2822
47
48use alloc::borrow::Cow;
49use alloc::ffi::CString;
50use alloc::format;
51use core::ffi::CStr;
52use core::mem;
53use core::ptr::{self, NonNull};
54
55use crate::encode::{Encode, Encoding};
56use crate::runtime::{AnyClass, AnyObject, ClassBuilder, MessageReceiver, Sel};
57use crate::{sel, ClassType, DefinedClass};
58
59/// A type representing the drop flags that may be set for a type.
60#[repr(u8)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub(crate) enum DropFlag {
63    /// Set to zero to ensure that this is the default when created by the
64    /// Objective-C runtime.
65    ///
66    /// Ivars are [documented][obj-init-zeroed] to be zero-initialized after
67    /// allocation, and that has been true since at least [the Objective-C
68    /// version shipped with Mac OS X 10.0][objc4-208-init].
69    ///
70    /// [obj-init-zeroed]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithObjects/WorkingwithObjects.html#//apple_ref/doc/uid/TP40011210-CH4-SW7
71    /// [objc4-208-init]: https://github.com/apple-oss-distributions/objc4/blob/objc4-208/runtime/objc-class.m#L367
72    #[allow(dead_code)]
73    Allocated = 0x00,
74    /// Used when `mem::needs_drop::<T::Ivars>()`, or with debug assertions enabled.
75    InitializedIvars = 0x0f,
76    /// Used when `mem::needs_drop::<T>()`, or with debug assertions enabled.
77    Finalized = 0xff,
78}
79
80// SAFETY: The DropFlag is #[repr(u8)]
81unsafe impl Encode for DropFlag {
82    const ENCODING: Encoding = u8::ENCODING;
83}
84
85pub trait DefinedIvarsHelper {
86    const HAS_IVARS: bool;
87    const HAS_DROP_FLAG: bool;
88}
89
90impl<T: DefinedClass> DefinedIvarsHelper for T {
91    /// Only add ivar if we need the runtime to allocate memory for it.
92    ///
93    /// We can avoid doing so if the type is a zero-sized type (ZST), and the
94    /// required alignment is less than the alignment of a pointer (objects
95    /// are guaranteed to have at least that alignment themselves).
96    const HAS_IVARS: bool = {
97        mem::size_of::<T::Ivars>() > 0
98            || mem::align_of::<T::Ivars>() > mem::align_of::<*mut AnyObject>()
99    };
100    /// Only add drop flag if the type or the ivars need it.
101    ///
102    /// `needs_drop::<T>` can reliably detect a direct implementation of
103    /// `Drop`, since the type only includes `ManuallyDrop` or `PhantomData`
104    /// fields.
105    const HAS_DROP_FLAG: bool = mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>();
106}
107
108/// Helper function for getting a pointer to the instance variable.
109///
110/// # Safety
111///
112/// The pointer must be valid, and the instance variable offset (if it has
113/// any) must have been initialized.
114#[inline]
115unsafe fn ptr_to_ivar<T: ?Sized + DefinedClass>(ptr: NonNull<T>) -> NonNull<T::Ivars> {
116    // This is called even when there is no ivars, but that's fine, since in
117    // that case the ivar is zero-sized, and the offset will be zero, so we
118    // can still compute a valid pointer to the ivar.
119    //
120    // debug_assert!(T::HAS_IVARS);
121
122    // SAFETY: That an instance variable with the given type exists at the
123    // specified offset is ensured by `DefinedClass` trait implementor.
124    unsafe { AnyObject::ivar_at_offset::<T::Ivars>(ptr.cast(), T::__ivars_offset()) }
125}
126
127/// Helper function for getting a pointer to the drop flag.
128///
129/// # Safety
130///
131/// The pointer must be valid and have an initialized drop flag.
132#[inline]
133unsafe fn ptr_to_drop_flag<T: DefinedClass>(ptr: NonNull<T>) -> *mut DropFlag {
134    debug_assert!(T::HAS_DROP_FLAG, "type did not have drop flag");
135    // SAFETY: That a drop flag exists at the specified offset is ensured
136    // by caller.
137    unsafe { AnyObject::ivar_at_offset::<DropFlag>(ptr.cast(), T::__drop_flag_offset()).as_ptr() }
138}
139
140pub(crate) fn setup_dealloc<T: DefinedClass>(builder: &mut ClassBuilder)
141where
142    T::Super: ClassType,
143{
144    // Add dealloc if the class or the ivars need dropping.
145    if mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>() {
146        let func: unsafe extern "C-unwind" fn(_, _) = dealloc::<T>;
147        // SAFETY: The function signature is correct, and method contract is
148        // upheld inside `dealloc`.
149        unsafe { builder.add_method(sel!(dealloc), func) };
150    } else {
151        // Users should not rely on this omission, it is only an optimization.
152    }
153}
154
155/// The `dealloc` Objective-C method.
156///
157/// See the following links for more details about `dealloc`:
158/// - <https://clang.llvm.org/docs/AutomaticReferenceCounting.html#dealloc>
159/// - <https://developer.apple.com/documentation/objectivec/nsobject/1571947-dealloc>
160/// - <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-SW2>
161unsafe extern "C-unwind" fn dealloc<T: DefinedClass>(this: NonNull<T>, cmd: Sel)
162where
163    T::Super: ClassType,
164{
165    /// Helper function for marking the cold path when branching.
166    #[inline]
167    #[cold]
168    fn cold_path() {}
169
170    // SAFETY: `dealloc` is only registered when there is a need for dropping,
171    // and hence a need for a drop flag.
172    let drop_flag = unsafe { *ptr_to_drop_flag(this) };
173
174    if mem::needs_drop::<T>() {
175        match drop_flag {
176            // Don't deallocate the current instance if it has not been fully
177            // initialized.
178            //
179            // Note that we still run the superclass deinitializer below.
180            DropFlag::Allocated | DropFlag::InitializedIvars => cold_path(),
181            // SAFETY: This is the `dealloc` method, so we know that the type
182            // never needs to be deallocated again.
183            //
184            // Additionally, we know that the type was fully initialized, since
185            // that's what the drop flag says.
186            //
187            // TODO: This can unwind, is it correct to just let that
188            // propagate?
189            DropFlag::Finalized => unsafe { ptr::drop_in_place(this.as_ptr()) },
190        }
191    }
192
193    // Note: This should be done inside `.cxx_destruct`, since if a superclass
194    // calls an overwritten method in its `dealloc`, it can access
195    // deinitialized instance variables; but we can't do that without
196    // generating statics, so we have to do it in `dealloc` for now.
197    //
198    // It is very important that we do this after the `Drop` of the class
199    // itself above, though.
200    //
201    // Another possibility would be to read the contents of the ivars onto the
202    // stack here, and only deinitialize after the superclass' `dealloc`, but
203    // that would break the pinning guarantee that ivars otherwise have.
204    if mem::needs_drop::<T::Ivars>() {
205        match drop_flag {
206            // Do nothing if the ivars have not been initialized.
207            DropFlag::Allocated => cold_path(),
208            DropFlag::InitializedIvars | DropFlag::Finalized => {
209                // SAFETY: The instance variable is initialized, so it is
210                // valid to drop here.
211                //
212                // TODO: This can unwind, is it correct to just let that
213                // propagate?
214                unsafe { ptr::drop_in_place(ptr_to_ivar(this).as_ptr()) };
215            }
216        }
217    }
218
219    // The superclass' "marker" that this stores is wrapped in `ManuallyDrop`,
220    // we drop it by calling the superclass' `dealloc` method instead.
221    //
222    // Note: ARC does this automatically, which means most Objective-C code in
223    // the wild don't contain this call; but we _are_ ARC, so we must do this.
224    //
225    // SAFETY: The argument and return types are correct, and we make sure to
226    // only call this once.
227    unsafe {
228        MessageReceiver::send_super_message(
229            this,
230            <T as ClassType>::Super::class(),
231            cmd, // Reuse the selector
232            (),  // No arguments
233        )
234    }
235}
236
237/// Register the class, and get the ivar offsets.
238#[inline]
239pub(crate) fn register_with_ivars<T: DefinedClass>(
240    mut builder: ClassBuilder,
241) -> (&'static AnyClass, isize, isize) {
242    let (ivar_name, drop_flag_name): (Cow<'static, CStr>, Cow<'static, CStr>) = {
243        if cfg!(feature = "gnustep-1-7") {
244            // GNUStep does not support a subclass having an ivar with the
245            // same name as a superclass, so let's use the class name as the
246            // ivar name to ensure uniqueness.
247            (
248                CString::new(format!("{}_ivars", T::NAME)).unwrap().into(),
249                CString::new(format!("{}_drop_flag", T::NAME))
250                    .unwrap()
251                    .into(),
252            )
253        } else {
254            // SAFETY: The byte slices are NUL-terminated, and do not contain
255            // interior NUL bytes.
256            // TODO: Use `c"my_str"` syntax once in MSRV
257            unsafe {
258                (
259                    CStr::from_bytes_with_nul_unchecked(b"ivars\0").into(),
260                    CStr::from_bytes_with_nul_unchecked(b"drop_flag\0").into(),
261                )
262            }
263        }
264    };
265
266    if T::HAS_IVARS {
267        // TODO: Consider not adding a encoding - Swift doesn't do it.
268        let ivar_encoding = Encoding::Array(
269            mem::size_of::<T::Ivars>() as u64,
270            match mem::align_of::<T::Ivars>() {
271                1 => &u8::ENCODING,
272                2 => &u16::ENCODING,
273                4 => &u32::ENCODING,
274                // The alignment of `u64` may not be 8 on all architectures
275                8 if mem::align_of::<u64>() == 8 => &u64::ENCODING,
276                alignment => panic!("unsupported alignment {alignment} for `{}::Ivars`", T::NAME),
277            },
278        );
279        unsafe { builder.add_ivar_inner::<T::Ivars>(&ivar_name, &ivar_encoding) };
280    }
281
282    if T::HAS_DROP_FLAG {
283        // TODO: Maybe we can reuse the drop flag when subclassing an already
284        // defined class?
285        builder.add_ivar::<DropFlag>(&drop_flag_name);
286    }
287
288    let cls = builder.register();
289
290    let ivars_offset = if T::HAS_IVARS {
291        // Monomorphized error handling
292        // Intentionally not #[track_caller], we expect this error to never occur
293        fn get_ivar_failed() -> ! {
294            unreachable!("failed retrieving instance variable on newly defined class")
295        }
296
297        cls.instance_variable(&ivar_name)
298            .unwrap_or_else(|| get_ivar_failed())
299            .offset()
300    } else {
301        // Fallback to an offset of zero.
302        //
303        // This is fine, since any reads here will only be via zero-sized
304        // ivars, where the actual pointer doesn't matter.
305        0
306    };
307
308    let drop_flag_offset = if T::HAS_DROP_FLAG {
309        // Monomorphized error handling
310        // Intentionally not #[track_caller], we expect this error to never occur
311        fn get_drop_flag_failed() -> ! {
312            unreachable!("failed retrieving drop flag instance variable on newly defined class")
313        }
314
315        cls.instance_variable(&drop_flag_name)
316            .unwrap_or_else(|| get_drop_flag_failed())
317            .offset()
318    } else {
319        // Fall back to an offset of zero.
320        //
321        // This is fine, since the drop flag is never actually used in the
322        // cases where it was not added.
323        0
324    };
325
326    (cls, ivars_offset, drop_flag_offset)
327}
328
329/// # Safety
330///
331/// The pointer must be a valid, newly allocated instance.
332#[inline]
333#[track_caller]
334pub(crate) unsafe fn initialize_ivars<T: DefinedClass>(ptr: NonNull<T>, val: T::Ivars) {
335    // Debug assert the state of the drop flag
336    if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
337        // SAFETY: Just checked that the drop flag is available.
338        match unsafe { *ptr_to_drop_flag(ptr) } {
339            DropFlag::Allocated => {
340                // Allow initialization after allocation
341            }
342            DropFlag::InitializedIvars => {
343                panic!("tried to initialize ivars after they were already initialized")
344            }
345            DropFlag::Finalized => {
346                panic!("tried to initialize ivars on an already initialized object")
347            }
348        }
349    }
350
351    // SAFETY:
352    // - Caller ensures the pointer is valid.
353    // - The location is properly aligned by `ClassBuilder::add_ivar`.
354    // - This write is done as part of initialization, so we know that the
355    //   pointer is not shared elsewhere.
356    unsafe { ptr_to_ivar(ptr).as_ptr().write(val) };
357
358    // Write to drop flag that we've initialized the instance variables.
359    //
360    // Note: We intentionally only do this _after_ writing to the ivars,
361    // for better unwind safety.
362    if T::HAS_DROP_FLAG && (mem::needs_drop::<T::Ivars>() || cfg!(debug_assertions)) {
363        // SAFETY: Just checked that the drop flag is available.
364        unsafe { ptr_to_drop_flag(ptr).write(DropFlag::InitializedIvars) }
365    }
366}
367
368/// # Safety
369///
370/// The pointer must be valid and finalized (i.e. all super initializers must
371/// have been run).
372#[inline]
373#[track_caller]
374pub(crate) unsafe fn set_finalized<T: DefinedClass>(ptr: NonNull<T>) {
375    // Debug assert the state of the drop flag
376    if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
377        // SAFETY: Just checked that the drop flag is available.
378        match unsafe { *ptr_to_drop_flag(ptr) } {
379            DropFlag::Allocated => {
380                panic!("tried to finalize an object that was not yet fully initialized")
381            }
382            DropFlag::InitializedIvars => {
383                // Allow finalizing after initialization
384            }
385            DropFlag::Finalized => {
386                panic!("tried to finalize an already finalized object")
387            }
388        }
389    }
390
391    // Write to drop flag that we've fully initialized the class.
392    if T::HAS_DROP_FLAG && (mem::needs_drop::<T>() || cfg!(debug_assertions)) {
393        // SAFETY: Just checked that the drop flag is available.
394        unsafe { ptr_to_drop_flag(ptr).write(DropFlag::Finalized) }
395    }
396}
397
398/// # Safety
399///
400/// The pointer must be valid and the instance variables must be initialized.
401#[inline]
402#[track_caller]
403pub(crate) unsafe fn get_initialized_ivar_ptr<T: DefinedClass>(
404    ptr: NonNull<T>,
405) -> NonNull<T::Ivars> {
406    // Debug assert the state of the drop flag
407    if T::HAS_DROP_FLAG && cfg!(debug_assertions) {
408        // SAFETY: Just checked that the drop flag is available.
409        match unsafe { *ptr_to_drop_flag(ptr) } {
410            DropFlag::Allocated => {
411                panic!("tried to access uninitialized instance variable")
412            }
413            DropFlag::InitializedIvars => {
414                // Allow accessing even if not finalized, since we only set
415                // that state _after_ it actually happens, while accesses may
416                // be done by the superclass initializer in e.g. an
417                // overwritten method.
418            }
419            DropFlag::Finalized => {
420                // Allow accessing if finalized
421            }
422        }
423    }
424
425    // SAFETY: That the pointer is valid is ensured by caller.
426    unsafe { ptr_to_ivar(ptr) }
427}
428
429#[cfg(test)]
430mod tests {
431    use alloc::vec::Vec;
432    use core::cell::Cell;
433    use std::sync::OnceLock;
434
435    use super::*;
436    use crate::rc::{Allocated, PartialInit, RcTestObject, Retained, ThreadTestData};
437    use crate::runtime::NSObject;
438    use crate::{define_class, msg_send, AnyThread, Message};
439
440    /// Initialize superclasses, but not own class.
441    unsafe fn init_only_superclasses<T: DefinedClass>(obj: Allocated<T>) -> Retained<T>
442    where
443        T::Super: ClassType,
444    {
445        unsafe { Retained::from_raw(msg_send![super(Allocated::into_ptr(obj)), init]) }.unwrap()
446    }
447
448    /// Initialize, but fail to finalize (which is done internally by
449    /// `msg_send!` when returning `Retained`).
450    unsafe fn init_no_finalize<T: DefinedClass>(obj: Allocated<T>) -> Retained<T>
451    where
452        T::Super: ClassType,
453        T::Ivars: Default,
454    {
455        let obj = obj.set_ivars(Default::default());
456        unsafe { Retained::from_raw(msg_send![super(PartialInit::into_ptr(obj)), init]) }.unwrap()
457    }
458
459    /// Initialize properly.
460    unsafe fn init<T: DefinedClass>(obj: Allocated<T>) -> Retained<T> {
461        unsafe { msg_send![obj, init] }
462    }
463
464    #[test]
465    fn assert_size() {
466        assert_eq!(mem::size_of::<DropFlag>(), 1);
467    }
468
469    #[test]
470    #[cfg(feature = "std")]
471    fn test_dealloc_and_dealloc_subclasses() {
472        use std::sync::Mutex;
473
474        #[derive(Debug, PartialEq)]
475        enum Operation {
476            DropIvar,
477            DropClass,
478        }
479
480        static OPERATIONS: Mutex<Vec<Operation>> = Mutex::new(Vec::new());
481
482        #[derive(Default)]
483        struct IvarThatImplsDrop;
484
485        impl Drop for IvarThatImplsDrop {
486            fn drop(&mut self) {
487                OPERATIONS.lock().unwrap().push(Operation::DropIvar);
488            }
489        }
490
491        #[track_caller]
492        fn check<const N: usize>(expected: [Operation; N]) {
493            let mut operations = OPERATIONS.lock().unwrap();
494            assert_eq!(&**operations, expected);
495            operations.clear();
496        }
497
498        // First class
499
500        define_class!(
501            #[unsafe(super(NSObject))]
502            #[ivars = ()]
503            struct ImplsDrop;
504
505            impl ImplsDrop {
506                #[unsafe(method_id(init))]
507                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
508                    unsafe { msg_send![super(this.set_ivars(())), init] }
509                }
510            }
511        );
512
513        impl Drop for ImplsDrop {
514            fn drop(&mut self) {
515                OPERATIONS.lock().unwrap().push(Operation::DropClass);
516            }
517        }
518
519        let _ = ImplsDrop::alloc();
520        check([]);
521
522        let _ = unsafe { init_only_superclasses(ImplsDrop::alloc()) };
523        check([]);
524
525        let _ = unsafe { init_no_finalize(ImplsDrop::alloc()) };
526        check([]);
527
528        let _ = unsafe { init(ImplsDrop::alloc()) };
529        check([Operation::DropClass]);
530
531        // Subclass
532
533        define_class!(
534            #[unsafe(super(ImplsDrop))]
535            #[ivars = IvarThatImplsDrop]
536            struct IvarsImplDrop;
537
538            impl IvarsImplDrop {
539                #[unsafe(method_id(init))]
540                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
541                    unsafe { msg_send![super(this.set_ivars(IvarThatImplsDrop)), init] }
542                }
543            }
544        );
545
546        let _ = IvarsImplDrop::alloc();
547        check([]);
548
549        let _ = unsafe { init_only_superclasses(IvarsImplDrop::alloc()) };
550        check([Operation::DropClass]);
551
552        let _ = unsafe { init_no_finalize(IvarsImplDrop::alloc()) };
553        check([Operation::DropIvar, Operation::DropClass]);
554
555        let _ = unsafe { init(IvarsImplDrop::alloc()) };
556        check([Operation::DropIvar, Operation::DropClass]);
557
558        // Further subclass
559
560        define_class!(
561            #[unsafe(super(IvarsImplDrop))]
562            #[ivars = IvarThatImplsDrop]
563            struct BothIvarsAndTypeImplsDrop;
564
565            impl BothIvarsAndTypeImplsDrop {
566                #[unsafe(method_id(init))]
567                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
568                    unsafe { msg_send![super(this.set_ivars(IvarThatImplsDrop)), init] }
569                }
570            }
571        );
572
573        impl Drop for BothIvarsAndTypeImplsDrop {
574            fn drop(&mut self) {
575                OPERATIONS.lock().unwrap().push(Operation::DropClass);
576            }
577        }
578
579        let _ = BothIvarsAndTypeImplsDrop::alloc();
580        check([]);
581
582        let _ = unsafe { init_only_superclasses(BothIvarsAndTypeImplsDrop::alloc()) };
583        check([Operation::DropIvar, Operation::DropClass]);
584
585        let _ = unsafe { init_no_finalize(BothIvarsAndTypeImplsDrop::alloc()) };
586        check([
587            Operation::DropIvar,
588            Operation::DropIvar,
589            Operation::DropClass,
590        ]);
591
592        let _ = unsafe { init(BothIvarsAndTypeImplsDrop::alloc()) };
593        check([
594            Operation::DropClass,
595            Operation::DropIvar,
596            Operation::DropIvar,
597            Operation::DropClass,
598        ]);
599    }
600
601    #[test]
602    fn test_no_generated_dealloc_if_not_needed() {
603        #[allow(unused)]
604        struct Ivar {
605            field1: u8,
606            field2: bool,
607        }
608
609        define_class!(
610            #[unsafe(super(NSObject))]
611            #[ivars = Ivar]
612            struct IvarsNoDrop;
613        );
614
615        assert!(!mem::needs_drop::<IvarsNoDrop>());
616        assert!(!mem::needs_drop::<Ivar>());
617        assert_eq!(
618            IvarsNoDrop::class().instance_method(sel!(dealloc)),
619            NSObject::class().instance_method(sel!(dealloc)),
620        );
621    }
622
623    #[test]
624    fn zst_ivar() {
625        #[derive(Default, Debug, Clone, Copy)]
626        struct Ivar;
627
628        define_class!(
629            #[unsafe(super(NSObject))]
630            #[ivars = Cell<Ivar>]
631            struct IvarZst;
632
633            impl IvarZst {
634                #[unsafe(method_id(init))]
635                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
636                    unsafe { msg_send![super(this.set_ivars(Cell::new(Ivar))), init] }
637                }
638            }
639        );
640
641        assert_eq!(
642            IvarZst::class().instance_size(),
643            NSObject::class().instance_size(),
644        );
645        let ivar_name = if cfg!(feature = "gnustep-1-7") {
646            "IvarZst_ivars"
647        } else {
648            "ivars"
649        };
650        let ivar_name = CString::new(ivar_name).unwrap();
651        assert!(IvarZst::class().instance_variable(&ivar_name).is_none());
652
653        let obj = unsafe { init(IvarZst::alloc()) };
654        #[cfg(feature = "std")]
655        std::println!("{:?}", obj.ivars().get());
656        obj.ivars().set(Ivar);
657    }
658
659    #[test]
660    #[should_panic = "unsupported alignment 16 for `HasIvarWithHighAlignment::Ivars`"]
661    fn test_generate_ivar_high_alignment() {
662        #[repr(align(16))]
663        struct HighAlignment;
664
665        define_class!(
666            #[unsafe(super(NSObject))]
667            #[name = "HasIvarWithHighAlignment"]
668            #[ivars = HighAlignment]
669            struct HasIvarWithHighAlignment;
670        );
671
672        // Have to allocate up to the desired alignment, but no need to go
673        // further, since the object is zero-sized.
674        assert_eq!(HasIvarWithHighAlignment::class().instance_size(), 16);
675
676        let ivar_name = if cfg!(feature = "gnustep-1-7") {
677            "IvarZst_ivars"
678        } else {
679            "ivars"
680        };
681        let ivar_name = CString::new(ivar_name).unwrap();
682        let ivar = HasIvarWithHighAlignment::class()
683            .instance_variable(&ivar_name)
684            .unwrap();
685        assert_eq!(ivar.offset(), 16);
686    }
687
688    #[test]
689    fn test_ivar_access() {
690        define_class!(
691            #[unsafe(super(NSObject))]
692            #[ivars = Cell<Option<Retained<RcTestObject>>>]
693            struct RcIvar;
694
695            impl RcIvar {
696                #[unsafe(method_id(init))]
697                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
698                    let this = this.set_ivars(Cell::new(Some(RcTestObject::new())));
699                    unsafe { msg_send![super(this), init] }
700                }
701            }
702        );
703
704        let mut expected = ThreadTestData::current();
705
706        let _ = RcIvar::alloc();
707        expected.assert_current();
708
709        let _ = unsafe { init_only_superclasses(RcIvar::alloc()) };
710        expected.assert_current();
711
712        // Ivar access is valid even if the class is not finalized.
713        let obj = unsafe { init_no_finalize(RcIvar::alloc()) };
714        expected.assert_current();
715
716        obj.ivars().set(Some(RcTestObject::new()));
717        expected.alloc += 1;
718        expected.init += 1;
719        expected.assert_current();
720
721        drop(obj);
722        expected.release += 1;
723        expected.drop += 1;
724        expected.assert_current();
725
726        let obj = unsafe { init(RcIvar::alloc()) };
727        expected.alloc += 1;
728        expected.init += 1;
729        expected.assert_current();
730
731        // SAFETY: Cloned immediately after, so not accessed while borrowed
732        let ivar = unsafe { &*obj.ivars().as_ptr() }.clone();
733        obj.ivars().set(ivar);
734        expected.retain += 1;
735        expected.release += 1;
736        expected.assert_current();
737
738        drop(obj);
739        expected.release += 1;
740        expected.drop += 1;
741        expected.assert_current();
742
743        #[derive(Default)]
744        struct RcIvarSubclassIvars {
745            int: Cell<i32>,
746            obj: Cell<Retained<RcTestObject>>,
747        }
748
749        define_class!(
750            #[unsafe(super(RcIvar))]
751            #[ivars = RcIvarSubclassIvars]
752            struct RcIvarSubclass;
753
754            impl RcIvarSubclass {
755                #[unsafe(method_id(init))]
756                fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
757                    let this = this.set_ivars(RcIvarSubclassIvars {
758                        int: Cell::new(42),
759                        obj: Cell::new(RcTestObject::new()),
760                    });
761                    unsafe { msg_send![super(this), init] }
762                }
763            }
764        );
765
766        let obj = unsafe { init(RcIvarSubclass::alloc()) };
767        expected.alloc += 2;
768        expected.init += 2;
769        expected.assert_current();
770        assert_eq!(obj.ivars().int.get(), 42);
771
772        obj.ivars().int.set(obj.ivars().int.get() + 1);
773        assert_eq!(obj.ivars().int.get(), 43);
774
775        // SAFETY: Cloned immediately after, so not accessed while borrowed
776        let ivar = unsafe { &*(**obj).ivars().as_ptr() }.clone().unwrap();
777        obj.ivars().obj.set(ivar);
778        expected.retain += 1;
779        expected.release += 1;
780        expected.drop += 1;
781        expected.assert_current();
782
783        // Change super ivars
784        (**obj).ivars().set(None);
785        expected.release += 1;
786        expected.assert_current();
787
788        drop(obj);
789        expected.release += 1;
790        expected.drop += 1;
791        expected.assert_current();
792
793        let obj = unsafe { init_only_superclasses(RcIvarSubclass::alloc()) };
794        expected.alloc += 1;
795        expected.init += 1;
796        expected.assert_current();
797
798        // Accessing superclass ivars is valid
799        // SAFETY: Cell not accessed while ivar is borrowed
800        #[cfg(feature = "std")]
801        std::println!("{:?}", unsafe { &*(**obj).ivars().as_ptr() });
802
803        drop(obj);
804        expected.release += 1;
805        expected.drop += 1;
806        expected.assert_current();
807    }
808
809    #[test]
810    #[cfg_attr(not(debug_assertions), ignore = "only panics with debug assertions")]
811    #[should_panic = "tried to access uninitialized instance variable"]
812    fn access_invalid() {
813        define_class!(
814            #[unsafe(super(NSObject))]
815            // Type has to have a drop flag to detect invalid access
816            #[ivars = Retained<NSObject>]
817            struct InvalidAccess;
818        );
819
820        let obj = unsafe { init_only_superclasses(InvalidAccess::alloc()) };
821        #[cfg(feature = "std")]
822        std::println!("{:?}", obj.ivars());
823    }
824
825    #[test]
826    #[should_panic = "panic in drop"]
827    #[ignore = "panicking in Drop requires that we actually implement `dealloc` as `C-unwind`"]
828    fn test_panic_in_drop() {
829        define_class!(
830            #[unsafe(super(NSObject))]
831            struct DropPanics;
832        );
833
834        impl Drop for DropPanics {
835            fn drop(&mut self) {
836                panic!("panic in drop");
837            }
838        }
839
840        let obj = DropPanics::alloc().set_ivars(());
841        let obj: Retained<DropPanics> = unsafe { msg_send![super(obj), init] };
842        drop(obj);
843    }
844
845    #[test]
846    #[should_panic = "panic in ivar drop"]
847    #[ignore = "panicking in Drop requires that we actually implement `dealloc` as `C-unwind`"]
848    fn test_panic_in_ivar_drop() {
849        struct DropPanics;
850
851        impl Drop for DropPanics {
852            fn drop(&mut self) {
853                panic!("panic in ivar drop");
854            }
855        }
856
857        define_class!(
858            #[unsafe(super(NSObject))]
859            #[ivars = DropPanics]
860            struct IvarDropPanics;
861        );
862
863        let obj = IvarDropPanics::alloc().set_ivars(DropPanics);
864        let obj: Retained<IvarDropPanics> = unsafe { msg_send![super(obj), init] };
865        drop(obj);
866    }
867
868    // We cannot really guard against this, so dealloc must be unsafe!
869    //
870    // At least not by checking `retainCount`, since that gets set to `0` when
871    // dropping. I guess we _could_ override `retain` and check `retainCount`
872    // before we do anything, but that seems brittle, and it would hurt
873    // performance.
874    #[test]
875    fn test_retain_leak_in_drop() {
876        define_class!(
877            // SAFETY: Intentionally broken!
878            #[unsafe(super(NSObject))]
879            #[derive(Debug)]
880            struct DropRetainsAndLeaksSelf;
881        );
882
883        unsafe impl Send for DropRetainsAndLeaksSelf {}
884        unsafe impl Sync for DropRetainsAndLeaksSelf {}
885
886        static OBJ: OnceLock<Retained<DropRetainsAndLeaksSelf>> = OnceLock::new();
887
888        impl Drop for DropRetainsAndLeaksSelf {
889            fn drop(&mut self) {
890                fn inner(this: &DropRetainsAndLeaksSelf) {
891                    // Smuggle a reference out of this context.
892                    OBJ.set(this.retain()).unwrap();
893                }
894
895                inner(self)
896            }
897        }
898
899        let obj = DropRetainsAndLeaksSelf::alloc().set_ivars(());
900        let obj: Retained<DropRetainsAndLeaksSelf> = unsafe { msg_send![super(obj), init] };
901        drop(obj);
902
903        // Suddenly, the object is alive again!
904        let _ = OBJ.get().unwrap();
905    }
906}