Question on Sendability (Swift 6 data race safety) and FFI interfaces

I'm using Mozilla's UniFFI-rs project for a Rust library to expose it to Swift. It does some code generation and generally works through the FFI layer. A large part of the exposure of types has come across as Swift classes.

When pushing this into Swift 6 compilation, there were a few places where the generated code used the static var pattern for globals that could be easily converted to static let, so that was nailed down. The effort for further Swift 6 compatibility has continued within the project, and there's currently a PR pending that is leaning towards dropping in unchecked @Sendable annotations on the classes provided, which strikes me as a potentially very incorrect approach, but my knowledge of the details is pretty wound-around-the-axel these days, and I just don't know sufficient details.

I'm sure there's folks who are deeply familiar with both Rust and Swift that might be able to advise on how to approach these scenario, or just help provide some advice on how to even think about this problem to assert what should be in place. (There's a bit of pending question of particular rust traits (Send + Sync) that provide some apparently similar mechanisms to what's happening in Swift's setup?)

For anyone deeply familiar with both sides of this equation (Rust & it's data-race safety, Swift and it's declarations and expectations), could you lay out what good (or at least reasonable) expectations should be for a library such as this - providing some convenience bindings across an FFI layer to another language?

(The relevant PR in this project is Add `Sendable` to Swift Templates by martinmose · Pull Request #2318 · mozilla/uniffi-rs · GitHub, but I suspect the topic is useful more broadly, hence asking the question here)

2 Likes

Thanks for asking here based on me pinging you on Github!

1 Like

I think @John_McCall has good Rust skills.

And best would ofc be an answer not here - but on Github in the PR thread :)

1 Like

I’m probably most surprised that you would want to bring a Rust interface over to Swift mostly as classes, but I’ll take it as given.

Swift’s Sendable corresponds to Rust’s Send: ensuring that individual operations on a value are well-ordered must be sufficient to disallow data races, which is of course not true if e.g. the value embeds a shared reference to unsynchronized mutable state. All class types with mutable stored properties do that, so they generally should not be Sendable unless you have some practical guarantee that there can’t actually be any mutation.

1 Like

I’m probably most surprised that you would want to bring a Rust interface over to Swift mostly as classes, but I’ll take it as given.

UniFFI supports exporting of Rust struct as both class (In Rust exported with uniffi::Object) and (swift) struct (In Rust exported with uniffi::Record). Personally I mostly use Record since I prefer value types. But for very large hierarchies of uniffi::Record one hits performance bottle necks. Since there exists a binary format translating between Rust and Swift and many copies incur when "crossing the UniFfi boundary" e.g. for calling a to Swift exported global freestanding function which accepts some uniffi::Record as input. So classes have some merit in those examples.

Furthermore, only uniffi::Object exported Rust structs - Swift classes - support ability to conform to Swift protocols - which themselves are uniffi::exported from Rust traits.

2 Likes

Wrapping in a class is like wrapping in Arc, so I think whether to make the class Sendable comes out to whether Arc<Foo> would be Send in Rust, which is whether Foo is Sync. That’s a simpler rule than the value case, where Send is probably the right check, but maybe not quite because of Swift’s sending, which works even on non-Sendable types…anyway, Sync.

2 Likes

That is correct only for classes with no mutable stored properties. Rust’s Arc<T> forces the object to be immutable (get_mut aside), so it is not an equivalent to a class with mutable properties. Swift allows this but forces them to be non-Sendable (unless you override it with @unchecked).

3 Likes

Ah, right. I was thinking of wrapping a Rust struct directly rather than having several stored properties, but even then it would have to not directly expose &mut self methods on the class to still maintain Sendable. Or a setter, for that matter. (And using several properties wouldn’t change that, as you say; it only changes how easy it is to pass the whole structure across an FFI boundary by reference instead of by serializing.)

Still, if the Rust struct is not Sync, then the Swift class shouldn’t be Sendable.

2 Likes