//! # Dynamically creating classes and protocols. use alloc::ffi::CString; use alloc::format; use alloc::string::ToString; use core::ffi::CStr; use core::mem; use core::mem::ManuallyDrop; use core::ptr; use core::ptr::NonNull;
fn method_type_encoding(ret: &Encoding, args: &[Encoding]) -> CString { // First two arguments are always self and the selector letmut types = format!("{ret}{}{}", <*mut AnyObject>::ENCODING, Sel::ENCODING); for enc in args { use core::fmt::Write;
write!(&mut types, "{enc}").unwrap();
}
CString::new(types).unwrap()
}
trait Log2Alignment { const LOG2_ALIGNMENT: u8;
}
impl<T> Log2Alignment for T { const LOG2_ALIGNMENT: u8 = { let align = mem::align_of::<T>();
assert!(
align.count_ones() == 1, "alignment required to be a power of 2"
); // log2 of a power of 2 is the number of trailing zeros
align.trailing_zeros() as u8
};
}
/// A type for creating a new class and adding new methods and ivars to it /// before registering it. /// /// **Note**: You likely don't need the dynamicism that this provides! /// Consider using the [`define_class!`][crate::define_class] macro instead. /// /// /// # Example /// /// Create a class named `MyNumber` that has one ivar, a `u32` named `_number` /// and a few constructor methods and methods for interfacing with the number /// (using interior mutability, as is common for Objective-C objects). /// /// ``` /// use core::cell::Cell; /// /// use objc2::rc::Retained; /// use objc2::runtime::{AnyClass, AnyObject, ClassBuilder, NSObject, Sel}; /// use objc2::{sel, msg_send, ClassType}; /// /// fn register_class() -> &'static AnyClass { /// // Inherit from NSObject /// let mut builder = ClassBuilder::new(c"MyNumber", NSObject::class()) /// .expect("a class with the name MyNumber likely already exists"); /// /// // Add an instance variable of type `Cell<u32>` /// builder.add_ivar::<Cell<u32>>(c"_number"); /// /// // Add an Objective-C method for initializing an instance with a number /// // /// // We "cheat" a bit here, and use `AnyObject` instead of `NSObject`, /// // since only the former is allowed to be a mutable receiver (which is /// // always safe in `init` methods, but not in others). /// unsafe extern "C-unwind" fn init_with_number( /// this: &mut AnyObject, /// _cmd: Sel, /// number: u32, /// ) -> Option<&mut AnyObject> { /// let this: Option<&mut AnyObject> = msg_send![super(this, NSObject::class()), init]; /// this.map(|this| { /// let ivar = AnyClass::get(c"MyNumber").unwrap().instance_variable(c"_number").unwrap(); /// // SAFETY: The ivar is added with the same type above /// *ivar.load_mut::<Cell<u32>>(this) = Cell::new(number); /// this /// }) /// } /// unsafe { /// builder.add_method( /// sel!(initWithNumber:), /// init_with_number as unsafe extern "C-unwind" fn(_, _, _) -> _, /// ); /// } /// /// // Add convenience method for getting a new instance with the number /// extern "C-unwind" fn with_number( /// cls: &AnyClass, /// _cmd: Sel, /// number: u32, /// ) -> *mut NSObject { /// let obj: Option<Retained<NSObject>> = unsafe { /// msg_send![ /// msg_send![cls, alloc], /// initWithNumber: number, /// ] /// }; /// obj.map(Retained::autorelease_return).unwrap_or(std::ptr::null_mut()) /// } /// unsafe { /// builder.add_class_method( /// sel!(withNumber:), /// with_number as extern "C-unwind" fn(_, _, _) -> _, /// ); /// } /// /// // Add an Objective-C method for setting the number /// extern "C-unwind" fn my_number_set(this: &NSObject, _cmd: Sel, number: u32) { /// let ivar = AnyClass::get(c"MyNumber").unwrap().instance_variable(c"_number").unwrap(); /// // SAFETY: The ivar is added with the same type above /// unsafe { ivar.load::<Cell<u32>>(this) }.set(number); /// } /// unsafe { /// builder.add_method(sel!(setNumber:), my_number_set as extern "C-unwind" fn(_, _, _)); /// } /// /// // Add an Objective-C method for getting the number /// extern "C-unwind" fn my_number_get(this: &NSObject, _cmd: Sel) -> u32 { /// let ivar = AnyClass::get(c"MyNumber").unwrap().instance_variable(c"_number").unwrap(); /// // SAFETY: The ivar is added with the same type above /// unsafe { ivar.load::<Cell<u32>>(this) }.get() /// } /// unsafe { /// builder.add_method(sel!(number), my_number_get as extern "C-unwind" fn(_, _) -> _); /// } /// /// builder.register() /// } /// /// // Usage /// /// // Note: you should only do class registration once! This can be ensured /// // with `std::sync::Once` or the `once_cell` crate. /// let cls = register_class(); /// /// let obj: Retained<NSObject> = unsafe { /// msg_send![cls, withNumber: 42u32] /// }; /// /// let n: u32 = unsafe { msg_send![&obj, number] }; /// assert_eq!(n, 42); /// /// let _: () = unsafe { msg_send![&obj, setNumber: 12u32] }; /// let n: u32 = unsafe { msg_send![&obj, number] }; /// assert_eq!(n, 12); /// ``` #[derive(Debug)] pubstruct ClassBuilder { // Note: Don't ever construct a &mut AnyClass, since it is possible to // get this pointer using `AnyClass::classes`!
cls: NonNull<AnyClass>,
}
// SAFETY: The stuff that touch global state does so using locks internally. // // Modifying the class itself can only be done through `&mut`, so Sync is // safe (e.g. we can't accidentally call `add_ivar` at the same time from two // different threads). // // (Though actually, that would be safe since the entire runtime is locked // when doing so...). // // Finally, there are no requirements that the class must be registered on the // same thread that allocated it (so Send is safe). unsafeimpl Send for ClassBuilder {} unsafeimpl Sync for ClassBuilder {}
#[allow(unused)] pub(crate) fn superclass(&self) -> Option<&AnyClass> { // SAFETY: Though the class is not finalized, `class_getSuperclass` is // still safe to call. unsafe { AnyClass::superclass_raw(self.cls.as_ptr()) }
}
#[allow(unused)] fn name(&self) -> &CStr { // SAFETY: Same as `superclass` unsafe { AnyClass::name_raw(self.cls.as_ptr()) }
}
/// Constructs a [`ClassBuilder`] with the given name and superclass. /// /// Returns [`None`] if the class couldn't be allocated, or a class with /// that name already exist. #[inline] pubfn new(name: &CStr, superclass: &AnyClass) -> Option<Self> { Self::with_superclass(name, Some(superclass))
}
/// Constructs a [`ClassBuilder`] that will construct a new root class /// with the given name. /// /// Returns [`None`] if the class couldn't be allocated. /// /// An implementation for `+initialize` must also be given; the runtime /// calls this method for all classes, so it must be defined on root /// classes. /// /// Note that implementing a root class is not a simple endeavor! /// For example, your class probably cannot be passed to Cocoa code unless /// the entire `NSObject` protocol is implemented. /// Functionality it expects, like implementations of `-retain` and /// `-release` used by ARC, will not be present otherwise. pubfn root<F>(name: &CStr, initialize_fn: F) -> Option<Self> where
F: MethodImplementation<Callee = AnyClass, Arguments = (), Return = ()>,
{ Self::with_superclass(name, None).map(|mut this| { unsafe { this.add_class_method(sel!(initialize), initialize_fn) };
this
})
}
/// Adds a method with the given name and implementation. /// /// /// # Panics /// /// Panics if the method wasn't successfully added (e.g. a method with that /// name already exists). /// /// May also panic if the method was detected to be invalid in some way; /// for example if `debug_assertions` are enabled and the method is /// overriding another method, we verify that their encodings are equal. /// /// /// # Safety /// /// The caller must ensure that the types match those that are expected /// when the method is invoked from Objective-C. pubunsafefn add_method<T, F>(&mutself, sel: Sel, func: F) where
T: Message + ?Sized,
F: MethodImplementation<Callee = T>,
{ unsafe { self.add_method_inner(
sel,
F::Arguments::ENCODINGS,
&F::Return::ENCODING_RETURN,
func.__imp(),
);
}
}
/// Adds a class method with the given name and implementation. /// /// /// # Panics /// /// Panics in the same cases as [`add_method`][Self::add_method]. /// /// /// # Safety /// /// The caller must ensure that the types match those that are expected /// when the method is invoked from Objective-C. pubunsafefn add_class_method<F>(&mutself, sel: Sel, func: F) where
F: MethodImplementation<Callee = AnyClass>,
{ unsafe { self.add_class_method_inner(
sel,
F::Arguments::ENCODINGS,
&F::Return::ENCODING_RETURN,
func.__imp(),
);
}
}
// Verify that, if the method is present on the superclass, that the // encoding is correct. #[cfg(all(debug_assertions, not(feature = "disable-encoding-assertions")))] iflet Some(superclass) = self.superclass() { iflet Some(method) = superclass.class_method(sel) { iflet Err(err) = crate::verify::verify_method_signature(method, enc_args, enc_ret)
{
panic!( "defined invalid method +[{} {sel}]: {err}", self.name().to_string_lossy()
)
}
}
}
let types = method_type_encoding(enc_ret, enc_args); let success = unsafe { ffi::class_addMethod(self.metaclass_mut(), sel, func, types.as_ptr()) };
assert!(success.as_bool(), "failed to add class method {sel}");
}
/// Adds an ivar with type `T` and the provided name. /// /// /// # Panics /// /// If the ivar wasn't successfully added for some reason - this usually /// happens if there already was an ivar with that name. pubfn add_ivar<T: Encode>(&mutself, name: &CStr) { // SAFETY: The encoding is correct unsafe { self.add_ivar_inner::<T>(name, &T::ENCODING) }
}
// Monomorphized version unsafefn add_ivar_inner_mono(
&mutself,
name: &CStr,
size: usize,
align: u8,
encoding: &Encoding,
) { let encoding = CString::new(encoding.to_string()).unwrap();
// Note: The Objective-C runtime contains functionality to do stuff // with "instance variable layouts", but we don't have to touch any of // that, it was only used in the garbage-collecting runtime. // // Note: On GNUStep, instance variables cannot have the same name // on subclasses as it has on superclasses. // // See <https://github.com/gnustep/libobjc2/issues/246> let success = unsafe {
ffi::class_addIvar( self.as_mut_ptr(),
name.as_ptr(),
size,
align,
encoding.as_ptr(),
)
};
assert!(success.as_bool(), "failed to add ivar {name:?}");
}
/// Makes the class conform to the given protocol. /// /// This will also make the class conform to any super-protocols that the /// given protocol may have. /// /// Returns whether the class did not already conform to the protocol. /// This may commonly return false if you first add e.g. /// `NSProgressReporting`, and then later try to add `NSObjectProtocol`, /// which is a super-protocol thereof. #[inline] pubfn add_protocol(&mutself, proto: &AnyProtocol) -> bool { let success = unsafe { ffi::class_addProtocol(self.as_mut_ptr(), proto) };
success.as_bool()
}
/// Registers the [`ClassBuilder`], consuming it, and returns a reference /// to the newly registered [`AnyClass`]. #[inline] pubfn register(self) -> &'static AnyClass { // Forget self, otherwise the class will be disposed in drop letmut this = ManuallyDrop::new(self); unsafe { ffi::objc_registerClassPair(this.as_mut_ptr()) }; unsafe { this.cls.as_ref() }
}
}
impl Drop for ClassBuilder { #[inline] fn drop(&mutself) { // Disposing un-registered classes doesn't work properly on GNUStep, // so we register the class before disposing it. // // Doing it this way is _technically_ a race-condition, since other // code could read e.g. `AnyClass::classes()` and then pick the class // before it got disposed - but let's not worry about that for now. #[cfg(feature = "gnustep-1-7")] unsafe {
ffi::objc_registerClassPair(self.as_mut_ptr());
}
/// A type for creating a new protocol and adding new methods to it /// before registering it. #[derive(Debug)] pubstruct ProtocolBuilder {
proto: NonNull<AnyProtocol>,
}
// SAFETY: Similar to ClassBuilder unsafeimpl Send for ProtocolBuilder {} unsafeimpl Sync for ProtocolBuilder {}
/// Constructs a [`ProtocolBuilder`] with the given name. /// /// Returns [`None`] if the protocol couldn't be allocated. /// /// /// # Panics /// /// Panics if the name contains an internal NULL byte. #[inline] pubfn new(name: &CStr) -> Option<Self> { let proto = unsafe { ffi::objc_allocateProtocol(name.as_ptr()) };
NonNull::new(proto.cast()).map(|proto| Self { proto })
}
/// Add an instance method with a given description. pubfn add_method_description<Args, Ret>(&mutself, sel: Sel, required: bool) where
Args: EncodeArguments,
Ret: EncodeReturn,
{ self.add_method_description_inner(
sel,
Args::ENCODINGS,
&Ret::ENCODING_RETURN,
required, true,
);
}
/// Add a class method with a given description. pubfn add_class_method_description<Args, Ret>(&mutself, sel: Sel, required: bool) where
Args: EncodeArguments,
Ret: EncodeReturn,
{ self.add_method_description_inner(
sel,
Args::ENCODINGS,
&Ret::ENCODING_RETURN,
required, false,
);
}
/// Adds a requirement on another protocol. pubfn add_protocol(&mutself, proto: &AnyProtocol) { unsafe { ffi::protocol_addProtocol(self.as_mut_ptr(), proto) };
}
/// Registers the [`ProtocolBuilder`], consuming it and returning a reference /// to the newly registered [`AnyProtocol`]. pubfn register(mutself) -> &'static AnyProtocol { unsafe {
ffi::objc_registerProtocol(self.as_mut_ptr()); self.proto.as_ref()
}
}
}
impl Drop for ProtocolBuilder { #[inline] fn drop(&mutself) { // We implement Drop to communicate to the type-system that this type // may drop in the future (once Apple add some way of disposing // protocols).
}
}
#[cfg(test)] mod tests { use core::hash::Hasher; use std::collections::hash_map::DefaultHasher; use std::hash::Hash;
let protocol = <dyn NSObjectProtocol>::protocol().unwrap();
// GNUStep is more eagerly returning false in the case where we // inherit something that implements the protocol. if cfg!(feature = "gnustep-1-7") {
assert!(!builder.add_protocol(protocol));
} else {
assert!(builder.add_protocol(protocol));
}
let cls = builder.register();
assert!(cls.conforms_to(protocol));
}
#[test] fn add_protocol_subprotocol_ordering() { // The value returned by `class_addProtocol` is inherently dependent // on the order in which you add the super- and subprotocols. let builder = ProtocolBuilder::new(&c("Superprotocol")).unwrap(); let superprotocol = builder.register();
letmut builder = ProtocolBuilder::new(&c("Subprotocol")).unwrap();
builder.add_protocol(superprotocol); let subprotocol = builder.register();
#[test] fn test_classbuilder_drop() { let cls = test_utils::custom_class(); let builder = ClassBuilder::new(&c("TestClassBuilderDrop"), cls).unwrap();
drop(builder); // After we dropped the class, we can create a new one with the same name: let _builder = ClassBuilder::new(&c("TestClassBuilderDrop"), cls).unwrap();
}
#[test] fn test_custom_class() { // Registering the custom class is in test_utils let obj = test_utils::custom_object(); let _: () = unsafe { msg_send![&obj, setFoo: 13u32] }; let result: u32 = unsafe { msg_send![&obj, foo] };
assert_eq!(result, 13);
}
#[test] fn test_in_all_classes() { fn is_present(cls: *const AnyClass) -> bool { // Check whether the class is present in AnyClass::classes()
AnyClass::classes().iter().any(|item| ptr::eq(cls, *item))
}
let superclass = test_utils::custom_class(); let builder = ClassBuilder::new(&c("TestFetchWhileCreatingClass"), superclass).unwrap();
if cfg!(all(
target_vendor = "apple",
any(target_arch = "aarch64", target_arch = "x86_64")
)) { // It is IMO a bug that it is present here!
assert!(is_present(builder.cls.as_ptr().cast()));
} else {
assert!(!is_present(builder.cls.as_ptr().cast()));
}
let cls = builder.register();
assert!(is_present(cls));
}
#[test] fn test_class_method() { let cls = test_utils::custom_class(); let result: u32 = unsafe { msg_send![cls, classFoo] };
assert_eq!(result, 7);
}
// Proof-of-concept how we could make define_class! accept generic types. #[test] fn test_generic() { struct GenericDefineClass<T>(T);
letmut subclass =
ClassBuilder::new(&c("DefineClassDuplicateIvarSubclass"), superclass).unwrap(); // Try to overwrite instance variables
subclass.add_ivar::<i16>(&c("ivar1"));
subclass.add_ivar::<usize>(&c("ivar2"));
subclass.add_ivar::<*const AnyObject>(&c("ivar3"));
subclass.add_ivar::<usize>(&c("ivar4")); let subclass = subclass.register();
// Test that ivar layout matches that of C // // In particular, ivars are not reordered, though any extra padding on // superclasses are utilized on subclasses. #[repr(C)] struct NSObjectLayout {
isa: *const AnyClass,
}
assert_eq!(
NSObject::class().instance_size(),
mem::size_of::<NSObjectLayout>(),
);
#[repr(C)] struct SuperLayout {
isa: *const AnyClass,
ivar1: u8, // Padding (7 on 64bit, 11 on 32bit)
ivar2: U128align16,
ivar3: u8,
ivar4: [u8; 0], // Padding (15 in Rust, 7 on 64bit, 3 on 32bit)
} // Class's ivar size is only rounded up to a pointer-sized boundary, // not all the way up to the maximum alignment. // // This is surprising, but actually fine, since Objective-C objects // are never packed closely like Rust structs would be in an array.
assert_eq!(
superclass.instance_size(),
mem::size_of::<SuperLayout>() - 16 + mem::size_of::<*const AnyClass>(),
);
let superclass_ivar1 = superclass.instance_variable(&c("ivar1")).unwrap(); let superclass_ivar2 = superclass.instance_variable(&c("ivar2")).unwrap(); let superclass_ivar3 = superclass.instance_variable(&c("ivar3")).unwrap(); let superclass_ivar4 = superclass.instance_variable(&c("ivar4")).unwrap(); let subclass_ivar1 = subclass.instance_variable(&c("ivar1")).unwrap(); let subclass_ivar2 = subclass.instance_variable(&c("ivar2")).unwrap(); let subclass_ivar3 = subclass.instance_variable(&c("ivar3")).unwrap(); let subclass_ivar4 = subclass.instance_variable(&c("ivar4")).unwrap();
// Ensure that duplicate names do not conflict
assert_ne!(superclass_ivar1, subclass_ivar1);
assert_ne!(superclass_ivar2, subclass_ivar2);
assert_ne!(superclass_ivar3, subclass_ivar3);
assert_ne!(superclass_ivar4, subclass_ivar4);
// Ensure that all offsets are as expected
assert_eq!(
superclass_ivar1.offset(),
offset_of!(SuperLayout, ivar1) as isize
);
assert_eq!(
superclass_ivar2.offset(),
offset_of!(SuperLayout, ivar2) as isize
);
assert_eq!(
superclass_ivar3.offset(),
offset_of!(SuperLayout, ivar3) as isize
);
assert_eq!(
superclass_ivar4.offset(),
offset_of!(SuperLayout, ivar4) as isize
);
assert_eq!(
subclass_ivar1.offset(),
offset_of!(SubLayout, ivar1_b) as isize
);
assert_eq!(
subclass_ivar2.offset(),
offset_of!(SubLayout, ivar2_b) as isize
);
assert_eq!(
subclass_ivar3.offset(),
offset_of!(SubLayout, ivar3_b) as isize
);
assert_eq!(
subclass_ivar4.offset(),
offset_of!(SubLayout, ivar4_b) as isize
);
// Ensure our ivar loading works correctly let obj: Retained<NSObject> = unsafe { msg_send![subclass, new] }; let ptr = unsafe { *subclass_ivar3.load::<*const AnyObject>(&obj) };
assert!(ptr.is_null());
// Illustration of what goes wrong with the naive approach of loading // the Ivar dynamically; in short, we can't be sure of which instance // variable we're referring to here. // // let ivar = *obj.get_ivar::<u8>("ivar3");
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.