swift-bridge
swift-bridge copied to clipboard
Enforce ownership in generated Swift code
Swift recently accepted a proposal for a consume
operator.
The consume
operator will let us declare that a function takes unique ownership of a type https://github.com/apple/swift-evolution/blob/main/proposals/0366-move-function.md?plain=1#L175-L184 .
Any attempts to use the type after it is consumed lead to a compile time error.
This new consume
operator will make it possible for us to solve one of the memory safety issues described in the book https://github.com/chinedufn/swift-bridge/blob/d4f28b31ecedca4b95e15612d8af293726defea1/book/src/safety/README.md?plain=1#L48-L73 .
After the consume
operator lands in stable Swift we can use it in our generated code to enforce ownership.
So, the following bridge module:
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
type Computer
fn eat(&self, meal: Food);
fn shutdown(self);
}
extern "Rust" {
type Food;
}
}
would generate Swift code along the lines of:
// Generated Swift (approximation)
class Computer {
// Notice the `consume`.
func eat(meal: consume Food) { /* */ }
// Notice the `__consuming`, which consumes self.
func __consuming shutdown() { /* */ }
}
I'm not sure when the consume
operator will land in stable Swift, or how we can track its progress.
Swift 5.9 introduces some new ownership features. I haven't thought through what we can do with all of them yet. Linking to some resources here:
-
Looks like a fairly comprehensive overview of some of the new ownership features https://www.hackingwithswift.com/articles/258/whats-new-in-swift-5-9
-
A section of a WWDC talk that briefly discusses some ownership features https://developer.apple.com/videos/play/wwdc2023/10164/?time=1405
Some Early Thoughts
- We should investigate moving from using Swift classes as handles to Rust types to using Swift
~Copyable
structs. This would remove an allocation, since instantiating a Swift class allocates memory but instantiating a Swift struct does not- Right now we use inheritance to allow owned Rust types to be passed to methods take that references. To support this behavior with structs we might need to generate a
MyTypeProtocol
MyTypeRefProtocol
andMyTypeRefMutProtocol
https://github.com/chinedufn/swift-bridge/blob/36565f2c59b4bc438b54fdf5fb5a696ea63348df/book/src/bridge-module/opaque-types/README.md?plain=1#L52-L71- We'll need to figure out whether or not there are any compile time implications to introducing these Swift protocols for every Rust type
- Right now we use inheritance to allow owned Rust types to be passed to methods take that references. To support this behavior with structs we might need to generate a
More resources:
- https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md
- https://github.com/apple/swift-evolution/blob/main/proposals/0366-move-function.md
- https://github.com/apple/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md
I think a good approach would be to introduce Swift compile-time enforced ownership support behind an attribute in the swift-bridge = "0.1.*"
, then after things stabilize we can remove the attribute, make Swift ownership the default behavior and remove our old runtime enforced ownership.
It's possible that this can be done without introducing any breaking changes. I'm not sure.
So, something like this would lead our code generators to emit Swift code that enforces ownership at compile time:
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
#[swift_bridge(__unstable__swift_compile_time_ownership)]
type Computer
fn eat(&self, meal: Food);
fn shutdown(self);
}
}
Then after things stabilize we'd remove the #[swift_bridge(__unstable__swift_compile_time_ownership)]
attribute.
One approach would be to start by just diving in and writing some tests and implementations for some basic cases such as bridging a Rust struct that has a single primitive field, then using what we learn to get a better sense of the extent of the work that is needed and how to best approach it.
@chinedufn
One approach would be to start by just diving in and writing some tests and implementations for some basic cases such as bridging a Rust struct that has a single primitive field,
What is bridging a Rust struct? You mean opaque types?