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

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

Open
chinedufn opened this issue Mar 15, 2023 · 3 comments
Open

Comments

@chinedufn
Copy link
Owner

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

/// Test code generation for an extern "Rust" type.
mod extern_rust_type {
use super::*;
fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
extern "Rust" {
type SomeType;
}
}
}
}
/// Verify that we generate a function that frees the memory behind an opaque pointer to a Rust
/// type.
fn expected_rust_tokens() -> ExpectedRustTokens {
ExpectedRustTokens::Contains(quote! {
#[export_name = "__swift_bridge__$SomeType$_free"]
pub extern "C" fn __swift_bridge__SomeType__free (
this: *mut super::SomeType
) {
let this = unsafe { Box::from_raw(this) };
drop(this);
}
})
}
fn expected_swift_code() -> ExpectedSwiftCode {
ExpectedSwiftCode::ContainsAfterTrim(
r#"
public class SomeType: SomeTypeRefMut {
var isOwned: Bool = true
public override init(ptr: UnsafeMutableRawPointer) {
super.init(ptr: ptr)
}
deinit {
if isOwned {
__swift_bridge__$SomeType$_free(ptr)
}
}
}
public class SomeTypeRefMut: SomeTypeRef {
public override init(ptr: UnsafeMutableRawPointer) {
super.init(ptr: ptr)
}
}
public class SomeTypeRef {
var ptr: UnsafeMutableRawPointer
public init(ptr: UnsafeMutableRawPointer) {
self.ptr = ptr
}
}
"#,
)
}

@chinedufn
Copy link
Owner Author

chinedufn commented Mar 22, 2023

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..

@rkreutz
Copy link
Contributor

rkreutz commented Mar 23, 2023

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.

@chinedufn
Copy link
Owner Author

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 #196 (comment) 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.

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

2 participants