Wed 15 September 2021
Earlier this year, I released a DriverKit-based port of GCAdapterDriver. For the most part it's been pretty smooth sailing, once you get accustomed to how things work in a DriverKit-based world rather than Kext-based world. There's one odd bug that's sprouted up recently that only seems to be related to distribution, though - and I've not seen it noted anywhere else, so I figured I'd throw up a quick entry about it in case any other developers start trying to debug this.
The Confusing Bug
The bug is simple (ish) in nature, but only shows up when you're shipping your application. Some quick context: DriverKit-based applications must be in /Applications
in order to load an extension (at least, when a user has System Integrity Protection enabled). If your user doesn't place the app in there and attempts to install the extension, the resulting error will be OSSystemExtensionErrorDomain Code 3
, which roughly corresponds to: Application is not in /Applications.
Now, users of GCAdapterDriver started getting this message even though the app was in the /Applications
folder. Odd, right? I started going through the usual debugging experience, making sure it was installed correctly, and that they had restarted the app after moving it into the folder (if they had started previously). All this turned up fine, though.
So what's going on?
Enter: Quarantine
A fitting title for the past year and some change, I guess.
I recalled from other projects that if the com.apple.quarantine
attribute was on an Application bundle, it could cause some odd errors to occur. This generally happens when an app is downloaded from the internet and flagged; for whatever reason, it sometimes doesn't get cleared when the user trusts the app. I've mostly seen this with Chrome, but I see no reason for it to not happen with any other browser.
In this case, I asked the user to check their status with the following:
xattr /Applications/GCAdapterDriver.app
Which, sure enough, produced the following:
com.apple.quarantine
On a hunch, I asked them to clear the attribute - which you can do with the following:
sudo xattr -r -d com.apple.quarantine /Applications/GCAdapterDriver.app
Lo and behold, after starting the app in a non-quarantined state, the driver activation worked just fine.
Is this really a bug?
It's hard to say. I can see some logic for not activating a quarantined app's driver, but at the very least I think the error should be updated to spot the case where the application is in /Applications
and the quarantine attribute is what's blocking driver activation. I've filed feedback (FB9628611) for this, so here's hoping it gets updated in Monterey at least.
Thu 24 June 2021
Consider the following: you've built a macOS app (perhaps outside of Xcode), signed and notarized it, and see no errors in your build log. This is a common scenario for CI (Continuous Integration) build pipelines.
Now you run the resulting .app
bundle... and the OS screams at you that the bundle is broken, and should be moved to the trash. What gives?
A Green Frog
One project I poke at from time to time - mostly with odd and/or esoteric macOS-related things - is Project Slippi. Earlier this week, they released a new Launcher, which helps with a number of things related to viewing replays, automatic updating, and more. Initial testing across the various Operating Systems seemingly went okay, but on release users started reporting that the macOS build started throwing up the aforementioned "bundle is broken" notification.
Nikhil reached out to me fairly quickly, and we went back and forth for a few minutes debugging. Poking and prodding at the bundle structure brought up nothing special, with nothing out of place (i.e, it was a well-formed bundle). I decided to then see if maybe signing or notarizing had broken somehow. The steps I tend to default to are below, but if you want to do some more reading about the various incantations, I highly recommend this Eclectic Light post.
# Checks and displays signature information
codesign -dvvv /path/to/app
# Checks and displays notarization status
spctl -a -vv -t install /path/to/app
Running the latter ultimately produced something useful to go on: a sealed resource is missing or invalid
.
Resource Oddities
Unfortunately, that error message is a little light on details. Still, we can debug further. Scanning CI build pipeline instructions verified it hadn't been changed, and the output had no warning or error messages. We also reviewed the CI build to make sure the bundle wasn't tampered with after being built/signed/notarized, and this too checked out... so what on earth is this error?
One piece of Apple documentation that never seems to show up in search engines (for me, at least) is how to actually troubleshoot failing signatures. It's a shame, too, because there's an incredibly useful codesign incantation in here that leads right to the error:
codesign --verify \
-vvvv \
-R='anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.1] exists and (certificate leaf[field.1.2.840.113635.100.6.1.2] exists or certificate leaf[field.1.2.840.113635.100.6.1.4] exists)' \
/path/to/the.app
Running the above brings us a slightly confusing (but still useful) error message:
Wat.
Special Character Issues...?
These are files used in showing what stage a game was played on. The app uses the filename as a key to display the appropriate image... and for whatever reason, the accented é
causes issues in signature verification.
Vinceau thankfully had a quick fix: just rename all the images to key on stage ID. After kicking CI to rebuild, the resulting bundle successfully passed signature verification.
This post illustrates a seemingly simple to debug issue, but one that I've found elusive on details for discerning the root cause. Hopefully this helps someone else in the future.
Thu 10 June 2021
Building native macOS apps outside the normal Apple ecosystem is sometimes a not-so-obvious
task. The shifting ecosystem of AppKit, Catalyst, and SwiftUI - coupled with documentation that's
sometimes non-existent, or spanning multiple operating system versions that each have their own nuance -
makes it tricky to know just what you need to tie into.
One language that I've felt hasn't had a great solution to this yet is Rust. I've released an initial version of
Cacao, which is my effort to enable building macOS (and iOS, see below) apps in Rust. This post showcases a bit of what Cacao is, how it works, and includes some opinions on GUI work sprinkled throughout.
History, Rust and Objective-C
I should clarify something before going any further. When I say that Rust hasn't had a great solution to this yet, I mean
that there hasn't been an elegant way to build a macOS app in Rust. There is a litany of prior work though - some of which Cacao uses.
- The objc crate by SSheldon has been around for years now, and does a very good job of providing a way to bridge Rust with Objective-C. Cacao relies on this heavily for bridging with the Apple side of things.
- Delisa Mason wrote about building macOS apps in Rust as far as back as 2016, and the examples repository still holds up.
- The core-foundation crate is often passed around as the "way" to build macOS apps in Rust. As far as I know, this was originally built to help Mozilla and Servo. It's a good crate, but I feel it's not as simple, and doesn't expose certain patterns that help make a macOS app feel "right".
- fruity and objrs both attempt to offer better interop with the Objective-C side of things. Neither is used by Cacao, but I felt they're worth mentioning as they both are decent reading anyway.
Each of these are great works on their own, but none of them contribute to what I feel is a "native" macOS API style in Rust.
Feeling Native
So, let's define native.
When you build a macOS app in the standard, conventional way (AppKit), your primary building block is your Application Delegate. This is effectively where you receive varying messages from the OS (notifications, lifecycle events), and then handle them accordingly. The above examples don't provide a simple way to do this, and feel very procedural. Trying to go against the grain on macOS development often becomes annoying very quickly, and thus I define native as being able to replicate the intended pattern of development.
Let's look at a basic Swift macOS window example. Note that we're doing this in a no-storyboards or interface-builder fashion, as that's what Rust equivalent code will be doing; it's entirely fair to say that this could be shorter if you're the type to use Interface Builder.
import Cocoa
import Foundation
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let frame = CGRect(
x: 10.0,
y: 10.0,
width: 400.0,
height: 400.0
)
let win = NSWindow(contentRect: frame, styleMask: [
.closable, .fullSizeContentView, .miniaturizable, .fullScreen,
.resizable, .unifiedTitleAndToolbar
], backing: .buffered, defer: true)
win.title = "Hello world"
win.makeKeyAndOrderFront(nil)
window = win
}
}
// Just for running the app without all the Xcode @main items.
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
All things considered, that's pretty succinct - even the Objective-C version wouldn't be much longer! What I take note of here is specifically the applicationDidFinishLaunching(_:)
method: this is a delegate method, and is a prime example of what I wanted in Rust. With Cacao, I feel like it gets fairly close:
use cacao::macos::{App, AppDelegate};
use cacao::macos::window::Window;
#[derive(Default)]
struct BasicApp {
window: Option<Window>
}
impl AppDelegate for BasicApp {
fn did_finish_launching(&self) {
let window = Window::default();
window.set_minimum_content_size(400., 400.);
window.set_title("Hello World!");
window.show();
self.window = Some(window);
}
}
fn main() {
App::new("com.hello.world", BasicApp::default()).run();
}
Here, we do roughly the same thing as our Swift example, albeit even more succinct due to being able to take advantage of Default
in Rust. App
wraps NSApplication
, and marshals events and method calls over to the AppDelegate
trait implementation. For macOS, most of the NSApplicationDelegate
methods and events are supported - although I haven't gotten around to wrapping NSNotification
and passing it in to some methods. Pull requests welcome. :)
From here, you could continue with Cacao, or defer to another framework of your choice and simply rely on Cacao for the "mac" pieces. In fact, each Cacao control exposes a public objc
property, which you can lock on and message pass to directly. This enables usage of other Rust wrappers (e.g, Metal), so you're not locked in to any one thing about Cacao.
On Subclassing
Rust doesn't have a concept of a Class
built in to the language, instead favoring more of a composition-based approach with trait implementations. Many existing GUI models are subclass-heavy, though, and working with them can feel a bit odd from the Rust side. gtk-rs provides some utilities for working with subclasses from the Rust side, but what if we could keep it composition-based?
Cacao supports this, in a sense. Most widget types can be declared one of two ways: stock, in which you can call into it and set your properties, and treat it like a normal control - or delegated, where you can provide a struct that implements a trait for a given widget. For example, the View
type could be constructed this way:
let view = View::default();
// Customize your view
This works fine - you could build up a tree from here and slap it wherever you want. If we did it as a delegate pattern, it'd look like this:
#[derive(Debug)]
pub struct MyView {
handle: Option<View>
}
impl ViewDelegate for MyView {
fn did_load(&mut self, view: View) {
// Customize your view
self.handle = Some(view);
}
// You could implement further methods here for handling drop and drop, etc
}
// elsewhere...
let view = View::with(MyView::default());
Feels kind of familiar, right?
A friend of mine once told me over beers: "If you go more than one subclass deep, you've done something wrong". Over the years, I've come around to this point of view, and I feel this approach fits that mantra perfectly - it feels like a subclass, in that you have a place to localize the logic and control setup, implement event handlers, and so on - but you can't go further into subclass hell.
The State of Things
As I'm writing this, Cacao is 0.2.0
. It would be 0.1.0
, but crates.io has a bit of a squatting issue, so 0.1.0
didn't truly exist.
It currently supports enough widgets on macOS to be usable. You can check out the examples folder to see more.
iOS support... it's more of a demo, but there's nothing really blocking controls from being ported other than the time investment.
Who's this even for...?
I don't expect that anybody would say to themselves "alright, time to write/rewrite macOS apps in Rust". I would expect that Cacao would never be able to be as usable as the blessed layers and frameworks that can be found with Swift and Objective-C, and I don't see the crossover between Apple-ecosystem developers and Rust-developers needing GUI tools to be massive.
I do see Cacao being useful for some things, though:
Rust-based Utilities
I think it's conceivable that there are Rust-based utilities (scripts, etc) that could be useful with a GUI, but bridging is (frankly) boring work, and frustrating when it goes wrong. Cacao is useful in this scenario as it enables building a native macOS GUI without having to leave the Rust ecosystem.
My dream for a Rust GUI framework is simple:
- Target only the big three (Windows, macOS, Gnome). Anything else can be community supported. Maybe iOS and/or Android, if the effort isn't too signficant.
- Offering more than the basic controls makes the framework unmaintainable over time. Follow the web model and keep the kitchen sink barebones, even if it's annoying. Long term maintainability > a million widgets.
- Always expose a native hook on each control for the cases where someone knows what they want. AKA, get out of the way when asked.
- A declarative (non-JSX-ish) model. Basically, copy SwiftUI.
I think some combination of winsafe, gtk-rs, and cacao could achieve this. If it happens, I'm happy to repurpose alchemy for this and make it a community thing - it was my original attempt at building a Rust cross-platform GUI, until I was sidelined by life events.
On iOS
A cool thing about Cacao is that the approach can work on iOS (and, presumably, tvOS) too. While support is currently very alpha in comparison to macOS, you are able to build a proper iOS app and run it:
This implements the newer UIScene
API, meaning you should have a modern app that works well across both iOS and iPad (and in particular supports the iPad multi-app capability).
Going Forwards
Part of why I wanted to push out 0.2.0
is for community usage. I'm curious to see how people like it, whether it sees adoption, and so on. The documentation isn't perfect by any means (I welcome pull requests on this, although I'll continue to work on it as well), but I'm hopeful that feedback drives it forward.
In the long (long) term, I think frameworks like this will fade into the background and become building blocks for something like a RustUI (akin to SwiftUI with AppKit/UIKit). This is fine, and I consider it a win if Cacao acts as a bridge between UI generations.