Skip to content

Commit 2d39cba

Browse files
committed
Cherry-pick ae1c875 for objc2 v0.6.4
1 parent 2ace80b commit 2d39cba

File tree

13 files changed

+1451
-1075
lines changed

13 files changed

+1451
-1075
lines changed

crates/objc2/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
## Added
1010
* Added support for the unstable `darwin_objc` feature.
1111

12+
## Changed
13+
* **BREAKING** (very slightly): `define_class!` now rejects non-static and
14+
non-unique class names.
15+
16+
## Fixed
17+
* Allow classes created with `define_class!` to be used in multiple shared
18+
dynamic libraries in the same process.
19+
1220

1321
## [0.6.3] - 2025-10-04
1422
[0.6.3]: https://github.com/madsmtm/objc2/compare/objc2-0.6.2...objc2-0.6.3

crates/objc2/src/__macro_helpers/define_class.rs

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use alloc::ffi::CString;
21
#[cfg(debug_assertions)]
32
use alloc::vec::Vec;
3+
use core::ffi::CStr;
44
use core::marker::PhantomData;
55
use core::panic::{RefUnwindSafe, UnwindSafe};
66
#[cfg(debug_assertions)]
@@ -15,7 +15,10 @@ use crate::runtime::{
1515
use crate::runtime::{AnyProtocol, MethodDescription};
1616
use crate::{AnyThread, ClassType, DefinedClass, Message, ProtocolType};
1717

18-
use super::defined_ivars::{register_with_ivars, setup_dealloc};
18+
use super::defined_ivars::{
19+
drop_flag_offset, ivar_drop_flag_names, ivars_offset, register_drop_flag, register_ivars,
20+
setup_dealloc,
21+
};
1922
use super::{CopyFamily, InitFamily, MutableCopyFamily, NewFamily, NoneFamily};
2023

2124
/// Helper for determining auto traits of defined classes.
@@ -184,42 +187,132 @@ impl<T: Message> MaybeOptionRetained for Option<Retained<T>> {
184187
}
185188
}
186189

187-
#[derive(Debug)]
188-
pub struct ClassBuilderHelper<T: ?Sized> {
189-
builder: ClassBuilder,
190-
p: PhantomData<T>,
190+
/// Convert a class name with a trailing NUL byte to a `CStr`, at `const`.
191+
#[track_caller]
192+
pub const fn class_c_name(name: &str) -> &CStr {
193+
let bytes = name.as_bytes();
194+
// Workaround for `from_bytes_with_nul` not being `const` in MSRV.
195+
let mut i = 0;
196+
while i < bytes.len() - 1 {
197+
if bytes[i] == 0 {
198+
panic!("class name must not contain interior NUL bytes");
199+
}
200+
i += 1;
201+
}
202+
if let Ok(c_name) = CStr::from_bytes_until_nul(bytes) {
203+
c_name
204+
} else {
205+
unreachable!()
206+
}
191207
}
192208

193-
// Outlined for code size
209+
// Kept separate for code size.
194210
#[track_caller]
195-
fn create_builder(name: &str, superclass: &AnyClass) -> ClassBuilder {
196-
let c_name = CString::new(name).expect("class name must be UTF-8");
197-
match ClassBuilder::new(&c_name, superclass) {
198-
Some(builder) => builder,
199-
None => panic!(
200-
"could not create new class {name}. Perhaps a class with that name already exists?"
201-
),
202-
}
211+
fn class_not_present(c_name: &CStr) -> ! {
212+
panic!("could not create new class {c_name:?}, though there was no other class with that name")
203213
}
204214

205-
impl<T: DefinedClass> ClassBuilderHelper<T> {
206-
#[inline]
207-
#[track_caller]
208-
#[allow(clippy::new_without_default)]
209-
pub fn new() -> Self
210-
where
211-
T::Super: ClassType,
212-
{
213-
let mut builder = create_builder(T::NAME, <T::Super as ClassType>::class());
215+
#[track_caller]
216+
fn class_not_unique(c_name: &CStr) -> ! {
217+
panic!("could not create new class {c_name:?}, perhaps a class with that name already exists?")
218+
}
214219

215-
setup_dealloc::<T>(&mut builder);
220+
#[inline]
221+
#[track_caller]
222+
#[allow(clippy::new_without_default)]
223+
pub fn define_class<T: DefinedClass>(
224+
c_name: &CStr,
225+
name_is_auto_generated: bool,
226+
register_impls: impl FnOnce(&mut ClassBuilderHelper<T>),
227+
) -> (&'static AnyClass, isize, isize)
228+
where
229+
T::Super: ClassType,
230+
{
231+
let (ivar_name, drop_flag_name) = ivar_drop_flag_names::<T>();
216232

217-
Self {
233+
let superclass = <T::Super as ClassType>::class();
234+
let cls = if let Some(builder) = ClassBuilder::new(c_name, superclass) {
235+
let mut this = ClassBuilderHelper {
218236
builder,
219237
p: PhantomData,
238+
};
239+
240+
setup_dealloc::<T>(&mut this.builder);
241+
242+
register_impls(&mut this);
243+
244+
register_ivars::<T>(&mut this.builder, &ivar_name);
245+
register_drop_flag::<T>(&mut this.builder, &drop_flag_name);
246+
247+
this.builder.register()
248+
} else {
249+
// When loading two dynamic libraries that both use a class from some
250+
// shared (static) Rust library, the dynamic linker will duplicate the
251+
// statics that `define_class!` defines.
252+
//
253+
// For most statics that people create, this is the desired behaviour.
254+
//
255+
// In our case though, there is only a single Objective-C runtime with
256+
// a single list of classes, and thus those two dynamic libraries end
257+
// up trying to register the same class multiple times.
258+
//
259+
// To support such use-cases, we assume that the existing class is the
260+
// one that we want, and don't try to declare it ourselves.
261+
//
262+
// This is **sound** within the context of a single linker invocation,
263+
// since we ensure in `define_class!` with `#[extern_name = ...]` that
264+
// the class name is unique.
265+
//
266+
// It is **unsound** in the case above of multiple dynamic libraries,
267+
// since we cannot guarantee that the name actually comes from the
268+
// same piece of code. The dynamic linker already does this same merge
269+
// operation though, so we will consider this a non-issue (i.e. the
270+
// same problem already exists in Objective-C w. dynamic libraries).
271+
//
272+
// See <https://github.com/rust-windowing/raw-window-metal/issues/29>
273+
// for more details on the use-case.
274+
//
275+
// NOTE: We _could_ also solve this by autogenerating a class name
276+
// based on the address of a static - but in the future, we would like
277+
// to generate fully static classes, and then such a solution wouldn't
278+
// be possible.
279+
//
280+
// ---
281+
//
282+
// We only do this by default when we've auto-generated the name,
283+
// since here we'll be reasonably sure that it's unique to that
284+
// specific piece of code.
285+
//
286+
// In the future, we might be able to relax this to also work with
287+
// user-specified names, though we'd have to somehow ensure that the
288+
// name isn't something crazy like "NSObject".
289+
//
290+
// We also have an (intentionally undocumented) workaround env var in
291+
// case this becomes a problem for users in the future.
292+
let overridden = option_env!("UNSAFE_OBJC2_ALLOW_CLASS_OVERRIDE") == Some("1");
293+
if name_is_auto_generated || overridden {
294+
AnyClass::get(c_name).unwrap_or_else(|| class_not_present(c_name))
295+
} else {
296+
class_not_unique(c_name)
220297
}
221-
}
298+
};
299+
300+
// Pass class and offsets back to allow storing them in statics for faster
301+
// subsequent access.
302+
(
303+
cls,
304+
ivars_offset::<T>(cls, &ivar_name),
305+
drop_flag_offset::<T>(cls, &drop_flag_name),
306+
)
307+
}
222308

309+
#[derive(Debug)]
310+
pub struct ClassBuilderHelper<T: ?Sized> {
311+
builder: ClassBuilder,
312+
p: PhantomData<T>,
313+
}
314+
315+
impl<T: DefinedClass> ClassBuilderHelper<T> {
223316
#[inline]
224317
pub fn add_protocol_methods<P>(&mut self) -> ClassProtocolMethodsBuilder<'_, T>
225318
where
@@ -279,11 +372,6 @@ impl<T: DefinedClass> ClassBuilderHelper<T> {
279372
// SAFETY: Checked by caller
280373
unsafe { self.builder.add_class_method(sel, func) }
281374
}
282-
283-
#[inline]
284-
pub fn register(self) -> (&'static AnyClass, isize, isize) {
285-
register_with_ivars::<T>(self.builder)
286-
}
287375
}
288376

289377
/// Helper for ensuring that:

crates/objc2/src/__macro_helpers/defined_ivars.rs

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -234,35 +234,34 @@ where
234234
}
235235
}
236236

237-
/// Register the class, and get the ivar offsets.
238237
#[inline]
239-
pub(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.
238+
pub(crate) fn ivar_drop_flag_names<T: DefinedClass>() -> (Cow<'static, CStr>, Cow<'static, CStr>) {
239+
if cfg!(feature = "gnustep-1-7") {
240+
// GNUStep does not support a subclass having an ivar with the
241+
// same name as a superclass, so let's use the class name as the
242+
// ivar name to ensure uniqueness.
243+
(
244+
CString::new(format!("{}_ivars", T::NAME)).unwrap().into(),
245+
CString::new(format!("{}_drop_flag", T::NAME))
246+
.unwrap()
247+
.into(),
248+
)
249+
} else {
250+
// SAFETY: The byte slices are NUL-terminated, and do not contain
251+
// interior NUL bytes.
252+
// TODO: Use `c"my_str"` syntax once in MSRV
253+
unsafe {
247254
(
248-
CString::new(format!("{}_ivars", T::NAME)).unwrap().into(),
249-
CString::new(format!("{}_drop_flag", T::NAME))
250-
.unwrap()
251-
.into(),
255+
CStr::from_bytes_with_nul_unchecked(b"ivars\0").into(),
256+
CStr::from_bytes_with_nul_unchecked(b"drop_flag\0").into(),
252257
)
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-
}
263258
}
264-
};
259+
}
260+
}
265261

262+
/// Register the ivars.
263+
#[inline]
264+
pub(crate) fn register_ivars<T: DefinedClass>(builder: &mut ClassBuilder, ivars_name: &CStr) {
266265
if T::HAS_IVARS {
267266
// TODO: Consider not adding a encoding - Swift doesn't do it.
268267
let ivar_encoding = Encoding::Array(
@@ -276,25 +275,33 @@ pub(crate) fn register_with_ivars<T: DefinedClass>(
276275
alignment => panic!("unsupported alignment {alignment} for `{}::Ivars`", T::NAME),
277276
},
278277
);
279-
unsafe { builder.add_ivar_inner::<T::Ivars>(&ivar_name, &ivar_encoding) };
278+
unsafe { builder.add_ivar_inner::<T::Ivars>(ivars_name, &ivar_encoding) };
280279
}
280+
}
281281

282+
/// Register the drop flag ivar.
283+
#[inline]
284+
pub(crate) fn register_drop_flag<T: DefinedClass>(
285+
builder: &mut ClassBuilder,
286+
drop_flag_name: &CStr,
287+
) {
282288
if T::HAS_DROP_FLAG {
283289
// TODO: Maybe we can reuse the drop flag when subclassing an already
284290
// defined class?
285-
builder.add_ivar::<DropFlag>(&drop_flag_name);
291+
builder.add_ivar::<DropFlag>(drop_flag_name);
286292
}
293+
}
287294

288-
let cls = builder.register();
289-
290-
let ivars_offset = if T::HAS_IVARS {
295+
#[inline]
296+
pub(crate) fn ivars_offset<T: DefinedClass>(cls: &AnyClass, ivars_name: &CStr) -> isize {
297+
if T::HAS_IVARS {
291298
// Monomorphized error handling
292299
// Intentionally not #[track_caller], we expect this error to never occur
293300
fn get_ivar_failed() -> ! {
294301
unreachable!("failed retrieving instance variable on newly defined class")
295302
}
296303

297-
cls.instance_variable(&ivar_name)
304+
cls.instance_variable(ivars_name)
298305
.unwrap_or_else(|| get_ivar_failed())
299306
.offset()
300307
} else {
@@ -303,16 +310,19 @@ pub(crate) fn register_with_ivars<T: DefinedClass>(
303310
// This is fine, since any reads here will only be via zero-sized
304311
// ivars, where the actual pointer doesn't matter.
305312
0
306-
};
313+
}
314+
}
307315

308-
let drop_flag_offset = if T::HAS_DROP_FLAG {
316+
#[inline]
317+
pub(crate) fn drop_flag_offset<T: DefinedClass>(cls: &AnyClass, drop_flag_name: &CStr) -> isize {
318+
if T::HAS_DROP_FLAG {
309319
// Monomorphized error handling
310320
// Intentionally not #[track_caller], we expect this error to never occur
311321
fn get_drop_flag_failed() -> ! {
312322
unreachable!("failed retrieving drop flag instance variable on newly defined class")
313323
}
314324

315-
cls.instance_variable(&drop_flag_name)
325+
cls.instance_variable(drop_flag_name)
316326
.unwrap_or_else(|| get_drop_flag_failed())
317327
.offset()
318328
} else {
@@ -321,9 +331,7 @@ pub(crate) fn register_with_ivars<T: DefinedClass>(
321331
// This is fine, since the drop flag is never actually used in the
322332
// cases where it was not added.
323333
0
324-
};
325-
326-
(cls, ivars_offset, drop_flag_offset)
334+
}
327335
}
328336

329337
/// # Safety

crates/objc2/src/__macro_helpers/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub use core::cell::UnsafeCell;
33
pub use core::cmp::{Eq, PartialEq};
44
pub use core::convert::AsRef;
55
pub use core::default::Default;
6+
pub use core::ffi::CStr;
67
pub use core::fmt;
78
pub use core::hash::{Hash, Hasher};
89
pub use core::marker::{PhantomData, Sized};
@@ -38,8 +39,8 @@ pub use self::class::{DoesNotImplDrop, MainThreadOnlyDoesNotImplSendSync, ValidT
3839
pub use self::common_selectors::{alloc_sel, dealloc_sel, init_sel, new_sel};
3940
pub use self::convert::{ConvertArgument, ConvertArguments, ConvertReturn, TupleExtender};
4041
pub use self::define_class::{
41-
ClassBuilderHelper, ClassProtocolMethodsBuilder, MaybeOptionRetained, MessageReceiveRetained,
42-
RetainedReturnValue, ThreadKindAutoTraits,
42+
class_c_name, define_class, ClassBuilderHelper, ClassProtocolMethodsBuilder,
43+
MaybeOptionRetained, MessageReceiveRetained, RetainedReturnValue, ThreadKindAutoTraits,
4344
};
4445
pub use self::defined_ivars::DefinedIvarsHelper;
4546
pub use self::image_info::ImageInfo;

0 commit comments

Comments
 (0)