$ ls ~kylewlacy

Rust <3 Swift: Writing Cocoa Apps in Rust (Eventually)

#rust #macos

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:

1extern crate new_cocoa as cocoa; // Our new library!
2
3struct AppDelegate {
4 super_: cocoa::NSObject, // (1)
5 app: cocoa::NSApplication,
6 controller: cocoa::NSWindowController
7}
8
9// (2)
10impl cocoa::Object for AppDelegate {
11 type Super = cocoa::NSObject;
12
13 fn super_ref(&self) -> &Self::Super {
14 &self.super_
15 }
16}
17
18impl 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)
29impl 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
48struct WindowController {
49 super_: cocoa::NSWindowController // (1)
50}
51
52// (2)
53impl cocoa::Object for WindowController {
54 type Super = cocoa::NSWindowController;
55
56 fn super_ref(&self) -> &Self::Super {
57 &self.super_
58 }
59}
60
61impl 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)
78impl cocoa::IsNSWindowController for WindowController {
79 fn new_with_coder(coder: cocoa::NSCoder) -> Self {
80 panic!("Not implemented");
81 }
82}
83
84fn 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:

  1. 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.
  2. The Object trait (lines 10 and 53). There’s nothing special about the aforementioned super_ field, so we use the Object trait to signal that we want to use it for “inheritance”. We’re going to go more in-depth on how this works later :)
  3. IsNSApplicationDelegate (line 29) and IsNSWindowController (line 78). We’ll talk about this more later, but the core idea is that we separate a class’s “instance type” (named like NSThing) from it’s trait “interface” (named like IsNSThing).
  4. 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:

1import Cocoa
2
3let menu = NSMenu(title: "Hello!")
4let app = NSApplication.sharedApplication()
5app.mainMenu = menu
6app.setActivationPolicy(.Regular)
7app.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 message alloc to the NSMenu class. Then, instantiate it by sending the initWithTitle: message (which sets the class up appropriately).
  • line 4: Get a reference to the current app by sending the sharedApplication message to the NSApplication class.
  • line 5: Set the app’s main menu by sending the setMainMenu: message to the app object
  • line 6: Send the setActivationPolicy: message to the app object
  • line 7: Run the app by sending the run message to the app 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:

  1. The (useless-looking) let statements. We use these to annotate the return type of a method, which msg_send! needs to know to call the correct version of objc_msgSend.
  2. 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 send alloc to create a new instance of a class, or when we call sharedApplication to get our NSApplication).

There’s also two additional important details in the above that I should mention:

  1. All objects are of type *mut Object (a.k.a id if you’re writing Objective-C, and AnyObject 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!
  2. Classes are objects too! We can get a class object using the Class::get(...) method, then we can send messages to it using msg_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 of NSResponder (this is necessary because of this Rust issue).
  • We automatically implement IsNSResponder for all NSResponder subclasses by forwarding to the superclass’s impls 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:

1struct MyResponder {
2 super_: NSResponder
3}
4
5impl MyResponder {
6 fn new() -> Self {
7 MyResponder {
8 super_: NSResponder::new()
9 }
10 }
11}
12
13// MyResponder inherits from NSResponder
14impl 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!
23impl 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
33fn use_ns_responder(responder: NSResponder) {
34 // ... do something with an `NSResponder` ...
35}
36
37fn 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:

37fn 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 impls to overlap. But, with specialization, two or more impls are allowed as long as:

  1. One impl is strictly more specific than the others (so that there are no ambiguities for method dispatch)
  2. The conflicting items are marked as default in the more general impl

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:

  1. Create a new Objective-C class called CustomResponder using the ClassDecl type from the objc crate (if the class hasn’t already been created).
  2. 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 our MyResponder type).
  3. Add Objective-C methods to our class that will just call the appropriate method from our trait object.
  4. Allocate a new CustomResponder with the normal alloc and init messages, then set our trait object instance variable to the object we want.
  5. Return an NSResponder with the id field containing our new CustomResponder 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

  1. 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 (but mainMenu must still have a title, even if it’s meaningless).