Skip to content

Commit dd6c804

Browse files
authored
Merge pull request #441 from madsmtm/improve-objc-sys
Fix memory leaks in exception handling
2 parents 032809f + 475e7cc commit dd6c804

File tree

10 files changed

+133
-63
lines changed

10 files changed

+133
-63
lines changed

crates/objc-sys/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## Unreleased - YYYY-MM-DD
88

9+
### Added
10+
* Improved documentation slightly.
11+
912

1013
## 0.3.0 - 2023-02-07
1114

crates/objc-sys/build.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,7 @@ fn main() {
210210
// - ...
211211
//
212212
// TODO: -fobjc-weak ?
213-
let mut cc_args = format!(
214-
"-fobjc-arc -fobjc-arc-exceptions -fobjc-exceptions -fobjc-runtime={clang_runtime}"
215-
);
213+
let mut cc_args = format!("-fobjc-exceptions -fobjc-runtime={clang_runtime}");
216214

217215
if let Runtime::ObjFW(_) = &runtime {
218216
// Add compability headers to make `#include <objc/objc.h>` work.
@@ -221,8 +219,6 @@ fn main() {
221219
cc_args.push_str(compat_headers.to_str().unwrap());
222220
}
223221

224-
println!("cargo:cc_args={cc_args}"); // DEP_OBJC_[version]_CC_ARGS
225-
226222
if let Runtime::ObjFW(_) = &runtime {
227223
// Link to libobjfw-rt
228224
println!("cargo:rustc-link-lib=dylib=objfw-rt");
@@ -250,10 +246,18 @@ fn main() {
250246
let mut builder = cc::Build::new();
251247
builder.file("extern/exception.m");
252248

249+
// Compile with exceptions enabled and with the correct runtime, but
250+
// _without ARC_!
253251
for flag in cc_args.split(' ') {
254252
builder.flag(flag);
255253
}
256254

257255
builder.compile("librust_objc_sys_0_3_try_catch_exception.a");
258256
}
257+
258+
// Add this to the `CC` args _after_ we've omitted it when compiling
259+
// `extern/exception.m`.
260+
cc_args.push_str(" -fobjc-arc -fobjc-arc-exceptions");
261+
262+
println!("cargo:cc_args={cc_args}"); // DEP_OBJC_[version]_CC_ARGS
259263
}

crates/objc-sys/extern/exception.m

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@
1212
unsigned char rust_objc_sys_0_3_try_catch_exception(void (*f)(void *), void *context, id *error) {
1313
@try {
1414
f(context);
15-
if (error) {
16-
*error = (id)0; // nil
17-
}
1815
return 0;
1916
} @catch (id exception) {
20-
if (error) {
21-
*error = objc_retain(exception);
22-
}
17+
// The exception is retained while inside the `@catch` block, but is
18+
// not guaranteed to be so outside of it; so hence we must do it here!
19+
*error = objc_retain(exception);
2320
return 1;
2421
}
2522
}

crates/objc-sys/src/constants.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,32 @@ pub const nil: id = 0 as *mut _;
1919
/// A quick alias for a [`null_mut`][`core::ptr::null_mut`] class.
2020
pub const Nil: *mut objc_class = 0 as *mut _;
2121

22+
/// Policies related to associative references.
23+
///
24+
/// These are options to [`objc_setAssociatedObject`].
25+
///
26+
/// [`objc_setAssociatedObject`]: crate::objc_setAssociatedObject
2227
pub type objc_AssociationPolicy = usize;
28+
/// Specifies a weak reference to the associated object.
29+
///
30+
/// This performs straight assignment, without message sends.
2331
pub const OBJC_ASSOCIATION_ASSIGN: objc_AssociationPolicy = 0;
32+
/// Specifies a strong reference to the associated object.
33+
///
34+
/// The association is not made atomically.
2435
pub const OBJC_ASSOCIATION_RETAIN_NONATOMIC: objc_AssociationPolicy = 1;
36+
/// Specifies that the associated object is copied.
37+
///
38+
/// The association is not made atomically.
2539
pub const OBJC_ASSOCIATION_COPY_NONATOMIC: objc_AssociationPolicy = 3;
26-
pub const OBJC_ASSOCIATION_RETAIN: objc_AssociationPolicy = 769;
27-
pub const OBJC_ASSOCIATION_COPY: objc_AssociationPolicy = 771;
40+
/// Specifies a strong reference to the associated object.
41+
///
42+
/// The association is made atomically.
43+
pub const OBJC_ASSOCIATION_RETAIN: objc_AssociationPolicy = 0o1401;
44+
/// Specifies that the associated object is copied.
45+
///
46+
/// The association is made atomically.
47+
pub const OBJC_ASSOCIATION_COPY: objc_AssociationPolicy = 0o1403;
2848

2949
#[cfg(any(doc, apple))]
3050
pub const OBJC_SYNC_SUCCESS: c_int = 0;
@@ -36,3 +56,17 @@ pub const OBJC_SYNC_TIMED_OUT: c_int = -2;
3656
/// Only relevant before macOS 10.13
3757
#[cfg(any(doc, apple))]
3858
pub const OBJC_SYNC_NOT_INITIALIZED: c_int = -3;
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::*;
63+
#[test]
64+
fn test_association_policy() {
65+
assert_eq!(OBJC_ASSOCIATION_RETAIN, 769);
66+
assert_eq!(OBJC_ASSOCIATION_COPY, 771);
67+
68+
// What the GNUStep headers define
69+
assert_eq!(OBJC_ASSOCIATION_RETAIN, 0x301);
70+
assert_eq!(OBJC_ASSOCIATION_COPY, 0x303);
71+
}
72+
}

crates/objc-sys/src/image_info.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// TODO: Move this to `objc2` once we can detect simulator targets without a
2+
// build script.
3+
14
/// Note: While `objc2` relies on this, you can freely break this, since it is
25
/// only used behind experimental features (`unstable-static-*`).
36
#[repr(C)]

crates/objc-sys/src/protocol.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ extern_c! {
3030
#[cfg(any(doc, not(objfw)))]
3131
pub fn objc_registerProtocol(proto: *mut objc_protocol);
3232

33-
// TODO: Verify unwinding
3433
pub fn protocol_conformsToProtocol(
3534
proto: *const objc_protocol,
3635
other: *const objc_protocol,
3736
) -> BOOL;
38-
// TODO: Verify unwinding
3937
pub fn protocol_isEqual(proto: *const objc_protocol, other: *const objc_protocol) -> BOOL;
4038
pub fn protocol_getName(proto: *const objc_protocol) -> *const c_char;
4139

crates/objc-sys/src/types.rs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,16 @@ pub type BOOL = inner::BOOL;
105105
//
106106
// Likewise for NSUInteger.
107107
//
108+
//
108109
// ## GNUStep / WinObjC
109110
//
110111
// Defined as intptr_t/uintptr_t, which is exactly the same as isize/usize.
111112
//
113+
//
112114
// ## ObjFW
113115
//
114-
// Doesn't define these, but e.g. `OFString -length` returns size_t, so our
115-
// definitions are should be correct on effectively all targets.
116+
// Doesn't define these, but e.g. -[OFString length] returns size_t, so our
117+
// definitions should be correct on effectively all targets.
116118
//
117119
// Things might change slightly in the future, see
118120
// <https://internals.rust-lang.org/t/pre-rfc-usize-is-not-size-t/15369>.
@@ -127,15 +129,23 @@ pub type BOOL = inner::BOOL;
127129
///
128130
/// [docs]: https://developer.apple.com/documentation/objectivec/nsinteger?language=objc
129131
///
132+
///
130133
/// # Examples
131134
///
132135
/// ```
133-
/// #[repr(isize)] // NSInteger
136+
/// use core::mem::size_of;
137+
/// # use objc_sys::NSInteger;
138+
/// # #[cfg(not_available)]
139+
/// use objc2::ffi::NSInteger;
140+
///
141+
/// #[repr(isize)]
134142
/// pub enum NSComparisonResult {
135143
/// NSOrderedAscending = -1,
136144
/// NSOrderedSame = 0,
137145
/// NSOrderedDescending = 1,
138146
/// }
147+
///
148+
/// assert_eq!(size_of::<NSComparisonResult>(), size_of::<NSInteger>());
139149
/// ```
140150
pub type NSInteger = isize;
141151

@@ -149,36 +159,43 @@ pub type NSInteger = isize;
149159
///
150160
/// [docs]: https://developer.apple.com/documentation/objectivec/nsuinteger?language=objc
151161
///
162+
///
152163
/// # Examples
153164
///
154165
/// ```
155-
/// use objc_sys::NSUInteger;
156-
/// // Or:
157-
/// // use objc2::ffi::NSUInteger;
158-
/// // use icrate::Foundation::NSUInteger;
166+
/// # use objc_sys::NSUInteger;
167+
/// # #[cfg(not_available)]
168+
/// use objc2::ffi::NSUInteger;
169+
///
159170
/// extern "C" {
160171
/// fn some_external_function() -> NSUInteger;
161172
/// }
162173
/// ```
163174
///
164175
/// ```
165-
/// #[repr(usize)] // NSUInteger
166-
/// enum NSRoundingMode {
167-
/// NSRoundPlain = 0,
168-
/// NSRoundDown = 1,
169-
/// NSRoundUp = 2,
170-
/// NSRoundBankers = 3,
171-
/// };
176+
/// use core::mem::size_of;
177+
/// # use objc_sys::NSUInteger;
178+
/// # #[cfg(not_available)]
179+
/// use objc2::ffi::NSUInteger;
180+
///
181+
/// #[repr(usize)]
182+
/// enum CLRegionState {
183+
/// Unknown = 0,
184+
/// Inside = 1,
185+
/// Outside = 2,
186+
/// }
187+
///
188+
/// assert_eq!(size_of::<CLRegionState>(), size_of::<NSUInteger>());
172189
/// ```
173190
pub type NSUInteger = usize;
174191

175-
/// The maximum value for an NSInteger.
192+
/// The maximum value for a [`NSInteger`].
176193
pub const NSIntegerMax: NSInteger = NSInteger::MAX;
177194

178-
/// The minimum value for an NSInteger.
195+
/// The minimum value for a [`NSInteger`].
179196
pub const NSIntegerMin: NSInteger = NSInteger::MIN;
180197

181-
/// The maximum value for an NSUInteger.
198+
/// The maximum value for a [`NSUInteger`].
182199
pub const NSUIntegerMax: NSUInteger = NSUInteger::MAX;
183200

184201
/// An immutable pointer to a selector.
@@ -189,7 +206,9 @@ pub type SEL = *const objc_selector;
189206

190207
/// A mutable pointer to an object / instance.
191208
///
192-
/// Type alias provided for convenience. See `objc2::runtime::Object` for a
193-
/// higher level binding, and `objc2::rc::Id` for an easier way of handling
194-
/// objects.
209+
/// Type alias provided for convenience. You'll likely want to use one of:
210+
/// - `icrate::Foundation::NS[...]` for when you know the class of the object
211+
/// you're dealing with.
212+
/// - `objc2::rc::Id` for a proper way of doing memory management.
213+
/// - `objc2::runtime::Object` for a bit safer representation of this.
195214
pub type id = *mut objc_object;

crates/objc2/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6868

6969
### Fixed
7070
* Fixed using autorelease pools on 32bit macOS and older macOS versions.
71+
* Fixed memory leaks in and improved performance of `exception::catch`.
7172

7273
### Removed
7374
* **BREAKING**: Removed `rc::SliceId`, since it is trivially implementable

crates/objc2/src/exception.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Id<Exception>
237237
// SAFETY:
238238
// The exception is always a valid object or NULL.
239239
//
240+
// Since we do a retain inside `extern/exception.m`, the object has
241+
// +1 retain count.
242+
//
240243
// Code throwing an exception know that they don't hold sole access to
241244
// that object any more, so even if the type was originally mutable,
242245
// it is okay to create a new `Id` to it here.

crates/tests/src/exception.rs

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,39 @@ fn throw_catch_raise_catch() {
2222
let reason = NSString::from_str("def");
2323

2424
let exc = NSException::new(&name, Some(&reason), None).unwrap();
25-
// TODO: Investigate why this is required on GNUStep!
26-
let _exc2 = exc.clone();
2725

28-
assert_retain_count(&exc, 2);
29-
30-
// TODO: Investigate this!
31-
let extra_retain = usize::from(cfg!(all(
32-
feature = "apple",
33-
target_os = "macos",
34-
target_arch = "x86"
35-
)));
26+
assert_retain_count(&exc, 1);
3627

3728
let exc = autoreleasepool(|_| {
3829
let exc = NSException::into_exception(exc);
3930
let res = unsafe { catch(|| throw(exc)) };
4031
let exc = res.unwrap_err().unwrap();
4132
let exc = NSException::from_exception(exc).unwrap();
4233

43-
assert_retain_count(&exc, 3 + extra_retain);
34+
assert_retain_count(&exc, 1);
35+
exc
36+
});
37+
38+
assert_retain_count(&exc, 1);
39+
40+
let exc = autoreleasepool(|_| {
41+
let inner = || {
42+
autoreleasepool(|pool| {
43+
let exc = Id::autorelease(exc, pool);
44+
unsafe { exc.raise() }
45+
})
46+
};
47+
48+
let res = unsafe { catch(inner) };
49+
let exc = NSException::from_exception(res.unwrap_err().unwrap()).unwrap();
50+
51+
// Undesired: The inner pool _should_ have been drained on unwind, but
52+
// it isn't, see `rc::Pool::drain`.
53+
assert_retain_count(&exc, 2);
4454
exc
4555
});
4656

47-
assert_retain_count(&exc, 2 + extra_retain);
57+
assert_retain_count(&exc, 1);
4858

4959
assert_eq!(exc.name(), name);
5060
assert_eq!(exc.reason().unwrap(), reason);
@@ -85,28 +95,26 @@ fn raise_catch() {
8595
let exc = NSException::new(&name, Some(&reason), None).unwrap();
8696
assert_retain_count(&exc, 1);
8797

88-
let res = autoreleasepool(|_| {
89-
let res = unsafe {
90-
catch(|| {
91-
if exc.name() == name {
92-
exc.raise();
93-
} else {
94-
4
95-
}
96-
})
98+
let exc = autoreleasepool(|pool| {
99+
let exc = Id::autorelease(exc, pool);
100+
let inner = || {
101+
if exc.name() == name {
102+
unsafe { exc.raise() };
103+
} else {
104+
42
105+
}
97106
};
98-
assert_retain_count(&exc, 3);
107+
let res = unsafe { catch(inner) }.unwrap_err().unwrap();
108+
assert_retain_count(&exc, 2);
99109
res
100110
});
101111

102-
assert_retain_count(&exc, 2);
103-
104-
let obj = res.unwrap_err().unwrap();
112+
assert_retain_count(&exc, 1);
105113

106-
assert_eq!(format!("{obj}"), "def");
114+
assert_eq!(format!("{exc}"), "def");
107115
assert_eq!(
108-
format!("{obj:?}"),
109-
format!("exception <NSException: {:p}> 'abc' reason:def", &*obj)
116+
format!("{exc:?}"),
117+
format!("exception <NSException: {:p}> 'abc' reason:def", &*exc)
110118
);
111119
}
112120

0 commit comments

Comments
 (0)