$ ls ~kylewlacy


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:

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:

1
import Cocoa
2
3
class AppDelegate: NSObject, NSApplicationDelegate {
4
    let app: NSApplication
5
    let controller: NSWindowController
6
7
    init(app: NSApplication) {
8
        self.app = app
9
        self.controller = WindowController()
10
    }
11
12
    func applicationDidFinishLaunching(_: NSNotification) {
13
        controller.showWindow(nil)
14
        app.activateIgnoringOtherApps(true)
15
16
        print("Application launched!")
17
    }
18
19
    func applicationWillTerminate(_: NSNotification) {
20
        print("Application terminated!")
21
    }
22
23
    func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
24
        return true
25
    }
26
}
27
28
class WindowController: NSWindowController {
29
    required init(coder: NSCoder) {
30
        fatalError("Not implemented")
31
    }
32
33
    init() {
34
        let rect = NSMakeRect(0, 0, 480, 320)
35
        let style = NSTitledWindowMask
36
                  | NSClosableWindowMask
37
                  | NSResizableWindowMask
38
        let window = NSWindow(contentRect: rect,
39
                              styleMask: style,
40
                              backing: .Buffered,
41
                              defer: false)
42
        window.title = "App"
43
        super.init(window: window)
44
    }
45
}
46
47
let app = NSApplication.sharedApplication()
48
let delegate = AppDelegate(app: app)
49
app.delegate = delegate
50
51
app.setActivationPolicy(.Regular)
52
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:

  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:

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:

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:

1
#[macro_use] extern crate objc;
2
use std::ffi::CString;
3
use objc::runtime::{Object, Class};
4
5
// Link to Cocoa
6
#[link(name = "Cocoa", kind = "framework")]
7
extern { }
8
9
fn main() {
10
    unsafe {
11
        // Convert "Hello!" to an `NSString`
12
        let title = CString::new("Hello!").unwrap();
13
        let title = title.as_ptr();
14
15
        let NSString = Class::get("NSString").unwrap();
16
        let title: *mut Object = msg_send![NSString, stringWithUTF8String:title];
17
18
        // `title` is now an `NSString` that holds
19
        // the string "Hello!", so proceed as normal
20
21
        let NSMenu = Class::get("NSMenu").unwrap();
22
        let menu: *mut Object = msg_send![NSMenu, alloc];
23
        let menu: *mut Object = msg_send![menu, initWithTitle:title];
24
25
        let NSApplication = Class::get("NSApplication").unwrap();
26
        let app: *mut Object = msg_send![NSApplication, sharedApplication];
27
28
        let _: () = msg_send![app, setMainMenu:menu];
29
30
        let _: () = msg_send![app, setActivationPolicy: 0 /* .Regular */];
31
32
        let _: () = msg_send![app, run];
33
    }
34
}

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:

1
#[macro_use] extern crate objc;
2
use objc::runtime::Object as AnyObject;
3
4
// Link to Cocoa
5
#[link(name = "Cocoa", kind = "framework")]
6
extern { }
7
8
struct NSWindowController {
9
    id: *mut AnyObject
10
}
11
12
trait IsNSWindowController {
13
    // [self windowDidLoad]
14
    fn window_did_load(&self) {
15
        // `windowDidLoad` does nothing unless a subclass overrides it
16
    }
17
18
    // ... every other selector `NSWindowController` responds to ...
19
}
20
21
impl IsNSWindowController for NSWindowController {
22
    fn window_did_load(&self) {
23
        unsafe {
24
            // Send `windowDidLoad` to the inner Objective-C object
25
            msg_send![self.id, windowDidLoad];
26
        }
27
    }
28
29
    // ...
30
}

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:

1
#[macro_use] extern crate objc;
2
use objc::runtime::Object as AnyObject;
3
use objc::runtime::{BOOL, YES};
4
5
// Link to Cocoa
6
#[link(name = "Cocoa", kind = "framework")]
7
extern { }
8
9
struct NSResponder {
10
    id: *mut AnyObject
11
}
12
13
trait IsNSResponder {
14
    // [self becomeFirstResponder]
15
    fn become_first_responder(&self) -> bool {
16
        // Default implementation returns true
17
        true
18
    }
19
20
    // ... every other selector `NSResponder` responds to ...
21
}
22
23
impl IsNSResponder for NSResponder {
24
    fn become_first_responder(&self) -> bool {
25
        unsafe {
26
            // Send `becomeFirstResponder` to the object and
27
            // convert the result to a `bool`
28
            let result: BOOL = msg_send![self.id, becomeFirstResponder];
29
            result == YES
30
        }
31
    }
32
33
    // ...
34
}
35
36
struct NSWindowController {
37
    id: *mut AnyObject
38
}
39
40
trait IsNSWindowController: IsNSResponder {
41
    // [self windowDidLoad]
42
    fn window_did_load(&self) {
43
        // default windowDidLoad impl does nothing
44
    }
45
46
    // ... every other selector `NSWindowController` responds to ...
47
}
48
49
impl IsNSResponder for NSWindowController {
50
    fn become_first_responder(&self) -> bool {
51
        unsafe {
52
            // Send `becomeFirstResponder` to the object and
53
            // convert the result to a `bool`
54
            let result: BOOL = msg_send![self.id, becomeFirstResponder];
55
            result == YES
56
        }
57
    }
58
59
    // ...
60
}
61
62
impl IsNSWindowController for NSWindowController {
63
    fn window_did_load(&self) {
64
        unsafe {
65
            // Send `windowDidLoad` to the inner Objective-C object
66
            msg_send![self.id, windowDidLoad];
67
        }
68
    }
69
70
    // ...
71
}

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:

1
struct NSWindowController {
2
    id: NSResponder
3
}
4
impl IsNSResponder for NSWindowController {
5
    fn become_first_responder(&self) -> bool {
6
        // Forward along the call...
7
        self.id.become_first_responder()
8
    }
9
10
    // ...
11
}
12
13
impl IsNSWindowController for NSWindowController {
14
    fn window_did_load(&self) {
15
        unsafe {
16
            // Send `windowDidLoad` to the inner Objective-C object
17
            msg_send![self.id.id, windowDidLoad];
18
        }
19
    }
20
21
    // ...
22
}

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:

1
#[macro_use] extern crate objc;
2
use objc::runtime::{BOOL, YES};
3
use objc::runtime::Object as AnyObject;
4
5
// Link to Cocoa
6
#[link(name = "Cocoa", kind = "framework")]
7
extern { }
8
9
// A trait used to indicate that a type is a "class"
10
trait Object {
11
    // The "superclass" this type inherits from
12
    type Super;
13
14
    // Get a pointer to the superclass
15
    fn super_ref(&self) -> &Self::Super;
16
}
17
18
struct NSResponder {
19
    super_: *mut AnyObject
20
}
21
22
impl Object for NSResponder {
23
    // `NSResponder` doesn't really have a superclass in our example,
24
    // so we use `*mut AnyObject` as a sort of "base class".
25
    type Super = *mut AnyObject;
26
27
    fn super_ref(&self) -> &Self::Super {
28
        &self.super_
29
    }
30
}
31
32
trait IsNSResponder {
33
    // [self becomeFirstResponder]
34
    fn become_first_responder(&self) -> bool {
35
        // Default implementation returns true
36
        true
37
    }
38
39
    // ... every other selector `NSResponder` responds to ...
40
}
41
42
// A trait that indicates that a type is a subclass
43
// of `NSResponder` specifically
44
trait SubNSResponder {
45
    type SuperNSResponder: IsNSResponder;
46
47
    fn super_ns_responder(&self) -> &Self::SuperNSResponder;
48
}
49
50
// Automatically implement `SubNSResponder`
51
// for all `NSResponder` subclasses
52
impl<T> SubNSResponder for T
53
    where T: Object, T::Super: IsNSResponder
54
{
55
    type SuperNSResponder = T::Super;
56
57
    fn super_ns_responder(&self) -> &Self::SuperNSResponder {
58
        self.super_ref()
59
    }
60
}
61
62
// The base impl of `IsNSResponder`, which all
63
// subclasses will fallback to
64
impl IsNSResponder for NSResponder {
65
    fn become_first_responder(&self) -> bool {
66
        unsafe {
67
            // Send `becomeFirstResponder` to the object and
68
            // convert the result to a `bool`
69
            let result: BOOL = msg_send![self.super_, becomeFirstResponder];
70
            result == YES
71
        }
72
    }
73
74
    // ...
75
}
76
77
// The default impl of `IsNSResponder` for all subclasses
78
impl<T> IsNSResponder for T
79
    where T: SubNSResponder
80
{
81
    fn become_first_responder(&self) -> bool {
82
        // Forward `become_first_responder` to our superclass
83
        self.super_ns_responder().become_first_responder()
84
    }
85
}
86
87
struct NSWindowController {
88
    super_: NSResponder
89
}
90
91
impl Object for NSWindowController {
92
    type Super = NSResponder;
93
94
    fn super_ref(&self) -> &Self::Super {
95
        &self.super_
96
    }
97
}
98
99
trait IsNSWindowController: IsNSResponder {
100
    // [self windowDidLoad]
101
    fn window_did_load(&self) {
102
        // default windowDidLoad impl does nothing
103
    }
104
105
    // ... every other selector `NSWindowController` responds to ...
106
}
107
108
// `IsNSResponder` is now automatically
109
// implemented for `NSWindowController`!
110
111
impl IsNSWindowController for NSWindowController {
112
    fn window_did_load(&self) {
113
        unsafe {
114
            // Send `windowDidLoad` to the inner Objective-C object
115
            msg_send![self.super_.super_, windowDidLoad];
116
        }
117
    }
118
119
    // ...
120
}

To summarize the above:

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:

1
// ...
2
3
impl NSResponder {
4
    fn new() -> Self {
5
        // ... initialize a new NSResponder ...
6
        unimplemented!();
7
    }
8
}
9
10
impl NSWindowController {
11
    fn new() -> Self {
12
        // ... initialize a new NSWindowController ...
13
        unimplemented!();
14
    }
15
}
16
17
fn use_ns_responder(responder: NSResponder) {
18
    // ... do something with an `NSResponder` ...
19
}
20
21
fn main() {
22
    let controller = NSWindowController::new();
23
    use_ns_responder(controller);
24
    //               ^^^^^^^^^^
25
    //               expected struct `NSResponder`
26
    //               found struct `NSWindowController
27
    // error: mismatched types
28
}

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:

1
pub trait Duck<T> {
2
    // Cast `self` to the subtype `T`
3
    fn duck(self) -> T;
4
}
5
6
impl Duck<NSResponder> for NSWindowController {
7
    fn duck(self) -> NSResponder {
8
        // Since `NSWindowController` is just a newtype around `NSResponder`,
9
        // we can "convert" it by returning the inner `NSResponder`
10
        self.super_
11
    }
12
}

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:

1
// ...
2
3
impl NSResponder {
4
    fn new() -> Self {
5
        // ... initialize a new NSResponder ...
6
        unimplemented!();
7
    }
8
}
9
10
impl NSWindowController {
11
    fn new() -> Self {
12
        // ... initialize a new NSWindowController ...
13
        unimplemented!();
14
    }
15
}
16
17
fn use_ns_responder(responder: NSResponder) {
18
    // ... do something with an `NSResponder` ...
19
}
20
21
fn main() {
22
    let controller = NSWindowController::new();
23
    use_ns_responder(controller.duck());
24
    // yay! no more errors!
25
}

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:

1
fn main() {
2
    let responder = MyResponder::new();
3
    use_ns_responder(responder.duck());
4
}

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:

1
#![feature(specialization)]
2
3
#[macro_use] extern crate objc;
4
use objc::runtime::Object as AnyObject;
5
use objc::runtime::{BOOL, YES};
6
7
// Link to Cocoa
8
#[link(name = "Cocoa", kind = "framework")]
9
extern { }
10
// The default impl of `IsNSResponder` for all subclasses
11
impl<T> IsNSResponder for T
12
    where T: SubNSResponder
13
{
14
    // Allow every method to be specialized,
15
    // using the `default` keyword
16
    default fn become_first_responder(&self) -> bool {
17
        // Forward `become_first_responder` to our superclass
18
        self.super_ns_responder().become_first_responder()
19
    }
20
}

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:

1
use std::mem;
2
use std::os::raw::c_void;
3
use objc::runtime::{Sel, Class, NO};
4
use objc::declare::ClassDecl;
5
6
// Our new blanket impl, which will enable us to
7
// call `.duck()` on our `MyResponder` class!
8
impl<T> Duck<NSResponder> for T
9
    where T: IsNSResponder
10
{
11
    // Must be marked default, so that
12
    // NSWindowController's impl doesn't conflict
13
    default fn duck(self) -> NSResponder {
14
        // No easy tricks like last time...
15
16
        // Try to find the "CustomResponder" class
17
        let CustomResponder = match Class::get("CustomResponder") {
18
            Some(CustomResponder) => {
19
                // The "CustomResponder" class already
20
                // exists (which means we've already
21
                // created it), so don't recreate it.
22
                CustomResponder
23
            }
24
            None => {
25
                // The "CustomResponder" class doesn't
26
                // exist, so we need to create it...
27
28
                // (1)
29
                let NSObject = Class::get("NSObject").unwrap();
30
                let mut CustomResponder =
31
                    ClassDecl::new("CustomResponder", NSObject).unwrap();
32
33
                // First, we add an "instance variable" to our class.
34
                // It will be of type `*mut c_void`, but
35
                // it will hold a `*mut Box<IsNSResponder>`
36
                // in practice.
37
38
                // (2)
39
                CustomResponder.add_ivar::<*mut c_void>("_boxed");
40
41
                // Next, we need add all of the methods our custom class
42
                // will respond to. For each method, we need a function
43
                // to call (like `impl_becomeFirstResponder`), and
44
                // we need to register the method with `.add_method()`.
45
46
                // Here is the function that will be called when our
47
                // `CustomResponder` receives the `becomeFirstResponder`
48
                // method
49
50
                // (3)
51
                extern "C" fn impl_becomeFirstResponder(self_: &mut AnyObject,
52
                                                        sel: Sel)
53
                    -> BOOL
54
                {
55
                    // Get a pointer to our "_boxed" instance variable
56
                    let boxed: &mut *mut c_void = unsafe {
57
                        self_.get_mut_ivar("_boxed")
58
                    };
59
60
                    // Cast it to it's actual type
61
                    let boxed: &mut *mut Box<IsNSResponder> = unsafe {
62
                        mem::transmute(boxed)
63
                    };
64
65
                    // Call the "real" `become_first_responder` method
66
                    let result = unsafe {
67
                        (**boxed).become_first_responder()
68
                    };
69
70
                    // Cast the result to an Objective-C `BOOL`
71
                    match result {
72
                        true => YES,
73
                        false => NO
74
                    }
75
                }
76
77
                let f = impl_becomeFirstResponder as extern "C" fn(&mut AnyObject, Sel) -> BOOL;
78
                unsafe {
79
                    CustomResponder.add_method(sel!(becomeFirstResponder), f);
80
                }
81
82
                // Finally, register our new class with the
83
                // Objective-C runtime! This returns the class,
84
                // so we can send messages to it
85
86
                CustomResponder.register()
87
            }
88
        };
89
90
        // Convert `self` to a `*mut Box<IsNSResponder>`
91
        let boxed: Box<IsNSResponder> = Box::new(self);
92
        let boxed: Box<Box<IsNSResponder>> = Box::new(boxed);
93
        let boxed: *mut Box<IsNSResponder> = Box::into_raw(boxed);
94
95
        unsafe {
96
            // To create an instance of "CustomResponder",
97
            // we call `alloc`, then `init`
98
            // (4)
99
            let responder: *mut AnyObject = msg_send![CustomResponder, alloc];
100
            let responder: *mut AnyObject = msg_send![responder, init];
101
102
            // Check that `responder` is not `NULL` (and `panic!` if it is)
103
            let responder: &mut AnyObject = responder.as_mut().unwrap();
104
105
            // Convert our `*mut Box<IsNSResponder>` into a `*mut c_void`
106
            let boxed: *mut c_void = mem::transmute(boxed);
107
108
            // Set our "_boxed" instance variable to our pointer
109
            responder.set_ivar("_boxed", boxed);
110
111
            // Finally, convert our `&mut AnyObject` into an `NSResponder`!
112
            // (5)
113
            NSResponder {
114
                super_: responder
115
            }
116
        }
117
    }
118
}

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:

1
class CustomResponder: NSResponder {
2
    var _boxed: Box<IsNSResponder>
3
4
    init(responder: IsNSResponder) {
5
        _boxed = Box.new(responder)
6
    }
7
8
    func becomeFirstResponder() {
9
        _boxed.becomeFirstResponder()
10
    }
11
12
    // ...
13
}

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:

…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):

In other words, a lot of the same reasons Rust is good for other things :)


  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).