use core::mem::ManuallyDrop;
use core::ptr::NonNull;
use super::ConvertArgument;
use crate::rc::Retained;
use crate::Message;
impl<T: Message + 'static> ConvertArgument for &mut Retained<T> {
type __Inner = NonNull<*mut T>;
type __WritebackOnDrop = WritebackOnDrop<T>;
#[inline]
fn __from_defined_param(_inner: Self::__Inner) -> Self {
todo!("`&mut Retained<_>` is not supported in `define_class!` yet")
}
#[inline]
unsafe fn __into_argument(self) -> (Self::__Inner, Self::__WritebackOnDrop) {
let ptr: NonNull<Retained<T>> = NonNull::from(self);
let ptr: NonNull<NonNull<T>> = ptr.cast();
let old: NonNull<T> = unsafe { *ptr.as_ptr() };
let ptr: NonNull<*mut T> = ptr.cast();
(ptr, WritebackOnDrop { ptr, old })
}
}
#[derive(Debug)]
pub struct WritebackOnDrop<T: Message> {
ptr: NonNull<*mut T>,
old: NonNull<T>,
}
impl<T: Message> Drop for WritebackOnDrop<T> {
#[inline]
fn drop(&mut self) {
let new: Option<Retained<T>> = unsafe { Retained::retain(*self.ptr.as_ptr()) };
let _new = ManuallyDrop::new(new);
#[cfg(debug_assertions)]
if _new.is_none() {
panic!("found that NULL was written to `&mut Retained<_>`, which is UB! You should handle this with `&mut Option<Retained<_>>` instead");
}
let _: Retained<T> = unsafe { Retained::new_nonnull(self.old) };
}
}
impl<T: Message + 'static> ConvertArgument for &mut Option<Retained<T>> {
type __Inner = NonNull<*mut T>;
type __WritebackOnDrop = WritebackOnDropNullable<T>;
#[inline]
fn __from_defined_param(_inner: Self::__Inner) -> Self {
todo!("`&mut Option<Retained<_>>` is not supported in `define_class!` yet")
}
#[inline]
unsafe fn __into_argument(self) -> (Self::__Inner, Self::__WritebackOnDrop) {
let ptr: NonNull<Option<Retained<T>>> = NonNull::from(self);
let ptr: NonNull<*mut T> = ptr.cast();
let old: *mut T = unsafe { *ptr.as_ptr() };
(ptr, WritebackOnDropNullable { ptr, old })
}
}
#[derive(Debug)]
pub struct WritebackOnDropNullable<T: Message> {
ptr: NonNull<*mut T>,
old: *mut T,
}
impl<T: Message> Drop for WritebackOnDropNullable<T> {
#[inline]
fn drop(&mut self) {
let new: Option<Retained<T>> = unsafe { Retained::retain(*self.ptr.as_ptr()) };
let _ = ManuallyDrop::new(new);
let _: Option<Retained<T>> = unsafe { Retained::from_raw(self.old) };
}
}
impl<T: Message + 'static> ConvertArgument for Option<&mut Retained<T>> {
type __Inner = Option<NonNull<*mut T>>;
type __WritebackOnDrop = Option<WritebackOnDrop<T>>;
#[inline]
fn __from_defined_param(_inner: Self::__Inner) -> Self {
todo!("`Option<&mut Retained<_>>` is not supported in `define_class!` yet")
}
#[inline]
unsafe fn __into_argument(self) -> (Self::__Inner, Self::__WritebackOnDrop) {
if let Some(this) = self {
let (ptr, helper) = unsafe { this.__into_argument() };
(Some(ptr), Some(helper))
} else {
(None, None)
}
}
}
impl<T: Message + 'static> ConvertArgument for Option<&mut Option<Retained<T>>> {
type __Inner = Option<NonNull<*mut T>>;
type __WritebackOnDrop = Option<WritebackOnDropNullable<T>>;
#[inline]
fn __from_defined_param(_inner: Self::__Inner) -> Self {
todo!("`Option<&mut Option<Retained<_>>>` is not supported in `define_class!` yet")
}
#[inline]
unsafe fn __into_argument(self) -> (Self::__Inner, Self::__WritebackOnDrop) {
if let Some(this) = self {
let (ptr, stored) = unsafe { this.__into_argument() };
(Some(ptr), Some(stored))
} else {
(None, None)
}
}
}
#[cfg(test)]
mod tests {
use std::panic::{catch_unwind, AssertUnwindSafe};
use super::*;
use crate::rc::{autoreleasepool, Allocated, RcTestObject, ThreadTestData};
use crate::{msg_send, ClassType};
#[test]
fn test_bool_error() {
let mut expected = ThreadTestData::current();
fn bool_error(should_error: bool, error: Option<&mut Option<Retained<RcTestObject>>>) {
let cls = RcTestObject::class();
let did_succeed: bool =
unsafe { msg_send![cls, boolAndShouldError: should_error, error: error] };
assert_ne!(should_error, did_succeed);
}
bool_error(false, None);
bool_error(true, None);
expected.assert_current();
fn helper(
expected: &mut ThreadTestData,
should_error: bool,
mut error: Option<Retained<RcTestObject>>,
) {
autoreleasepool(|_| {
bool_error(should_error, Some(&mut error));
if should_error {
expected.alloc += 1;
expected.init += 1;
expected.autorelease += 1;
}
expected.assert_current();
});
if should_error {
expected.release += 1;
}
expected.assert_current();
if error.is_some() {
expected.release += 1;
expected.drop += 1;
}
drop(error);
expected.assert_current();
}
helper(&mut expected, false, None);
expected.retain += 1;
helper(&mut expected, true, None);
expected.alloc += 1;
expected.init += 1;
expected.retain += 1;
expected.release += 1;
helper(&mut expected, false, Some(RcTestObject::new()));
expected.alloc += 1;
expected.init += 1;
expected.retain += 1;
expected.release += 1;
expected.drop += 1;
helper(&mut expected, true, Some(RcTestObject::new()));
}
#[test]
#[cfg_attr(
any(
not(debug_assertions),
all(not(target_pointer_width = "64"), feature = "catch-all")
),
ignore = "invokes UB which is only caught with debug_assertions"
)]
#[should_panic = "found that NULL was written to `&mut Retained<_>`, which is UB! You should handle this with `&mut Option<Retained<_>>` instead"]
fn test_debug_check_ub() {
let cls = RcTestObject::class();
let mut param: Retained<_> = RcTestObject::new();
let _: () = unsafe { msg_send![cls, outParamNull: &mut param] };
}
const AUTORELEASE_SKIPPED: bool = cfg!(feature = "gnustep-1-7");
#[test]
fn test_retained_interaction() {
let mut expected = ThreadTestData::current();
let cls = RcTestObject::class();
let mut err: Retained<RcTestObject> = RcTestObject::new();
expected.alloc += 1;
expected.init += 1;
expected.assert_current();
autoreleasepool(|_| {
let obj: Option<Retained<RcTestObject>> =
unsafe { msg_send![cls, idAndShouldError: false, error: &mut err] };
expected.alloc += 1;
expected.init += 1;
if !AUTORELEASE_SKIPPED {
expected.autorelease += 1;
expected.retain += 1;
}
expected.retain += 1;
expected.release += 1;
expected.assert_current();
drop(obj);
expected.release += 1;
if AUTORELEASE_SKIPPED {
expected.drop += 1;
}
expected.assert_current();
});
if !AUTORELEASE_SKIPPED {
expected.release += 1;
expected.drop += 1;
}
expected.assert_current();
drop(err);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
}
#[test]
fn test_error_alloc() {
let mut expected = ThreadTestData::current();
let mut error: Option<Retained<RcTestObject>> = None;
let res: Allocated<RcTestObject> = unsafe {
msg_send![RcTestObject::class(), allocAndShouldError: false, error: &mut error]
};
expected.alloc += 1;
expected.assert_current();
assert!(!Allocated::as_ptr(&res).is_null());
assert!(error.is_none());
drop(res);
expected.release += 1;
expected.assert_current();
let res: Retained<RcTestObject> = autoreleasepool(|_pool| {
let mut error = None;
let res: Allocated<RcTestObject> = unsafe {
msg_send![RcTestObject::class(), allocAndShouldError: true, error: &mut error]
};
expected.alloc += 1;
expected.init += 1;
expected.autorelease += 1;
expected.retain += 1;
expected.assert_current();
assert!(Allocated::as_ptr(&res).is_null());
error.unwrap()
});
expected.release += 1;
expected.assert_current();
drop(res);
expected.release += 1;
expected.drop += 1;
expected.assert_current();
}
fn will_panic(param: Option<&mut Option<Retained<RcTestObject>>>, panic_after: bool) {
unsafe { msg_send![RcTestObject::class(), willPanicWith: param, panicsAfter: panic_after] }
}
#[test]
#[cfg_attr(
feature = "catch-all",
ignore = "panics intentionally, which catch-all interferes with"
)]
fn basic_method_panics() {
let expected = ThreadTestData::current();
let res = catch_unwind(|| {
will_panic(None, false);
});
assert!(res.is_err());
expected.assert_current();
let res = catch_unwind(|| {
will_panic(None, true);
});
assert!(res.is_err());
expected.assert_current();
}
#[test]
#[cfg_attr(
any(feature = "catch-all", panic = "abort"),
ignore = "panics intentionally"
)]
fn method_panics() {
let cases = [
(false, None),
(true, None),
(false, Some(RcTestObject::new())),
(true, Some(RcTestObject::new())),
];
let mut expected = ThreadTestData::current();
for (panic_after, mut param) in cases {
let initially_set = param.is_some();
autoreleasepool(|_| {
let unwindsafe = AssertUnwindSafe(&mut param);
let res = catch_unwind(|| {
let param = unwindsafe;
will_panic(Some(param.0), panic_after);
});
assert!(res.is_err());
if panic_after {
expected.alloc += 1;
expected.init += 1;
expected.autorelease += 1;
}
if panic_after || initially_set {
expected.retain += 1;
}
if initially_set {
expected.release += 1;
if panic_after {
expected.drop += 1;
}
}
expected.assert_current();
});
if panic_after {
expected.release += 1;
}
expected.assert_current();
drop(param);
if panic_after || initially_set {
expected.release += 1;
expected.drop += 1;
}
expected.assert_current();
}
}
}