This is going to be another one of those posts where I did something ridiculous and then show you how I got there, so let’s just get right to it.
use objc_rust::*;
use std::ffi::CStr;
pub fn main() {
#[link(name = "Foundation", kind = "framework")]
extern {}
objc! {
let cls = ObjCClass::lookup("NSNumber\0").unwrap();
let value = [[cls.into() numberWithUnsignedInt:42u32] stringValue];
let result = unsafe { CStr::from_ptr([value UTF8String]) };
println!("string: {}", result.to_string_lossy()); // string: 42
}
}
Yep, that’s Rust code with embedded Objective-C syntax, and it works. Why would you do such a thing? Maybe you want tighter interop between the Rust and Objective-C parts of your iOS app. Maybe you want to write your iOS app entirely in Rust. Or maybe you just wanted to see if it was possible after your colleague’s offhand remark.
(For me, it was the last one, absolutely.)
This post is going to walk through what it took to make this possible, so here’s a table of contents:
Sections 3-5 might actually be useful to other people looking to write Rust macros, so even though this project is a toy, you may still get something out of reading the post. However, if you’re looking to use Objective-C from Rust in production, you should not be using my unsafe toy here. Instead, use Steven Sheldon’s objc crate. Sheldon also has a blog post from near the start of the project that talks about his design process beyond the bare message-send implementation.
That said, if you want to see the full source of my little monstrosity, you can check out the repository.
My normal audience these days is probably Swift developers, but I expect this one to make rounds with at least some Rust people as well. It’d be easy for both groups to not have much direct experience with Objective-C, the primary language used by Apple for both macOS1 and iOS since their releases in 2001 and 2007. So, here’s the quick summary: it’s “just” C, except when you start working with “object” types. And nearly everything you do with those objects is based on a dynamic dispatch model implemented in the runtime. Because of the two parts of that sentence (“dynamic dispatch” and “implemented in the runtime”), people have come up with a lot of clever and powerful techniques to make programs simpler, more expressive, or more extensible, though sometimes at the cost of security, secrecy, and stability.
More relevant to us, however, is that a language whose features are (nearly) all available in a runtime library is a language that’s easy to bridge to dynamically, as long as you don’t have really tight performance constraints. So here’s the deal: nearly everything you do in Objective-C is calling methods by sending messages, and the way this works is that methods are regular C functions stored in a dispatch table keyed by a uniqued string called a selector. It is likely that the most heavily-optimized piece of code in Apple’s libraries is objc_msgSend, which takes a receiver, a selector, and the arguments to the method, does a (cached) lookup of the selector in the receiver’s class’s dispatch table(s), and then jumps directly to the appropriate, polymorphically-selected implementation of the method.
There are more things the Objective-C runtime exposes, but messages are absolutely the most important.
…Oh, one more thing. Because Objective-C is an extension to C, it has to use syntax that doesn’t conflict with C syntax. That means keywords with @ signs in front of them—one of the few symbols on the US keyboard that doesn’t already have a meaning in C—and a unique bracket-based “message send” syntax that takes a while to get used to:
// Objective-C
NSString *fileStr = [[NSString alloc] initWithData:fileContents encoding:NSUTF8StringEncoding];
// Pseudo-Swift
let fileStr: NSString = NSString.alloc().initWithData(fileContents, encoding: NSUTF8StringEncoding)
// Actual Swift
let fileStr = NSString(data: fileContents, encoding: .utf8)
// Pseudo-Rust
let fileStr: *const NSString = NSString::alloc().initWithData(fileContents, NSUTF8StringEncoding)
// Idiomatic-ish Rust
let file_str = NSString::from_data(file_contents, NSStringEncoding::UTF8)
People almost universally think Objective-C syntax is ugly when they first see it, almost universally find it completely normal after a few years, and largely (though not almost universally) find idiomatic Swift easier to read even if they’re used to Objective-C.
From this point on you’ll be expected to read Rust syntax without step-by-step explanations. I’ll try to explain what’s going on for my Swift and other non-Rust readers—I myself am still a relative newcomer to Rust—but it’ll be pretty fast. This is not intended to be an introduction to Rust or Rust macros!
Given that the Objective-C runtime exposes a public API, we should be able to pretty much just call that from Rust, and indeed we can:
use std::ffi::CStr;
#[repr(C)]
struct ObjCObject {
isa: isize
}
#[repr(transparent)]
#[derive(Clone,Copy)]
struct Selector(*const u8);
#[link(name = "objc")]
extern "C" {
fn sel_registerName(name: *const u8) -> Selector;
fn objc_getClass(name: *const u8) -> Option<&'static ObjCObject>;
fn objc_msgSend(); // see below
}
fn main() {
#[link(name = "Foundation", kind = "framework")]
extern {}
// Get a function pointer for transmuting later.
let msg_send = objc_msgSend as unsafe extern "C" fn();
unsafe {
let url_class = objc_getClass("NSURL\0".as_ptr()).unwrap();
let description_sel = sel_registerName("description\0".as_ptr());
let description_method = std::mem::transmute::<_, unsafe extern "C" fn(_, _) -> _>(msg_send);
let description_obj: *const ObjCObject = description_method(url_class, description_sel);
let utf8_sel = sel_registerName("UTF8String\0".as_ptr());
let utf8_method = std::mem::transmute::<_, unsafe extern "C" fn(_, _) -> _>(msg_send);
let utf8_ptr = utf8_method(description_obj, utf8_sel);
println!("{}", CStr::from_ptr(utf8_ptr).to_string_lossy());
}
}
Now, this code should make most Rust users pretty horrified. It starts out okay, declaring some types and then declaring the Rust versions of the C API from libobjc. Then it’s got a funny empty extern block to link the Foundation framework, but sure, that’s fine. And it’s got explicitly-null-terminated strings because that’s what C-based APIs generally use. (Rust doesn’t guarantee null termination by default, which makes slicing a Rust string easier.)