Rust <3 Swift: Writing Cocoa Apps in Rust (Eventually)
This article was written using Rust 1.13 nightly (the latest as of September 2016) with unstable features. Certain unstable features may have been changed or removed since.
Have you ever noticed how similar Rust and Swift are? I don’t mean just syntactically either— the languages have a lot of similar design philosophies:
- Both languages have pretty good static type systems that prevent many errors at compile time, like
NULL
reference errors - Both languages often encourage using simple struct and enum values with traits/protocols where possible, over using object-oriented class hierarchies
- Both languages provide very nice high-level constructs (things like
map
, or pattern matching) that get compiled and optimized into a static, native (and fast!) binary
Really, you can’t go wrong with using either language: both are really compelling for building things in a wide range of different domains!
That said, I personally prefer Rust, so learning about Swift inspired me to sit down and think, “what can Swift do that Rust can’t, and why?” To me, the most glaring thing was that Swift is really good for app development, while Rust in this regard is… lacking.
For now, let’s just talk about building Cocoa apps for macOS. Sure, you can build a native Cocoa app in Rust right now using the cocoa
crate, but it’s very far removed from writing a Cocoa app in Swift: notice the very procedural nature of their “hello world” example app.
So, how can we improve on the ergonomics of the cocoa
crate?
Well, let’s first start with a very simple Swift app: all this app does is open an empty window and print some messages to stdout
, then the app quits when the window is closed:
import Cocoa class AppDelegate: NSObject, NSApplicationDelegate { let app: NSApplication let controller: NSWindowController init(app: NSApplication) { self.app = app self.controller = WindowController() } func applicationDidFinishLaunching(_: NSNotification) { controller.showWindow(nil) app.activateIgnoringOtherApps(true) print("Application launched!") } func applicationWillTerminate(_: NSNotification) { print("Application terminated!") } func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { return true } } class WindowController: NSWindowController { required init(coder: NSCoder) { fatalError("Not implemented") } init() { let rect = NSMakeRect(0, 0, 480, 320) let style = NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask let window = NSWindow(contentRect: rect, styleMask: style, backing: .Buffered, defer: false) window.title = "App" super.init(window: window) } } let app = NSApplication.sharedApplication() let delegate = AppDelegate(app: app) app.delegate = delegate app.setActivationPolicy(.Regular) app.run()
(Credit for the above to lucamarrocoo via a GitHub Gist)
Now, let’s try translating this app to use our new “ideal” Rust crate:
1 extern crate new_cocoa as cocoa; // Our new library! 2 3 struct AppDelegate { 4 super_: cocoa::NSObject, // (1) 5 app: cocoa::NSApplication, 6 controller: cocoa::NSWindowController 7 } 8 9 // (2) 10 impl cocoa::Object for AppDelegate { 11 type Super = cocoa::NSObject; 12 13 fn super_ref(&self) -> &Self::Super { 14 &self.super_ 15 } 16 } 17 18 impl cocoa::AppDelegate { 19 fn new_with_app(app: cocoa::NSApplication) -> Self { 20 AppDelegate { 21 super_: cocoa::NSObject::new(), 22 app: app.clone(), 23 controller: WindowController::new() 24 } 25 } 26 } 27 28 // (3) 29 impl cocoa::IsNSApplicationDelegate for AppDelegate { 30 fn application_did_finish_launching(&self, _: &cocoa::NSNotificaton) { 31 self.show_window(None); 32 self.activate_ignoring_other_apps(true); 33 34 println!("Application launched!"); 35 } 36 37 fn application_will_terminate(&self, _: &cocoa::NSNotification) { 38 println!("Application terminated!"); 39 } 40 41 fn application_should_terminate_after_last_window_closed(_: &cocoa::NSApplication) 42 -> bool 43 { 44 true 45 } 46 } 47 48 struct WindowController { 49 super_: cocoa::NSWindowController // (1) 50 } 51 52 // (2) 53 impl cocoa::Object for WindowController { 54 type Super = cocoa::NSWindowController; 55 56 fn super_ref(&self) -> &Self::Super { 57 &self.super_ 58 } 59 } 60 61 impl WindowController { 62 fn new() -> Self { 63 let rect = cocoa::NSMakeRect(0.0, 0.0, 480.0, 320.0); 64 let style = cocoa::NSTitledWindowMask 65 | cocoa::NSClosableWindowMask 66 | cocoa::NSResizableWindowMask; 67 let backing = cocoa::NSBackingStoreType::Buffered; 68 let window = cocoa::NSWindow::new(rect, style, backing, false); 69 window.set_title("App"); // (4) 70 71 WindowController { 72 super_: NSWindowController::new_with_window(window) 73 } 74 } 75 } 76 77 // (3) 78 impl cocoa::IsNSWindowController for WindowController { 79 fn new_with_coder(coder: cocoa::NSCoder) -> Self { 80 panic!("Not implemented"); 81 } 82 } 83 84 fn main() { 85 let app = NSApplication::shared_application(); 86 let delegate = AppDelegate::new(app); 87 app.set_delegate(delegate); // (4) 88 }
I think the above translation is pretty good! It’s not perfect (as we’ll see), but it’s a pretty good jumping-off point. That said, there’s a few things about the translation I wanted to point out:
- The
super_
fields (lines 4 and 49). Because Rust doesn’t have object-oriented-style inheritance, we need to explicitly add a field to hold our base class (which is roughly how inheritance works under-the-hood in some other languages, we’re just doing it manually). You can also see how this affects our constructors on lines 20-24 and 71-73. - The
Object
trait (lines 10 and 53). There’s nothing special about the aforementionedsuper_
field, so we use theObject
trait to signal that we want to use it for “inheritance”. We’re going to go more in-depth on how this works later :) IsNSApplicationDelegate
(line 29) andIsNSWindowController
(line 78). We’ll talk about this more later, but the core idea is that we separate a class’s “instance type” (named likeNSThing
) from it’s trait “interface” (named likeIsNSThing
).- Explicit getters and setters (e.g. lines 69 and 87). Rust doesn’t have any form of overriding getters and setters, so we just use plain ol’ methods instead!
So, we have a clear objective: to build this nice Cocoa library for Rust. Now, the real question is: how would we go about building it? To be clear, I’m not going to walk through building the whole example to completion; instead, I’m going to focus on some of the higher-level design questions, as well as some of the hurdles I faced while building it to completion. If you’re impatient and just want to dive in with the resulting library, feel free to skip to the epilogue (where I also discuss where this project might be headed).
With that out of the way, let’s dive in! But, before getting our hands dirty, we need to talk about…
The Objective-C runtime!
The Objective-C runtime is going to be our gateway into the world of Cocoa, so we should at least have a cursory understanding of how it works.
First, let’s dissect a very simple piece of Swift code that uses Cocoa:
1 import Cocoa 2 3 let menu = NSMenu(title: "Hello!") 4 let app = NSApplication.sharedApplication() 5 app.mainMenu = menu 6 app.setActivationPolicy(.Regular) 7 app.run()
This code seems pretty innocuous, right? All it does is create a main menu for our app with the title, “Hello!”, then it runs the app1. What does the “runtime” even do here? Well, let’s break it down, line by line:
- line 3: Create a new
NSMenu
by sending the messagealloc
to theNSMenu
class. Then, instantiate it by sending theinitWithTitle:
message (which sets the class up appropriately). - line 4: Get a reference to the current app by sending the
sharedApplication
message to theNSApplication
class. - line 5: Set the app’s main menu by sending the
setMainMenu:
message to theapp
object - line 6: Send the
setActivationPolicy:
message to theapp
object - line 7: Run the app by sending the
run
message to theapp
object
Sending messages with the objc
crate
So, as you may have gathered, sending message is pretty important in the Objective-C runtime! But, how do you even “send a message” using Rust, then? Fortunately, the Objective-C runtime has a pretty simple C API. Double fortunately, there’s already an awesome crate for working with the Objective-C runtime in Rust! We can translate the above example to Rust using the objc
crate like this:
#[macro_use] extern crate objc; use std::ffi::CString; use objc::runtime::{Object, Class}; // Link to Cocoa #[link(name = "Cocoa", kind = "framework")] extern { } fn main() { unsafe { // Convert "Hello!" to an `NSString` let title = CString::new("Hello!").unwrap(); let title = title.as_ptr(); let NSString = Class::get("NSString").unwrap(); let title: *mut Object = msg_send![NSString, stringWithUTF8String:title]; // `title` is now an `NSString` that holds // the string "Hello!", so proceed as normal let NSMenu = Class::get("NSMenu").unwrap(); let menu: *mut Object = msg_send![NSMenu, alloc]; let menu: *mut Object = msg_send![menu, initWithTitle:title]; let NSApplication = Class::get("NSApplication").unwrap(); let app: *mut Object = msg_send![NSApplication, sharedApplication]; let _: () = msg_send![app, setMainMenu:menu]; let _: () = msg_send![app, setActivationPolicy: 0 /* .Regular */]; let _: () = msg_send![app, run]; } }
The very first thing we do is… add an empty extern { }
with a #[link(...)]
attribute. It’s kind of weird, but it basically just tells rustc
to link to Cocoa (you can do the same thing by running cargo rustc -- -l framework=Cocoa
). If you omit it, the code will still compile, but the program will panic at runtime because none of the classes could be found.
The next thing we need to do is to convert our “Hello!” string to an NSString
. To do so, we first convert it to a *const c_char
by using the CString
type. Then, we convert it to an NSString
with NSString
’s stringWithUTF8String
static method.
Once that’s done, the rest of the code is pretty straightforward! As you may have gathered, the syntax msg_send![foo, bar:baz]
means “send the bar:
message to foo
” (which was heavily influenced by the Objective-C syntax for sending messages).
There are two curious things that came up in our example, however:
- The (useless-looking)
let
statements. We use these to annotate the return type of a method, whichmsg_send!
needs to know to call the correct version ofobjc_msgSend
. - The
Class::get(...)
calls. As mentioned previously, classes can receive messages in the Objective-C runtime too, which is important for class methods (like when we sendalloc
to create a new instance of a class, or when we callsharedApplication
to get ourNSApplication
).
There’s also two additional important details in the above that I should mention:
- All objects are of type
*mut Object
(a.k.aid
if you’re writing Objective-C, andAnyObject
if you’re writing Swift)! This will come up when we talk about polymorphism, so just keep it in the back of your mind for now! - Classes are objects too! We can get a class object using the
Class::get(...)
method, then we can send messages to it usingmsg_send!
. As we’ll also see later, you can even make new classes at runtime!
Traits and classes
So, now we understand enough of the Objective-C runtime to actually dive into our library! But there’s still a pretty large unanswered question for our library design: how do we even make “classes” in Rust? Well, Rust doesn’t have classes or inheritance, but it does have structs and traits! So here’s an example of the NSWindowController
class represented with a struct and a trait, with an overridable windowDidLoad
method:
#[macro_use] extern crate objc; use objc::runtime::Object as AnyObject; // Link to Cocoa #[link(name = "Cocoa", kind = "framework")] extern { } struct NSWindowController { id: *mut AnyObject } trait IsNSWindowController { // [self windowDidLoad] fn window_did_load(&self) { // `windowDidLoad` does nothing unless a subclass overrides it } // ... every other selector `NSWindowController` responds to ... } impl IsNSWindowController for NSWindowController { fn window_did_load(&self) { unsafe { // Send `windowDidLoad` to the inner Objective-C object msg_send![self.id, windowDidLoad]; } } // ... }
In the above, NSWindowController
is just a newtype around an Objective-C object (remember, all objects in the Objective-C runtime are *mut Object
/ *mut AnyObject
/ id
). If we write a function that returns an NSWindowController
, it’s just an assertion that the id
field is some object that conforms to the NSWindowController
interface (i.e. it responds to windowDidLoad
), which means we can call any NSWindowController
methods on it safely (so, we can write controller.windowDidLoad()
). The important point is that, when returning an NSWindowController
, the object in the id
field doesn’t necessarily have to have the class NSWindowController
(it could be a subclass or a surrogate object, for instance).
Inheritance hierarchy
So why move all of the methods into a separate trait, rather than putting them in a normal impl NSWindowController
block? Well, let’s add a second class into the mix and see how our code changes; we’ll add NSWindowController
’s superclass, NSResponder
:
#[macro_use] extern crate objc; use objc::runtime::Object as AnyObject; use objc::runtime::{BOOL, YES}; // Link to Cocoa #[link(name = "Cocoa", kind = "framework")] extern { } struct NSResponder { id: *mut AnyObject } trait IsNSResponder { // [self becomeFirstResponder] fn become_first_responder(&self) -> bool { // Default implementation returns true true } // ... every other selector `NSResponder` responds to ... } impl IsNSResponder for NSResponder { fn become_first_responder(&self) -> bool { unsafe { // Send `becomeFirstResponder` to the object and // convert the result to a `bool` let result: BOOL = msg_send![self.id, becomeFirstResponder]; result == YES } } // ... } struct NSWindowController { id: *mut AnyObject } trait IsNSWindowController: IsNSResponder { // [self windowDidLoad] fn window_did_load(&self) { // default windowDidLoad impl does nothing } // ... every other selector `NSWindowController` responds to ... } impl IsNSResponder for NSWindowController { fn become_first_responder(&self) -> bool { unsafe { // Send `becomeFirstResponder` to the object and // convert the result to a `bool` let result: BOOL = msg_send![self.id, becomeFirstResponder]; result == YES } } // ... } impl IsNSWindowController for NSWindowController { fn window_did_load(&self) { unsafe { // Send `windowDidLoad` to the inner Objective-C object msg_send![self.id, windowDidLoad]; } } // ... }
The NSResponder
struct and IsNSResponder
trait look roughly like you’d probably expect, mirroring exactly how we had NSWindowController
and IsNSWindowController
before. But, we added two curious things: the IsNSWindowController: IsNSResponder
, and the impl IsNSResponder for NSWindowController
.
The IsNSWindowController: IsNSResponder
line says, “the IsNSWindowController
trait inherits the IsNSResponder
trait”. It doesn’t quite work like object-oriented inheritance works, though: instead, what is says is “all implementors of IsNSWindowController
must also implement IsNSResponder
“.
This explains the second strange addition: since NSWindowController
implements IsNSWindowController
, that means it must also implement IsNSResponder
. Combined, that means that any time we have an object that implements IsNSWindowController
(i.e. an object who’s class is NSWindowController
or a subclass), we can call any NSResponder
methods on it! This should make intuitive sense to those familiar with an understanding of object-oriented programming as well: every NSWindowController
is also an NSResponder
, so that means we can call any NSResponder
method for any given NSWindowController
.
Don’t repeat yourself!
Eagle-eyed readers may have noticed something fishy in the above: lines 24-33 are exactly the same as lines 50-59! We can clean this up a bit by changing the code to the following:
struct NSWindowController { id: NSResponder } impl IsNSResponder for NSWindowController { fn become_first_responder(&self) -> bool { // Forward along the call... self.id.become_first_responder() } // ... } impl IsNSWindowController for NSWindowController { fn window_did_load(&self) { unsafe { // Send `windowDidLoad` to the inner Objective-C object msg_send![self.id.id, windowDidLoad]; } } // ... }
So, by changing our NSWindowController
to hold an NSResponder
instead of a *mut AnyObject
, we can just forward the become_first_responder
call to NSResponder
directly! Note that this still allows for calling subclass overrides, because it’s still sending a message via the Objective-C runtime, since that’s what NSResponder::become_first_responder
does! Because of this change though, we did also needed to adjust our window_did_load
method.
Still, this is a great win in reducing duplication! So, all done, time to go home, right? Wrong, we can reduce duplication even further:
#[macro_use] extern crate objc; use objc::runtime::{BOOL, YES}; use objc::runtime::Object as AnyObject; // Link to Cocoa #[link(name = "Cocoa", kind = "framework")] extern { } // A trait used to indicate that a type is a "class" trait Object { // The "superclass" this type inherits from type Super; // Get a pointer to the superclass fn super_ref(&self) -> &Self::Super; } struct NSResponder { super_: *mut AnyObject } impl Object for NSResponder { // `NSResponder` doesn't really have a superclass in our example, // so we use `*mut AnyObject` as a sort of "base class". type Super = *mut AnyObject; fn super_ref(&self) -> &Self::Super { &self.super_ } } trait IsNSResponder { // [self becomeFirstResponder] fn become_first_responder(&self) -> bool { // Default implementation returns true true } // ... every other selector `NSResponder` responds to ... } // A trait that indicates that a type is a subclass // of `NSResponder` specifically trait SubNSResponder { type SuperNSResponder: IsNSResponder; fn super_ns_responder(&self) -> &Self::SuperNSResponder; } // Automatically implement `SubNSResponder` // for all `NSResponder` subclasses impl<T> SubNSResponder for T where T: Object, T::Super: IsNSResponder { type SuperNSResponder = T::Super; fn super_ns_responder(&self) -> &Self::SuperNSResponder { self.super_ref() } } // The base impl of `IsNSResponder`, which all // subclasses will fallback to impl IsNSResponder for NSResponder { fn become_first_responder(&self) -> bool { unsafe { // Send `becomeFirstResponder` to the object and // convert the result to a `bool` let result: BOOL = msg_send![self.super_, becomeFirstResponder]; result == YES } } // ... } // The default impl of `IsNSResponder` for all subclasses impl<T> IsNSResponder for T where T: SubNSResponder { fn become_first_responder(&self) -> bool { // Forward `become_first_responder` to our superclass self.super_ns_responder().become_first_responder() } } struct NSWindowController { super_: NSResponder } impl Object for NSWindowController { type Super = NSResponder; fn super_ref(&self) -> &Self::Super { &self.super_ } } trait IsNSWindowController: IsNSResponder { // [self windowDidLoad] fn window_did_load(&self) { // default windowDidLoad impl does nothing } // ... every other selector `NSWindowController` responds to ... } // `IsNSResponder` is now automatically // implemented for `NSWindowController`! impl IsNSWindowController for NSWindowController { fn window_did_load(&self) { unsafe { // Send `windowDidLoad` to the inner Objective-C object msg_send![self.super_.super_, windowDidLoad]; } } // ... }
To summarize the above:
Object
is a trait that indicates that a type “subclasses” another type.Object::Super
is the superclass’s type.Object::super_ref
is a method that “converts” a pointer from a subclass to its superclass (basically, it just returns a reference to a field that holds an instance of the superclass).- The extra trait,
SubNSResponder
, is a trait implemented for all subclasses ofNSResponder
(this is necessary because of this Rust issue). - We automatically implement
IsNSResponder
for allNSResponder
subclasses by forwarding to the superclass’simpl
s by default
Well, okay, we actually ended up with more code… But, on the plus side, we don’t need to add an impl
for every ancestor in a class’s hierarchy—we now only need one Object
impl, one Sub_
trait, one blanket Sub_
impl, and one Is_
blanket impl per class! This scales a lot better (and is a lot easier to write a macro for… *cough cough*)!
There is, however, one problem: we just took away the ability for a subclass to override a method from a superclass. We’ll come back to this problem later.
Polymorphism
So we have a (relatively) simple way of representing a Swift/Objective-C class hierarchy in Rust. Minus a couple of minor details (like how to represent constructors, or when we should write methods as taking &self
, &mut self
, or self
), we could basically extrapolate what we have to all of the classes from Foundation and Cocoa, and our library would be in pretty good shape! There is, however, one important detail we’ve missed, which we can see in this code snippet:
// ... impl NSResponder { fn new() -> Self { // ... initialize a new NSResponder ... unimplemented!(); } } impl NSWindowController { fn new() -> Self { // ... initialize a new NSWindowController ... unimplemented!(); } } fn use_ns_responder(responder: NSResponder) { // ... do something with an `NSResponder` ... } fn main() { let controller = NSWindowController::new(); use_ns_responder(controller); // ^^^^^^^^^^ // expected struct `NSResponder` // found struct `NSWindowController // error: mismatched types }
The problem is that an NSWindowController
is-an NSResponder
, so the equivalent code would work in Objective-C/Swift. Unfortunately, it’s impossible to get the code above to work as intended in Rust, so we have to make some tweaks.
Basically, we need a way to cast from a type that implements IsNSResponder
(a.k.a subclasses of NSResponder
) to a real NSResponder
in the Objective-C runtime.
Presenting… the Duck
trait:
pub trait Duck<T> { // Cast `self` to the subtype `T` fn duck(self) -> T; } impl Duck<NSResponder> for NSWindowController { fn duck(self) -> NSResponder { // Since `NSWindowController` is just a newtype around `NSResponder`, // we can "convert" it by returning the inner `NSResponder` self.super_ } }
Implementing Duck<Foo>
for Bar
says that Bar
can be coerced into a Foo
. Basically, Duck
acts just like the Rust Into
trait, but we’re using it specifically for casting from a superclass to a subclass.
Now, we can tweak our broken example code a bit to get it to compile:
// ... impl NSResponder { fn new() -> Self { // ... initialize a new NSResponder ... unimplemented!(); } } impl NSWindowController { fn new() -> Self { // ... initialize a new NSWindowController ... unimplemented!(); } } fn use_ns_responder(responder: NSResponder) { // ... do something with an `NSResponder` ... } fn main() { let controller = NSWindowController::new(); use_ns_responder(controller.duck()); // yay! no more errors! }
Not impressed? Don’t worry, Duck
will become really important… soon.
Passing Rust types into the Objective-C runtime
Alright, we have a formula for pulling in Objective-C classes into Rust, we can export methods, and we can support polymorphism pretty well! Library done, pack it up, throw it up on crates.io, call it a day!
Okay, not quite. There’s one large unanswered question: how do we export Rust types as classes for the Objective-C runtime? In the beginning, I alluded that something like the following would also work:
1 struct MyResponder { 2 super_: NSResponder 3 } 4 5 impl MyResponder { 6 fn new() -> Self { 7 MyResponder { 8 super_: NSResponder::new() 9 } 10 } 11 } 12 13 // MyResponder inherits from NSResponder 14 impl Object for MyResponder { 15 type Super = NSResponder; 16 17 fn super_ref(&self) -> &Self::Super { 18 &self.super_ 19 } 20 } 21 22 // Override `NSResponder` methods! 23 impl IsNSResponder for MyResponder { 24 fn become_first_responder(&self) -> bool { 25 println!("Called MyResponder::become_first_responder"); 26 false 27 } 28 29 // NOTE: We're only providing impls for the methods 30 // we actually want to override 31 } 32 33 fn use_ns_responder(responder: NSResponder) { 34 // ... do something with an `NSResponder` ... 35 } 36 37 fn main() { 38 let responder = MyResponder::new(); 39 use_ns_responder(responder); 40 }
Just like before, we’d get type error when trying to call use_ns_responder
with MyResponder
(rather than an actual NSResponder
).
So, also like before, let’s add another .duck()
call:
37 fn main() { 38 let responder = MyResponder::new(); 39 use_ns_responder(responder.duck()); 40 }
Now, all we need to do is write a blanket impl
to fulfill the requirement MyResponder: Duck<NSResponder>
(so that MyResponder::duck() -> NSResponder
). Easy, right?
Well, the first thing we need to do is address the issue of a subclass overriding a superclass’s methods. To do this, we’re going to turn on a nightly feature, then we’re going to modify our current blanket IsNSResponder
impl:
#![feature(specialization)] #[macro_use] extern crate objc; use objc::runtime::Object as AnyObject; use objc::runtime::{BOOL, YES}; // Link to Cocoa #[link(name = "Cocoa", kind = "framework")] extern { } // The default impl of `IsNSResponder` for all subclasses impl<T> IsNSResponder for T where T: SubNSResponder { // Allow every method to be specialized, // using the `default` keyword default fn become_first_responder(&self) -> bool { // Forward `become_first_responder` to our superclass self.super_ns_responder().become_first_responder() } }
Normally in Rust, it’s considered an error for two impl
s to overlap. But, with specialization, two or more impl
s are allowed as long as:
- One
impl
is strictly more specific than the others (so that there are no ambiguities for method dispatch) - The conflicting items are marked as
default
in the more generalimpl
Now, subclasses of NSResponder
can override methods by adding an impl NSResponder for _ { ... }
, then adding methods as needed!
Now we can add our Duck
impl for converting MyResponder
to NSResponder
. Here’s what our new duck()
method is going to do:
- Create a new Objective-C class called
CustomResponder
using the ClassDecl type from theobjc
crate (if the class hasn’t already been created). - Add an instance variable (a.k.a an “ivar”, or “field”) to the class, which will hold a
Box<IsNSResponder>
trait object. In a nutshell, a trait object lets us hold any instance of a trait, which can even differ at runtime (in our example, our trait object will always end up holding ourMyResponder
type). - Add Objective-C methods to our class that will just call the appropriate method from our trait object.
- Allocate a new
CustomResponder
with the normalalloc
andinit
messages, then set our trait object instance variable to the object we want. - Return an
NSResponder
with theid
field containing our newCustomResponder
instance.
And here it is translated to code:
use std::mem; use std::os::raw::c_void; use objc::runtime::{Sel, Class, NO}; use objc::declare::ClassDecl; // Our new blanket impl, which will enable us to // call `.duck()` on our `MyResponder` class! impl<T> Duck<NSResponder> for T where T: IsNSResponder { // Must be marked default, so that // NSWindowController's impl doesn't conflict default fn duck(self) -> NSResponder { // No easy tricks like last time... // Try to find the "CustomResponder" class let CustomResponder = match Class::get("CustomResponder") { Some(CustomResponder) => { // The "CustomResponder" class already // exists (which means we've already // created it), so don't recreate it. CustomResponder } None => { // The "CustomResponder" class doesn't // exist, so we need to create it... // (1) let NSObject = Class::get("NSObject").unwrap(); let mut CustomResponder = ClassDecl::new("CustomResponder", NSObject).unwrap(); // First, we add an "instance variable" to our class. // It will be of type `*mut c_void`, but // it will hold a `*mut Box<IsNSResponder>` // in practice. // (2) CustomResponder.add_ivar::<*mut c_void>("_boxed"); // Next, we need add all of the methods our custom class // will respond to. For each method, we need a function // to call (like `impl_becomeFirstResponder`), and // we need to register the method with `.add_method()`. // Here is the function that will be called when our // `CustomResponder` receives the `becomeFirstResponder` // method // (3) extern "C" fn impl_becomeFirstResponder(self_: &mut AnyObject, sel: Sel) -> BOOL { // Get a pointer to our "_boxed" instance variable let boxed: &mut *mut c_void = unsafe { self_.get_mut_ivar("_boxed") }; // Cast it to it's actual type let boxed: &mut *mut Box<IsNSResponder> = unsafe { mem::transmute(boxed) }; // Call the "real" `become_first_responder` method let result = unsafe { (**boxed).become_first_responder() }; // Cast the result to an Objective-C `BOOL` match result { true => YES, false => NO } } let f = impl_becomeFirstResponder as extern "C" fn(&mut AnyObject, Sel) -> BOOL; unsafe { CustomResponder.add_method(sel!(becomeFirstResponder), f); } // Finally, register our new class with the // Objective-C runtime! This returns the class, // so we can send messages to it CustomResponder.register() } }; // Convert `self` to a `*mut Box<IsNSResponder>` let boxed: Box<IsNSResponder> = Box::new(self); let boxed: Box<Box<IsNSResponder>> = Box::new(boxed); let boxed: *mut Box<IsNSResponder> = Box::into_raw(boxed); unsafe { // To create an instance of "CustomResponder", // we call `alloc`, then `init` // (4) let responder: *mut AnyObject = msg_send![CustomResponder, alloc]; let responder: *mut AnyObject = msg_send![responder, init]; // Check that `responder` is not `NULL` (and `panic!` if it is) let responder: &mut AnyObject = responder.as_mut().unwrap(); // Convert our `*mut Box<IsNSResponder>` into a `*mut c_void` let boxed: *mut c_void = mem::transmute(boxed); // Set our "_boxed" instance variable to our pointer responder.set_ivar("_boxed", boxed); // Finally, convert our `&mut AnyObject` into an `NSResponder`! // (5) NSResponder { super_: responder } } } }
Wow, that was a lot to take in! But, do you see what we just did? If not, here’s essentially the class we just created, written in a hybrid of Swift and Rust:
class CustomResponder: NSResponder { var _boxed: Box<IsNSResponder> init(responder: IsNSResponder) { _boxed = Box.new(responder) } func becomeFirstResponder() { _boxed.becomeFirstResponder() } // ... }
Put another way, we just created a class to connect the worlds of Rust and Swift! The “magic” is that our class holds a boxed trait object, and that each message simply ends up dispatching to that boxed trait object!
Epilogue
So, what have we learned? Well, we covered wrapping Objective-C classes as structs and traits, modeling class hierarchies using specialization, and exporting Rust types as Objective-C types with our Duck
trait. This post basically described the first phase of development of sorbet-cocoa
, which, as you may have gathered, is the library I’m working on to make it as easy to write Cocoa apps in Rust as it is in Swift/Objective-C (or nearly as easy, anyway!)
There’s still a ton about sorbet-cocoa
that I didn’t cover in this post, including:
- Memory management with the Objective-C runtime
- Wrapping Objective-C Protocols
- Building macros to reduce duplication even more
- Handling Rust/Objective-C type conversions (like
NSString
from/to&str
) - Working around Rust issue #36587 (it was harder than you’d think!)
…although there’s still a lot of experimentation going on. I still consider sorbet-cocoa
a proof-of-concept at this point (there’s not even any documentation yet!), but if you’re curious about how far along it is, here’s the example app from the beginning of this article, ported to use sorbet-cocoa
so it’ll actually run! That said, there aren’t many classes that have been exported yet, so it’d be hard to build anything beyond that example currently (at the time of writing).
Sorbet: beyond just Cocoa
I believe that most of the core ideas presented in this post aren’t necessarily specific to building Cocoa apps. Such a system could probably be written for other runtimes and GUI frameworks, like GTK+, Qt, or .NET/WinForms/WPF.
Xamarin (C#) and React Native (JavaScript) are two existing systems that are similar to what I’m imagining: a set of platform-specific libraries for writing native mobile and desktop apps with shared business logic. The overall project I’m going to call Sorbet. I believe Rust in this domain has several advantages over what already exists today (Xamarin, React Native, or even a shared C++ codebase):
- Fast, low-level language with a really good cross-platform standard library
- No built-in garbage-collector or other runtime features that would add any unnecessary overhead or latency
- Fast and simple FFI to interop with other languages easily (as long as they expose a C API)
- Awesome zero-cost abstractions!
In other words, a lot of the same reasons Rust is good for other things :)
Footnotes
If you run this snippet, you’ll notice that the menubar’s title doesn’t change. The title of the app’s
mainMenu
object doesn’t actually influence the title in the menubar (butmainMenu
must still have a title, even if it’s meaningless). ↩