Portals is a Mac app built in Swift. It’s open source and uses the Ockam Rust library to privately share TCP or HTTP services from your Mac with your friends over End-to-End Encrypted Ockam Portals. A shared service appears on their localhost!
In this post, we'll dig into how the SwiftUI macOS app interacts with Rust code.
If you're curious to try Portals for Mac. You can learn more about it in this article and install is using Homebrew as follows:
brew install build-trust/ockam/portals
Here's a 2 minute video of the application in action:
The Portals functionality was already implemented in the Ockam Rust library. We set out to create a great macOS-native experience.
Our first attempt at building the app was using Tauri. This made sense as we wanted to use the Ockam rust library and most people on our team are comfortable building things in Rust. This first version was easy to build and had all the basic functions we wanted. However, the experience of using the app wasn't great. Tauri only gave us minimal control over how the menu was rendered and what happened when a user interacts with the menu. This version of the app felt like it belonged in a 10 year old version of macOS when compared to super easy to use menubar items built into macOS Sonoma.
We realized that to have the rich experience we want, we must build the app using SwiftUI.
Unfortunately, we couldn't find an off-the-shelf solution, to integrate Swift and Rust, that would give us the best of both worlds; the safety of Rust, and the rich macOS-native experience of SwiftUI. After some more digging we realized we can connect the two using C-89. Rust is compatible with the C calling convention, and Swift is interoperable with Objective-C, which is a superset of C-89.
We wrote the Rust data structures that needed to be visible to Swift twice. One version is idiomatic in Rust and easy to use. The other version is C compatible using pointers and memory that is manually allocated with malloc. We also exposed some C-compatible APIs that use raw-pointers in unsafe rust to convert the idiomatic data structures to their C-compatible versions. Finally we automatically generated a C header with the help of the cbindgen library.
On the Swift side, we could have called the C APIs directly, but C data structures are not first class citizens in Swift. This makes them harder to use idiomatically within SwiftUI code. Instead, we chose to duplicate the data structures in Swift and convert between C and Swift. This may seem burdensome, but practically, the shared state doesn't change very often. The ability to quickly build components in SwiftUI using constructs like if let ...
, ForEach
, enum
etc. is super helpful and worth the tradeoff.
Here's an example of the same structure in its 4 forms:
// Rust idiomatic structure
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct LocalService {
pub name: String,
pub address: String,
pub port: u16,
pub shared_with: Vec<Invitee>,
pub available: bool,
}
// Rust C-compatible structure
#[repr(C)]
pub struct LocalService {
pub(super) name: *const c_char,
pub(super) address: *const c_char,
pub(super) port: u16,
pub(super) shared_with: *const *const Invitee,
pub(super) available: u8,
}
// Generated C header structure
typedef struct C_LocalService {
const char *name;
const char *address;
uint16_t port;
const struct C_Invitee *const *shared_with;
uint8_t available;
} C_LocalService;
// Swift idiomatic structure
class LocalService {
let name: String
@Published var address: String?
@Published var port: UInt16
@Published var sharedWith: [Invitee]
@Published var available: Bool
}
The Swift app is statically linked to our Rust lib at compile time. The data flow is simple: UI interactions are sent from Swift to Rust as actions by calling C APIs, change events are emitted only by Rust, and Swift is notified using callbacks that lead to updates to the UI.
Most code in the SwiftUI views looks just like any other SwiftUI application.
VStack(alignment: .leading, spacing: 0) {
Text(service.sourceName).lineLimit(1)
HStack(spacing: 0) {
Image(systemName: "circle.fill")
.font(.system(size: 7))
.foregroundColor( service.enabled ? (service.available ? .green : .red) : .orange)
if !service.enabled {
Text(verbatim: "Not connected")
} else {
if service.available {
Text(verbatim: service.address.unsafelyUnwrapped + ":" + String(service.port))
} else {
Text(verbatim: "Connecting")
}
}
}
...
If you're curious to learn more, checkout the code for the ockam_app_lib crate and the Portals app in Swift. The Makefile in the swift folder is also a good place to explore how everything is built and linked together.
If you're interested in contributing to Portals for Mac's Swift or Rust code, we add new good first issues every week and love helping new contributors. Join us on the contributors discord.
Also appears here.