No FFI survives contact with the integration
France has a long tradition of FFLs and FFIs, so I salute the Foreign Function Libres.
Very happy with the FFI for rpi-imgpatcher, I started working on a beautiful Swift app to set up the Spark-Club IoT fleet. I left the crate in 0.9.x because, in spite of what Codex1 was telling me, I knew the API couldn’t be finalized yet.
And as Claude would say:
You’re absolutely right!
— Claude © Anthropic 2023
So, as promised, let’s go over all the changes to the FFI written while I was integrating it to the Spark-Flasher Swift application.
Everything is a file (except when it’s not)
So far, I was mostly testing the crate through the Patcherfile feature: the crate was handling file opening, reading, writing,… I was assuming the crate would handle everything itself, which would quickly prove wrong: just like the official rpi-imager, an image flasher works better if it writes directly to the device.
Since I already passed a file descriptor from authexec to my Swift app, it should be easy to pass one from Swift to C to Rust, right?
Well, yes.
Thankfully, as long as we’re in userland, a file descriptor is just a native C integer we can pass around. Even on the Rust side, the integration was straightforward: while it’s unsafe2, it’s easy to turn one into a Write interface.
Plus, passing the file descriptor into a Rust File consumes it, so I don’t even have to worry about closing it later in Swift. Clearly, this implicit consumption could only help me later on, right?
The last error doesn’t mean you’re done making mistakes
Obviously, there were a few issues left and right, some stuff not working in spite of my tests,… And of course, the only thing the C API would give me from my errors were integers. Thanks, that’s very helpful.
Rust comes to the rescue again: its tagged enums make it very easy to write uniform errors, and even integrate properly the errors from other modules into my own. But seriously, do I want to read the source for std::io::Error any time I get an error for it?
Again, the solution comes from the giants whose shoulders I’m standing on: there’s always been some kind of get_last_error() function in a lot of C APIs, so let’s return a complete error string that my Swift app could just display in GUI or console…
It’s actually very easy to do: let’s not bother with integers, and when an error is triggered, let’s turn it into a string, which can later be retrieved from the C side as a pointer to the string. But it just brings more issues. Can you believe we went this far into an article on Rust without saying the word “ownership”?
For another crate, I worked on passing memory slices from C to Rust, so the end-user could control memory usage themselves. This taught me a very specific requirement: both sides must know the slice sizes. Which becomes a chicken-and-egg problem: the C side must allocate a string large enough to hold the error it will receive, but it needs to call the Rust API to know the string size. Oh god.
The only pragmatic solution is to get dirty: the Rust side will allocate the error string, and pass it to the Swift side, which will be responsible for calling Rust to free it.
That was very ugly, let’s fence that behind a ffi_debug feature.
Progress Bar goes Brrrr
Inspired by the HTTP protocol (and Rand-ian doctrines), I hate state. Unfortunately, users tend to enjoy knowing whether the 8GB image they’re flashing is at 1% or 99%. Well, I do too, because when testing, I’d rather know if my app is working, or if I ended up somewhere outside of Swift’s main thread.
But Swift’s got closures, I’ve used them a few times in other apps as “callbacks”, it feels like something I can work with. So let’s imagine something simple: I give my C function a callback function that will receive the progress, and I use that to update the GUI state.
On the Rust side, again, it’s beautiful and easy to create a ProgressWriter interface that can be used like other Write interfaces, while storing how many bytes were written, and then passing that value to a callback function, provided by the FFI caller.
This looks all fine and peachy, until Swift reminds me that the C ABI is a harsh mistress, and that while all programmers are born equal in rights, closure functions are not.
Sure, these two look equal:
1
2
3
4
5
6
7
try image.save(toFile: file) { writtenBytes in
// ...
}
try image.save(toFD: fd) { writtenBytes in
// ...
}
One function, one argument, one contextual state,…
Fuck. Me.
Of course, the C ABI has no notion of closures. It only knows function pointers. So while I can pass the written bytes back to Swift, there’s no (easy) way I can pass them back to my UI.
The solution turns out to be the closest thing C has to a closure: a function pointer paired with a void *context:
- On the Swift side, the closure that updates the UI is stored in the object, it’s okay, Swift can do that.
- A static callback is crafted, taking an
u64argument, and avoid *argument. - A pointer to that callback is passed to the FFI side, along with a pointer to the current Swift object.
- On the FFI side, when bytes are written, the callback function must be called with the written bytes, and the pointer to our Swift object.
- Back in Swift, from the
void *pointer, we can recover a reference to our object. - From that object, we can access the stored UI closure, and call it with the written bytes to update the UI.
Which explains the strange prototypes of the FFI:
1
2
3
4
5
6
7
8
9
10
int64_t rpi_image_save_to_file_with_progress(
struct RpiImage *rpi_image,
const char *file,
void (*progress)(uint64_t, void *),
void *context);
int64_t rpi_image_save_to_fd_with_progress(
struct RpiImage *rpi_image,
int32_t file_descriptor,
void (*progress)(uint64_t, void *),
void *context);
A part of me wants to scream at the performance loss that comes with converting a raw pointer into an object reference, but thankfully, we’re “just” doing a very dirty pointer cast. Nothing too extravagant if you’ve ever done one of those “Object Oriented C” projects.
Don’t trust and verify, you must own it
Very pleased with myself, I suddenly remember that the Spark-Club IoT devices that my app will provision will actually run in production, and that until my fear of flying is properly grounded, I don’t want to take a plane to Montréal3 to fix a badly flashed image.
Again, I follow rpi-imager’s guidance: just like it, my flasher should verify if the bytes were properly written to the device. The concept of double-checking a write that triggered no errors is a bit strange but given how fickle microSD cards can be, it’s not that uncommon. In our case, there’s no complicated algorithm: we just read the microSD and compare it against the data we have locally, whether it’s part of the original image, or the FAT we just modified.
This led me to rewrite some of my Rust API, because I was assuming saving would be the last action my struct would do, and I was consuming it, so I had to keep it alive after saving, in order to be able to verify its contents.
This brought the biggest API change: so far, calling Rust to do a save operation would implicitly free all resources. If I wanted to allow users to keep the object around longer, I would have to give them the responsibility to free the object themselves with a C function that would tell Rust to dispose of the allocated resources.
Again, it’s nothing too complicated, and by this point, if I haven’t convinced you to write Rust, I don’t know what to say, maybe you just don’t like writing code? Or maybe your political leaning rejects the concept of ownership, which I can understand, because it’s coming to bite me in the back again.
See, when we passed our file descriptor from Swift to Rust, Rust took ownership of it and closed it when done with it. So we cannot reuse it to verify our device’s contents. Crap.
Thankfully, Unix solved this problem decades ago: dup(2) lets us create another file descriptor pointing to the same underlying device. Rust can own and close one, while Swift keeps the other around for verification.
And since I had gone this far, I could even write a ProgressReader interface to give users a progress bar for the verification!
Ready for release
In the end, after changing how a few methods work, and adding fewer than ten, the integration worked as intended, and the first versions of the Spark-Flasher provisioned the test IoT resting on my desk multiple times.
Just like I love the ELF format for the beauty of its design, I love the design of Foreign function interface and its uses. As a huge advocate that “No single programming language is good for all tasks”, there was no way I would have done the flasher’s UI in Rust4, and I wouldn’t have felt comfortable manipulating an MBR or a FAT partition from Swift5.
While I didn’t have enough feedback on the final tool usage to design my FFI’s API right on the first try, once in the meat of things, it was really easy to adapt it to my tool, giving me the best of both worlds: the system side’s safety is enforced by Rust, while Swift gives me a pretty Mac-first application. And the FFI is the glue that holds them together.
-
I use ChatGPT and Codex as coworkers to challenge my assumptions. ↩︎
-
What’s the matter Rustascean, afraid of some unsafe code? ↩︎
-
Also, I have no reason to go to Canada. If I wanted to vacation somewhere cold, I’d go in my ex’s heart. ↩︎
-
As I’m writing this, we’re not GUI yet: “The roots aren’t deep but the seeds are planted.”. ↩︎
-
Could I even have done it? Maybe there are some, but I couldn’t find any packages handling it, so I probably would’ve had to write it all from scratch myself. ↩︎
Spark-Club
