Description
With the Objective-C crate, the machinery to send messages is available in stable Rust. From there, it should be possible to parse Objective-C headers, and generate Rust bindings for them.
This would greatly help with integrating Rust into existing projects, especially iOS and Mac apps.
Some ideas about how this could be done:
- Classes should be generated as unit-structs which wrap
id
. Class methods and properties
can generate an anonymous impl, which calls tomsg_send!
. Note that Objective-C has no
concept of 'const
' methods, so every generated method will take&mut self
. - Inheritance can be implemented with auto-deref. If you have 2 classes
A
andB
, whereB
is
inherited fromA
, then we canimpl Deref<Target=A> for B { .. }
andimpl DerefMut<Target=A> for B
. Because Objective-C only has single inheritance, there should be no ambiguity about which
class to deref to. Base classes won't need this. - Protocols can be cleanly mapped to traits. The way this works is that each trait will have a hidden
method which gets the object pointer, which will be auto implemented on the struct which wraps
the object which implements this protocol. The protocol's methods will then use this method to
provide default implementations which callmsg_send!
on the pointer returned by this method. - There can be multiple
impl
blocks on a type in a crate, so extensions can just open up another
impl
block.
A probably-incomplete example which ties these ideas together:
@protocol Adder <NSObject>
- (int)addNumber:(int)a toOtherNumber:(int)b;
@end
@interface Foo: NSObject
- (instancetype) initWithFirstNumber:(int)firstNumber;
@property(nonatomic, readonly) firstNumber;
@end
@interface Bar: Foo <Adder>
@property(nonatomic, readwrite) int someProperty;
- (instancetype) initWithFirstNumber:(int)firstNumber
AndSecondNumber:(int)secondNumber;
- (void) frobnicate;
- (NSString*) getDescriptionFor:(NSObject*)object atIndex:(size_t)index;
- (int) addNumber:(int)a toOtherNumber:(int)b;
@end
Could generate:
#[macro_use]
extern crate objc;
pub trait Adder: NSObjectProtocol {
// Gets the `self` pointer which will be used for calling `msg_send!`
#[doc(hidden)]
fn self_ptr(&self) -> id;
// All functions which call objc methods are unsafe to call.
unsafe fn addNumber_toOtherNumber_(a: libc::c_int, b: libc::c_int) -> libc::c_int {
msg_send![self, addNumber:a toOtherNumber:b]
}
}
pub struct Foo(id);
impl Deref for Foo {
type Target = NSObject;
fn deref(&self) -> &Self::Target {
// Both `self` and `NSObject` wrap an `id` pointer, so this should be safe
unsafe { ::core::mem::transmute(self.0) }
}
}
impl DerefMut for Foo {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { ::core::mem::transmute(self.0) }
}
}
impl Foo {
// An alloc method is generated automatically.
pub unsafe fn alloc() -> Self {
msg_send![class("Foo"), alloc]
}
// Objc's `instancetype` resolves to the `Self` type
pub unsafe fn initWithFirstNumber_(&mut self, firstNumber: libc::c_int) -> Self {
msg_send![self.0, initWithFirstNumber:firstNumber]
}
// Properties resolve to method calls, with the form:
// * getter = `propertyName`
// * setter = `setPropertyName:`
pub unsafe fn firstNumber(&mut self) -> libc::c_int {
msg_send![self.0, firstNumber]
}
}
pub struct Bar(id);
impl Deref for Bar {
type Target = Foo;
fn deref(&self) -> &Self::Target {
unsafe { ::core::mem::transmute(self.0) }
}
}
impl DerefMut for Bar {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { ::core::mem::transmute(self.0) }
}
}
// NSObjectProtocol implementation through inheritance.
impl Adder for Bar {
fn self_ptr(&self) -> id { self.0 }
}
impl Bar {
pub unsafe fn alloc() -> Self {
msg_send![class("Bar"), alloc]
}
pub unsafe fn initWithFirstNumber_andSecondNumber_(&mut self,
firstNumber: libc::c_int, secondNumber: libc::c_int) -> Self {
msg_send![self.0, initWithFirstNumber:firstNumber andSecondNumber:secondNumber]
}
pub unsafe fn someProperty(&mut self) -> libc::c_int {
msg_send![self.0, someProperty]
}
pub unsafe fn setSomeProperty_(&mut self, someProperty: libc::c_int) {
msg_send![self.0, setSomeProperty:someProperty]
}
pub unsafe fn frobnicate(&mut self) {
// ...
}
pub unsafe fn getDescriptionFor_atIndex_(&mut object: NSObject, atIndex: libc::size_t)
-> NSString {
// ...
}
// The `- (int) addNumber:(int)a toOtherNumber:(int)b;` declaration in `Bar` is ignored because
// it is a protocol method which is already implemented.
}
Note that these bindings are not the most ergonomic to use: each method is unsafe
, each variable has to be declared mut
to actually call any methods on it, and null pointer checking has to be performed manually (it may be worth adding an is_null
method to NSObject, or to a new trait which is automatically implemented by objc base objects). However, they take a lot of the tedium out of writing bindings manually. Furthermore, it could be possible to refine the ergonomics further:
- Methods which return objects could return
Option<_>
, with theNone
case matching to anil
.
If we can parse the the new nullability attributes,
we could probably just return the object if it was declared_Nonnull
in the header. - We could search the name of the class to see if the substring
Mutable
was present to decide
whether methods should pass&self
or&mut self
, or perhaps it might be possible to add a
custom attribute, e.g.__attribute__((rust_objc_mutating))
to method declarations. Of course,
Apple might also decide to add their own mutability attributes to Clang later. - We could decide that the bindings we generate are safe enough that we could enclose the
msg_send!
in anunsafe
block and just generate safefn
s. - There is no mention of generics in this report, but they could be added as a
PhantomData<T>
in
the struct. To maintain optional dynamism, this would require a new trait (e.g.AnyObject
) which
all objc objects implemented, and theT
generic would require. With this in place,NSArray
could
be declared asstruct NSArray<T: AnyObject = NSObject>(id, PhantomData<T>)
. - Various Foundation protocols map fairly cleanly on to traits from
std
. There is a clear
relationNSCopying
andClone
, and everyNSObject
-derived class can implement
std::hash::Hash
courtesy of thehash
method, and eachNSObject
type can bePartialEq
with theisEqual:
method.