Osprey and Swift interop: Mac and iPhone app tutorial

Osprey compiles to native code with a plain C interface (the iOS target specification defines it), so a Swift app can link it like any C library. This video follows one real app, Osprey Storage, across that boundary. Osprey Storage is a disk browser for the Mac and the iPhone: it measures a folder, hidden files included, and ranks what takes up the space. Its logic is written in Osprey and its platform code in Swift.

Osprey and Swift interop: Mac and iPhone app tutorial (3:14). Captions are on; choose a chapter to jump to it.

What the video covers

Interop here means two languages calling each other inside one process. The calls go both ways:

  • Swift calls Osprey. The compiler builds the Osprey project into a static library, libOspreyApp.a, and writes a C header, libOspreyApp.h, beside it. Each Osprey export becomes a C prototype in that header, and Swift calls it through a bridging header.
  • Osprey calls Swift. Osprey declares each host function it needs with extern. Swift defines that function with @_cdecl, so the linker finds exactly that C name. StorageServices.swift holds every one of them.

The video then shows how the app stays testable. Osprey only reaches the Swift functions through an algebraic effect handler at the boundary. A test handler answers the same effect with fixed data, so the whole browser runs in tests with no Swift and no disk. Finally, it shows one Xcode target building the same Osprey code for the Mac, the iPhone and the iPhone simulator.

Every app shot in the video is the real app measuring a staged folder. Every line of code shown is quoted from the examples/macos folder of the repository.

Get it working

You need:

  • An Apple silicon Mac with Xcode, selected with xcode-select. Add the iOS simulator runtime in Xcode if you want the iPhone build.
  • Rust, to build the Osprey compiler.
  • The LLVM/clang toolchain described in Install Osprey.

Clone the repository and run the app from its root:

git clone https://github.com/Nimblesite/osprey.git
cd osprey
make run-mac-hd                    # build the compiler, the Mac runtime and the app, then open it
examples/macos/run.sh ios-sim      # build the same app and launch it in the iPhone simulator

Run the checks the video mentions:

target/release/osprey examples/macos/storage/test --run   # the Osprey tests, with no Swift and no disk
examples/macos/run.sh --smoke                             # the real walk and screens on this Mac

The app opens on your home folder. Choose a folder to rank it, click any folder to open it, and use the eye button (⇧⌘.) to leave hidden files out of every total. Right-click a folder and choose Open in Finder to see it in a Finder window. The app never asks for disk access: it reads only folder metadata, the information Finder shows. The example's README explains how.

Read the walkthrough

Build a Mac and iPhone app with Osprey and Swift writes out the same steps with the full code. For an app that also handles SQLite and HTTP through effects, see iOS and Android apps. Osprey is alpha software.

Transcript

  1. This is Osprey Storage: one app for the Mac and the iPhone. Its logic is written in Osprey, and its platform code in Swift. Here's how the two work together.
  2. The app measures a folder, hidden files included, and ranks what takes up the space.
  3. Open any folder, and it ranks what's inside, all the way down.
  4. The work is split in two. Osprey owns the browser: its state, its events, the ranking, the hidden-file rule, and every screen.
  5. Swift owns what only the platform can do: the window, the folder picker, and the walk over the disk.
  6. Osprey exposes the app as two functions. Start builds the first screen for a home folder.
  7. Dispatch takes the state the last screen came with, and one event, and returns the next screen.
  8. The compiler builds the project for the Mac into a static library, and writes a C header beside it. Every export gets a plain C prototype: Osprey strings become char pointers, and integers become sixty-four bit ints.
  9. Swift includes that header through a bridging header, initializes Osprey once, and asks it for the first screen.
  10. Each tap or toggle goes back through dispatch, with the state Swift was handed, which it keeps as opaque text.
  11. Calls go the other way too. Osprey declares each function it needs from the host with extern.
  12. Storage scan, for example, lists one folder of the measured walk.
  13. The same header lists these as imports, functions the host must define with C linkage.
  14. Here is storage scan's prototype: a C string in, a sixty-four bit int out.
  15. Swift defines it with @_cdecl, so the linker finds exactly that C name and signature.
  16. The body reads the tree Swift measured, and returns how many entries the folder has.
  17. The browser's logic never calls those externs directly. It renders through a Disk Facts effect.
  18. At the boundary, this handler answers that effect with the Swift functions.
  19. That makes the Osprey side testable on its own. The test suite answers the same effect with fixed data.
  20. So the browser and every screen run natively in the tests, with no Swift and no disk.
  21. Each screen Osprey returns is a tree of native nodes. Here, the Hidden Files toggle is a node, with its value and the event it sends.
  22. A shared SwiftUI renderer draws that node as a real toggle, and sends its event straight back to Osprey.
  23. Flip it in the app, and the hidden folders drop out of every total.
  24. One Xcode target builds for the Mac, the iPhone, and the simulator. Its settings name the platform for each SDK.
  25. Then it links the Osprey library built for that platform, and imports its header through the bridging header.
  26. So the same Osprey code runs on iPhone, drawn with the phone's own controls, dark mode and larger text included.
  27. To try it, run make run-mac-hd for the Mac, and examples/macos/run.sh ios-sim for the simulator. Osprey is alpha software, and the full source is in the repository.