Hello!
I've been chatting about this with several Swift team members over the past few weeks/months, but I figured a post here on the forums was due.
At the Ladybird Browser project we've been experimenting with more and more Swift C++ Interop lately.
In general our goals with Swift fall into a few categories. The first one is that of course, we want to incrementally start writing new code in a memory-safe language. That's the current zeitgeist, and we don't want to be left behind! But how to achieve that is still a bit murky. The main idea is that the best places to add Swift to are places where we are processing untrusted input, and places where fearless concurrency could add some sizeable performance, ergonomics, and correctness gains. At its core a Web Engine is a big event loop with JavaScript only executing from a single Actor. The spec waves its hands and says "do this in parallel", but in practice we just throw everything on the single-threaded event loop anyway.
Prototypes
The first thing I looked at was converting some of our AppKit/Cocoa UI code for macOS into Swift, which was pretty straightforward. It required pulling in a lot of CMake special sauce from Apple's example repo, but hopefully over time more and more of this logic can be pulled into CMake itself.
The next thing I prototyped was rewriting our HTML Tokenizer in Swift. The current version is a straight port of the C++ code, for the most part. I'm sure there's a more idiomatic way to write it. As with all things in Ladybird though, the first priority with all code is to implement the relevant specification exactly as written, and only add optimizations when weird things start showing up in profiles :).
Rewriting existing code is cool and all, but how do we prevent a flood contributors from trying to rewrite the world while we're trying to meet our alpha deadline in two years? The ideal situation would be creating a framework and template for how to start writing new code and new features in Swift.
The Elephant in the Room
The main problem with writing new code is that for the most part, we've already isolated untrusted parsing of data into separate processes. Images are decoded in a special ImageDecoder process that is allowed, and even encouraged to crash and be restarted if any funny business happens when parsing raw image data bytes. All network traffic happens in our RequestServer process, which uses libcurl under the hood. There's still a few places like the HTML tokenizer, the JavaScript parser, font loading, and video decoding that could use some isolation, but in general those parts of the code are already written.
So what sorts of new code are we writing today? Well, for the most part it's going to be CSS parsing and tree-building, layout fixes, and fleshing out the missing Web APIs. LibJS, our custom JS engine, already passes a staggering 95% of test262, the official ECMAScript test suite. LibWasm, our custom WebAssembly interpreter, already passes 100% of the WebAssembly test suite as well.
The elephant in the room about all the areas under active and rapid development is that almost everything in LibWeb is garbage collected. We've placed the memory management of our DOM, our HTML APIs, and everything to support the HTML and CSS specifications (and any other w3c/whatwg specification) under the control of our LibJS (now split into LibGC) garbage collector. Our GC is quite naive, being a single-threaded stop-the-world conservative stack-scan based monolith. But it gets the job done, and we're not doing too bad on the Web Platform Tests these days, passing 1.6M out of 1.85M total tests.
Swift and GC?
I came into this thinking that we would have to do a lot of shenanigans to let Swift types interop with our garbage collector. But I think now that it might not be so bad.
The simplest case is when a C++ type wants to own a Swift type to defer some work to it. Thanks to the existing C++ interop capabilities in Swift 6.0, we don't have to do much of anything. We can just store a Swift type from the generated C++ bridging header as a data member in our C++ type, and presto. Memory managed. Whether it's a value type or an ARC type, the bindings hide that from us.
The more interesting case is when we want Swift types to participate in our GC. In an ideal world, we could add an annotation to our Swift types to say "You are blessed with GC, visit your edges when requested for the mark-and-sweep pass and all will be ok". Unfortunately, none of our contributors are swift-syntax or Macro gurus, so we need to do things the hard way first.
After chatting with the Interop team last week at their office hours, and with @grynspan on the Swift OSS Slack for far too much time this weekend (doesn't he have things to do on the weekend? :P ), we came up with a design that will allow us to import foreign types into our custom GC, and teach swift types how to allocate themselves into a "foreign cell" for ownership by someone more GC-aware than they are.
The PR is here if you'd like to take a look at the cursed FFI we cooked up.
This first cut plays games with void* and other type erasure to store both a this pointer and hold onto a reference for a Swift type's metadata pointer to make a JNI-like API for initializing, finalizing, destroying, and visiting edges of Swift-GC types. I'm looking forward to prototyping some mixed C++/Swift patches that use this new interface to try it out. I'm sure we'll learn along the way how to make it more ergonomic.
If anyone on the forums here has ideas on how to make this task easier, or how to make it look as snazzy as the swift-java work that's going on, I'd love to hear your thoughts! Either here on the forums, or in the # swift channel on the Ladybird discord.
Thanks,
Andrew Kaster