Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce ownership using Swift's consume operator #155

Open
chinedufn opened this issue Feb 3, 2023 · 3 comments
Open

Enforce ownership using Swift's consume operator #155

chinedufn opened this issue Feb 3, 2023 · 3 comments

Comments

@chinedufn
Copy link
Owner

chinedufn commented Feb 3, 2023

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

#### Using an owned value after free
Today, it is possible to pass ownership of a value from `Swift` to `Rust` and then
try to use the value from `Swift`.
This mean that a user can accidentally trigger undefined behavior.
```rust
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
type MyOwnedType;
fn drop(ty: MyOwnedType);
}
}
```
```swift
let myOwnedType = MyOwnedType()
drop(myOwnedType)
// Undefined behavior since we no longer own this value.
drop(myOwnedType)
```
.


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.

@chinedufn
Copy link
Owner Author

chinedufn commented Jun 23, 2023

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:

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 and MyTypeRefMutProtocol
      ## Owned, Ref and RefMut
      When you define a type in an `extern "Rust"` block, three corresponding Swift classes get generated.
      ```swift
      // Equivalent to `SomeType` in Rust
      class SomeType: SomeTypeRefMut {
      // ...
      }
      // Equivalent to `&mut SomeType` in Rust
      class SomeTypeRefMut: SomeTypeRef {
      // ...
      }
      // Equivalent to `&SomeType` in Rust
      class SomeTypeRef {
      // ...
      }
      ```
      • We'll need to figure out whether or not there are any compile time implications to introducing these Swift protocols for every Rust type

@chinedufn
Copy link
Owner Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant