swift-bridge icon indicating copy to clipboard operation
swift-bridge copied to clipboard

Support `swift_repr = "class"` to use a Swift class for transparent structs

Open chinedufn opened this issue 1 year ago • 3 comments

NOTE: I haven't thought through this very deeply. Just jotting down some quick notes/ideas that we can flesh out later...


Right now a transparent struct can only be represented using a Rust struct and a Swift struct.

#[swift_bridge::bridge]
mod ffi {
    // Notice the `swift_repr = "struct"
    #[swift_bridge(swift_repr = "struct")]
    struct ReprStruct {
        name: String
    }
}

We want it to also be possible to have a transparent struct show up as a struct on the Rust side and a class on the Swift side.

#[swift_bridge::bridge]
mod ffi {
    // Notice the `swift_repr = "class"
    #[swift_bridge(swift_repr = "class")]
    struct MyReprClassStruct {
        name: String
    }
}

We do not currently have a way to pass a reference to a Swift struct from Swift -> Rust. Whether or not there is a good way to do this will require some research.

We do, however, already support passing a Swift class from Swift -> Rust. This is how opaque types are passed from Swift -> Rust. We get a pointer to the Swift class and pass that pointer over FFI to Rust.

Supporting swift_repr = "class" will unlock some use cases such as supporting Vec<TransparentStruct> and sharing references to transparent structs between languages (both immutably and mutably).

Essentially, when a transparent struct has swift_repr = "class" it should be passed over FFI in the largely same way that we pass opaque types today. The only difference is that the generated Swift class should have accessible fields.

So this:

#[swift_bridge::bridge]
mod ffi {
    // Notice the `swift_repr = "class"
    #[swift_bridge(swift_repr = "class")]
    struct MyReprClassStruct {
        name: Vec<u8>
    }
}

would become something along the lines of:

class MyReprClassStruct {
    var name: RustVec<u8>

    // ...
}

similar to how we generate classes for opaque Rust types

https://github.com/chinedufn/swift-bridge/blob/e7aef343da039d0ff9e2783cf7989443ed39cca3/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type_codegen_tests.rs#L5-L63

chinedufn avatar Mar 15 '23 01:03 chinedufn

Ok I thought through this a bit more. Here's what I'm currently thinking about (work in progress ideas...).

Given the following bridge module:

// Rust

#[swift_bridge::bridge]
mod ffi {
    #[swift_repr = "class"]
    struct User {
        name: String,
        age: u8,
        friend: MyRustFriend
    }

    extern "Rust" {
        type MyRustFriend;
    }
}

We would generate Swift code like:

// Swift

class User {
    var name: RustString
    var age: UInt8
    var friend: MyRustFriend

    public init(name: RustString, age: UInt8, friend: MyRustFriend) {
        self.name = name
        self.age = age
        self.friend = friend
    }
}
class UserRefMut: UserRef {
    public override init(ptr: UnsafeMutableRawPointer) {
        super.init(ptr: ptr)
    }
 
    func name_mut() -> RustStringRefMut {
        __swift_bridge__$User$_name_mut()
    }

    func friend_mut() -> MyRustFriendRefMut {
        __swift_bridge__$User$_friend_mut()
    }
}
class UserRef {
    var ptr: UnsafeMutableRawPointer
    
    public init(ptr: UnsafeMutableRawPointer) {
        self.ptr = ptr
    }
    
    func name() -> RustStringRef {
        __swift_bridge__$User$_name()
    }
    
    func age() -> UInt8 {
        __swift_bridge__$User$_age()
    }

    func friend() -> MyRustFriendRef {
        __swift_bridge__$User$_friend()
    }
}
  • Three classes get generated, class User, class UserRef and class UserRefMut

    • UserRefMut subclasses UserRef, but User does not subclass UserRefMut since User doesn't have a pointer.
    • This is in contrast to opaque Rust types where an OpaqueRustType subclasses OpaqueRustTypeRefMut.
  • We do not generate an age_mut method since we don't currently support &mut u8 in swift-bridge. But we could in the future if we supported it.

    • So for now the age() method returns a UInt8 .. in the future it might return UnsafePointer<UInt8> when we support &u8.
  • If you have a User class instance you can access the fields directly

  • If you have a UserRef or UserRefMut you can call methods to get access to references to the fields.

  • Notably, we could support references in bridge modules such as:

    #[swift_bridge::bridge]
    mod ffi {
        #[swift_repr = "class"]
        struct User {
            name: String,
            age: u8,
            friend: MyRustFriend
        }
    
        extern "Rust" {
            type MyRustFriend;
    
            // Would return `UserRef` on the Swift side.
            fn get_user(&self) -> &User;
        }
    }
    

Hmmm.. I suppose this could maybe work for passing transparent structs from Rust -> Swift ... but what about the other direction ... ?

Need to think about all of this more... I don't like this solution ... But I'll still post it here in the interest of sharing ideas..

chinedufn avatar Mar 22 '23 01:03 chinedufn

Hey @chinedufn, that solution could work, but as you mentioned might have some conversion issues between Swift -> Rust. Plus it might possibly might introduce quite some overhead to get the Swift repr, since you need to first get the UserRef from Rust, then use it to get each property and populate the User object.


I've been thinking about whether it would be possible to instead of returning a pointer to the Rust repr, return the C (FFI) repr? Since the C repr is something that both Swift and Rust understand we should be able to easily convert them on both ways, Rust could look something like:

fn something(* const ffi_repr) {
    let ffi = usafe { &* ffi_repr }
    let rust = ffi.into_rust_repr()
}

And Swift:

func something(pointer: UnsafePointer) {
    let ffi = pointer.load(as: __swift_bridge__ffi.self)
    let swift = ffi.toSwiftRepr()
}

Honestly I'm not totally sure what I'm suggesting makes sense, I just assumed this could work having a look at how Vec support is implemented for Rust Opaque types (_len(vec: *const Vec<super::#ty>) seems to be using a pointer to the Rust opaque repr instead of an FFI pointer, I've done the same-ish on #199 for shared structs).

Plus I have no idea how to PoC this and see how it works out. If you could help out giving some instructions on how we could try this out on a small interface, I could try implementing it so we could validate if the idea works.

rkreutz avatar Mar 23 '23 01:03 rkreutz

Plus it might possibly might introduce quite some overhead to get the Swift repr, since you need to first get the UserRef from Rust, then use it to get each property and populate the User object.

Only if you wanted an owned User object, but if you want to go from a reference to an owned object you'll have to clone it no matter what (assuming it doesn't implement Copy).

I've been thinking about whether it would be possible to instead of returning a pointer to the Rust repr, return the C (FFI) repr?

I'm not really following the advantage here over a pointer?

If you could help out giving some instructions on how we could try this out on a small interface, I could try implementing it so we could validate if the idea works.

Hmm the challenge here is that I don't even have a good idea of how to best do this myself. So, to serve as a good guide I'd need to dive in and mess around a bit.

Ultimately the HOW behind all of this is an internal detail. The developer interface shouldn't really change much (as in, if we change how we solve this problem the bridge module shouldn't need to change much or at all).

So I'd say that the best way to start would be to get some portion of https://github.com/chinedufn/swift-bridge/issues/196#issuecomment-1478784360 working by writing 1 or more integration tests and 1 or more codegen tests.

Then we could iteratively build up from there.

Please feel free to let me know if you have more questions.

chinedufn avatar Apr 10 '23 17:04 chinedufn