Skip to content

Commit f5f28d9

Browse files
committed
Handle NULL errors
Fixes #653.
1 parent 45f153b commit f5f28d9

20 files changed

+729
-978
lines changed

crates/objc2/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
233233
automatically implementing the auto traits `Send` and `Sync`.
234234
* **BREAKING**: Fixed the signature of `NSObjectProtocol::isEqual` to take a
235235
nullable argument.
236+
* Fixed handling of methods that return NULL errors. This affected for example
237+
`-[MTLBinaryArchive serializeToURL:error:]`.
236238

237239

238240
## 0.5.2 - 2024-05-21

crates/objc2/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ objc2-foundation = { path = "../../framework-crates/objc2-foundation", default-f
135135
"NSDate",
136136
"NSDictionary",
137137
"NSEnumerator",
138+
"NSError",
138139
"NSKeyValueObserving",
139140
"NSNotification",
140141
"NSObject",

crates/objc2/src/__macro_helpers/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod method_family;
2525
mod module_info;
2626
mod msg_send;
2727
mod msg_send_retained;
28+
mod null_error;
2829
mod os_version;
2930
mod sync_unsafe_cell;
3031
mod writeback;

crates/objc2/src/__macro_helpers/msg_send.rs

+6-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::rc::Retained;
66
use crate::runtime::{AnyClass, AnyObject, MessageReceiver, Sel};
77
use crate::{ClassType, Encode, Message};
88

9+
use super::null_error::encountered_error;
910
use super::{ConvertArguments, ConvertReturn, TupleExtender};
1011

1112
pub trait MsgSend: Sized {
@@ -83,7 +84,7 @@ pub trait MsgSend: Sized {
8384
*mut *mut E: Encode,
8485
A: TupleExtender<*mut *mut E>,
8586
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
86-
E: Message,
87+
E: ClassType,
8788
{
8889
let mut err: *mut E = ptr::null_mut();
8990
let args = args.add_argument(&mut err);
@@ -107,7 +108,7 @@ pub trait MsgSend: Sized {
107108
*mut *mut E: Encode,
108109
A: TupleExtender<*mut *mut E>,
109110
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
110-
E: Message,
111+
E: ClassType,
111112
{
112113
let mut err: *mut E = ptr::null_mut();
113114
let args = args.add_argument(&mut err);
@@ -132,7 +133,7 @@ pub trait MsgSend: Sized {
132133
*mut *mut E: Encode,
133134
A: TupleExtender<*mut *mut E>,
134135
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
135-
E: Message,
136+
E: ClassType,
136137
{
137138
let mut err: *mut E = ptr::null_mut();
138139
let args = args.add_argument(&mut err);
@@ -145,14 +146,6 @@ pub trait MsgSend: Sized {
145146
}
146147
}
147148

148-
#[cold]
149-
#[track_caller]
150-
unsafe fn encountered_error<E: Message>(err: *mut E) -> Retained<E> {
151-
// SAFETY: Ensured by caller
152-
unsafe { Retained::retain(err) }
153-
.expect("error parameter should be set if the method returns NO")
154-
}
155-
156149
impl<T: MessageReceiver> MsgSend for T {
157150
type Inner = T::__Inner;
158151

@@ -200,7 +193,7 @@ mod tests {
200193
macro_rules! test_error_bool {
201194
($expected:expr, $($obj:tt)*) => {
202195
// Succeeds
203-
let res: Result<(), Retained<RcTestObject>> = unsafe {
196+
let res: Result<(), Retained<NSObject>> = unsafe {
204197
msg_send![$($obj)*, boolAndShouldError: false, error: _]
205198
};
206199
assert_eq!(res, Ok(()));
@@ -209,7 +202,7 @@ mod tests {
209202
// Errors
210203
let res = autoreleasepool(|_pool| {
211204
// `Ok` type is inferred to be `()`
212-
let res: Retained<RcTestObject> = unsafe {
205+
let res: Retained<NSObject> = unsafe {
213206
msg_send![$($obj)*, boolAndShouldError: true, error: _]
214207
}.expect_err("not err");
215208
$expected.alloc += 1;

crates/objc2/src/__macro_helpers/msg_send_retained.rs

+6-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::runtime::{AnyClass, AnyObject, Sel};
66
use crate::{sel, ClassType, DefinedClass, Message};
77

88
use super::defined_ivars::set_finalized;
9+
use super::null_error::encountered_error;
910
use super::{Alloc, ConvertArguments, Copy, Init, MsgSend, MutableCopy, New, Other, TupleExtender};
1011

1112
pub trait MsgSendRetained<T, U> {
@@ -29,7 +30,7 @@ pub trait MsgSendRetained<T, U> {
2930
*mut *mut E: Encode,
3031
A: TupleExtender<*mut *mut E>,
3132
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
32-
E: Message,
33+
E: ClassType,
3334
Option<R>: MaybeUnwrap<Input = U>,
3435
{
3536
let mut err: *mut E = ptr::null_mut();
@@ -111,7 +112,7 @@ pub trait MsgSendSuperRetained<T, U> {
111112
*mut *mut E: Encode,
112113
A: TupleExtender<*mut *mut E>,
113114
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
114-
E: Message,
115+
E: ClassType,
115116
Option<R>: MaybeUnwrap<Input = U>,
116117
{
117118
let mut err: *mut E = ptr::null_mut();
@@ -140,7 +141,7 @@ pub trait MsgSendSuperRetained<T, U> {
140141
*mut *mut E: Encode,
141142
A: TupleExtender<*mut *mut E>,
142143
<A as TupleExtender<*mut *mut E>>::PlusOneArgument: ConvertArguments,
143-
E: Message,
144+
E: ClassType,
144145
Option<R>: MaybeUnwrap<Input = U>,
145146
{
146147
let mut err: *mut E = ptr::null_mut();
@@ -156,16 +157,6 @@ pub trait MsgSendSuperRetained<T, U> {
156157
}
157158
}
158159

159-
// Marked `cold` to tell the optimizer that errors are comparatively rare.
160-
// And intentionally not inlined, for much the same reason.
161-
#[cold]
162-
#[track_caller]
163-
unsafe fn encountered_error<E: Message>(err: *mut E) -> Retained<E> {
164-
// SAFETY: Ensured by caller
165-
unsafe { Retained::retain(err) }
166-
.expect("error parameter should be set if the method returns NULL")
167-
}
168-
169160
impl<T: MsgSend, U: ?Sized + Message> MsgSendRetained<T, Option<Retained<U>>> for New {
170161
#[inline]
171162
unsafe fn send_message_retained<
@@ -1032,7 +1023,7 @@ mod tests {
10321023
($expected:expr, $if_autorelease_not_skipped:expr, $sel:ident, $($obj:tt)*) => {
10331024
// Succeeds
10341025
let res = autoreleasepool(|_pool| {
1035-
let res: Result<Retained<RcTestObject>, Retained<RcTestObject>> = unsafe {
1026+
let res: Result<Retained<RcTestObject>, Retained<NSObject>> = unsafe {
10361027
msg_send_id![$($obj)*, $sel: false, error: _]
10371028
};
10381029
let res = res.expect("not ok");
@@ -1053,7 +1044,7 @@ mod tests {
10531044

10541045
// Errors
10551046
let res = autoreleasepool(|_pool| {
1056-
let res: Result<Retained<RcTestObject>, Retained<RcTestObject>> = unsafe {
1047+
let res: Result<Retained<RcTestObject>, Retained<NSObject>> = unsafe {
10571048
msg_send_id![$($obj)*, $sel: true, error: _]
10581049
};
10591050
$expected.alloc += 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use core::ffi::CStr;
2+
use std::sync::OnceLock;
3+
4+
use crate::ffi::NSInteger;
5+
use crate::rc::{autoreleasepool, Retained};
6+
use crate::runtime::{AnyClass, NSObject};
7+
use crate::{msg_send_id, ClassType};
8+
9+
// Marked `#[cold]` to tell the optimizer that errors are comparatively rare.
10+
//
11+
// And intentionally not `#[inline]`, we'll let the optimizer figure out if it
12+
// wants to do that or not.
13+
#[cold]
14+
pub(crate) unsafe fn encountered_error<E: ClassType>(err: *mut E) -> Retained<E> {
15+
// SAFETY: Caller ensures that the pointer is valid.
16+
unsafe { Retained::retain(err) }.unwrap_or_else(|| {
17+
let err = null_error();
18+
assert!(E::IS_NSERROR_COMPATIBLE);
19+
// SAFETY: Just checked (via `const` assertion) that the `E` type is
20+
// either `NSError` or `NSObject`, and hence it is valid to cast the
21+
// `NSObject` that we have here to that.
22+
unsafe { Retained::cast_unchecked(err) }
23+
})
24+
}
25+
26+
/// Poor mans string equality in `const`. Implements `a == b`.
27+
const fn is_eq(a: &str, b: &str) -> bool {
28+
let a = a.as_bytes();
29+
let b = b.as_bytes();
30+
31+
if a.len() != b.len() {
32+
return false;
33+
}
34+
35+
let mut i = 0;
36+
while i < a.len() {
37+
if a[i] != b[i] {
38+
return false;
39+
}
40+
i += 1;
41+
}
42+
43+
true
44+
}
45+
46+
// TODO: Use inline `const` once in MSRV (or add proper trait bounds).
47+
trait IsNSError {
48+
const IS_NSERROR_COMPATIBLE: bool;
49+
}
50+
51+
impl<T: ClassType> IsNSError for T {
52+
const IS_NSERROR_COMPATIBLE: bool = {
53+
if is_eq(T::NAME, "NSError") || is_eq(T::NAME, "NSObject") {
54+
true
55+
} else {
56+
// The post monomorphization error here is not nice, but it's
57+
// better than UB because the user used a type that cannot be
58+
// converted to NSError.
59+
//
60+
// TODO: Add a trait bound or similar instead.
61+
panic!("error parameter must be either `NSError` or `NSObject`")
62+
}
63+
};
64+
}
65+
66+
#[cold] // Mark the NULL error branch as cold
67+
fn null_error() -> Retained<NSObject> {
68+
static CACHED_NULL_ERROR: OnceLock<NSErrorWrapper> = OnceLock::new();
69+
70+
// We use a OnceLock here, since performance doesn't really matter, and
71+
// using an AtomicPtr would leak under (very) high initialization
72+
// contention.
73+
CACHED_NULL_ERROR.get_or_init(create_null_error).0.clone()
74+
}
75+
76+
struct NSErrorWrapper(Retained<NSObject>);
77+
78+
// SAFETY: NSError is immutable and thread safe.
79+
unsafe impl Send for NSErrorWrapper {}
80+
unsafe impl Sync for NSErrorWrapper {}
81+
82+
#[cold] // Mark the error creation branch as cold
83+
fn create_null_error() -> NSErrorWrapper {
84+
// Wrap creation in an autoreleasepool, since we don't know anything about
85+
// the outside world, and we don't want to appear to leak.
86+
autoreleasepool(|_| {
87+
// TODO: Replace with c string literals once in MSRV.
88+
89+
// SAFETY: The string is NUL terminated.
90+
let cls = unsafe { CStr::from_bytes_with_nul_unchecked(b"NSString\0") };
91+
// Intentional dynamic lookup, we don't know if Foundation is linked.
92+
let cls = AnyClass::get(cls).unwrap_or_else(foundation_not_linked);
93+
94+
// SAFETY: The string is NUL terminated.
95+
let domain = unsafe { CStr::from_bytes_with_nul_unchecked(b"__objc2.missingError\0") };
96+
// SAFETY: The signate is correct, and the string is UTF-8 encoded and
97+
// NUL terminated.
98+
let domain: Retained<NSObject> =
99+
unsafe { msg_send_id![cls, stringWithUTF8String: domain.as_ptr()] };
100+
101+
// SAFETY: The string is valid.
102+
let cls = unsafe { CStr::from_bytes_with_nul_unchecked(b"NSError\0") };
103+
// Intentional dynamic lookup, we don't know if Foundation is linked.
104+
let cls = AnyClass::get(cls).unwrap_or_else(foundation_not_linked);
105+
106+
let domain: &NSObject = &domain;
107+
let code: NSInteger = 0;
108+
let user_info: Option<&NSObject> = None;
109+
// SAFETY: The signate is correct.
110+
let err: Retained<NSObject> =
111+
unsafe { msg_send_id![cls, errorWithDomain: domain, code: code, userInfo: user_info] };
112+
NSErrorWrapper(err)
113+
})
114+
}
115+
116+
fn foundation_not_linked() -> &'static AnyClass {
117+
panic!("Foundation must be linked to get a proper error message on NULL errors")
118+
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn test_is_eq() {
126+
assert!(is_eq("NSError", "NSError"));
127+
assert!(!is_eq("nserror", "NSError"));
128+
assert!(!is_eq("CFError", "NSError"));
129+
assert!(!is_eq("NSErr", "NSError"));
130+
assert!(!is_eq("NSErrorrrr", "NSError"));
131+
}
132+
133+
#[test]
134+
fn test_create() {
135+
let _ = create_null_error().0;
136+
}
137+
}

crates/objc2/src/macros/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,8 @@ macro_rules! __class_inner {
820820
///
821821
/// In particular, if you make the last argument the special marker `_`, then
822822
/// the macro will return a `Result<(), Retained<E>>` (where you must specify
823-
/// `E` yourself, usually you'd use `objc2_foundation::NSError`).
823+
/// `E` yourself, and where `E` must be either [`NSObject`] or
824+
/// `objc2_foundation::NSError`).
824825
///
825826
/// At runtime, we create the temporary error variable for you on the stack
826827
/// and send it as the out-parameter to the method. If the method then returns
@@ -832,6 +833,7 @@ macro_rules! __class_inner {
832833
///
833834
/// [cocoa-error]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorHandling/ErrorHandling.html
834835
/// [swift-error]: https://developer.apple.com/documentation/swift/about-imported-cocoa-error-parameters
836+
/// [`NSObject`]: crate::runtime::NSObject
835837
///
836838
///
837839
/// # Panics

0 commit comments

Comments
 (0)