chinedufn / swift-bridge

swift-bridge facilitates Rust and Swift interop.
https://chinedufn.github.io/swift-bridge
Apache License 2.0
802 stars 57 forks source link

Natively support generic extern "Rust" opaque types #44

Open chinedufn opened 2 years ago

chinedufn commented 2 years ago

Something like:

struct MyStruct<A, B> {
    field1: A,
    field2: B,
}

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyStruct<u32, String>;

        #[swift_bridge(associated_to = MyStruct<u32, String>)]
        fn new() -> MyStruct<u32, String>;
    }
}
// Swift

let myStruct = MyStruct_u32_String.new();

Note that generics are possible today using type aliases:

struct MyStruct<A, B> {
    field1: A,
    field2: B,
}

type MyStruct_u32_String = MyStruct<u32, String>;

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyStruct_u32_String;
    }
}
Jomy10 commented 2 years ago

Would there be a possibility that we generate an overarching struct in swift, something like

struct MyStruct<A, B> {
    // ...
}

That uses the correct struct for each type of A and B.

Just a thought, not sure how realistic/practical this is or how we would implement it.

Might also just be something to keep in mind for later.

chinedufn commented 2 years ago

Hmm interesting.

So that would look something like:

struct MyStruct<A, B> {
    field1: A,
    field2: B,
}

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        #[swift_bridge(declare_generic)]
        type MyStruct<A, B>;
    }

    extern "Rust" {
        type MyStruct<u32, String>;

        #[swift_bridge(associated_to = MyStruct<u32, String>)]
        fn new() -> MyStruct<u32, String>;
    }
}
// Swift
let myStruct: MyStruct<u32, String> = MyStruct.new();

Where the generated Swift code looked something like:

// This comes from the `#[swift_bridge(declare_generic)]`
class MyStruct<A, B> {
    // ...
}

extension MyStruct where A == UInt32, B == RustString {
    class func new() -> MyStruct<UInt32, RustString> {
        return __swift_bridge__$MyStruct$u32_RustString$new()
    }
}

Definitely seems do-able, and seems a bit more ergonomic. I like the idea.

chinedufn commented 2 years ago

@cozyGalvinism I'm curious about your thoughts on how to best support generics.

Did you find that using type aliases felt better, or would you have preferred to be able to do something like what's been outlined in this issue?

I'm trying to get a good sense of whether or not we should even implement generics support or if we should just stick to type aliases as the recommended way for handling generics.

I'd like to avoid introducing extra complexity unless there's a clear reason to do so.


@Jomy10 if you have any thoughts on this feel free to share as well

Jomy10 commented 2 years ago

Where the generated Swift code looked something like:

// This comes from the `#[swift_bridge(declare_generic)]`
class MyStruct<A, B> {
    // ...
}

extension MyStruct where A == UInt32, B == RustString {
    class func new() -> MyStruct<UInt32, RustString> {
        return __swift_bridge__$MyStruct$u32_RustString$new()
    }
}

That looks right to me.

chinedufn commented 2 years ago

Instead of #[declare(generic)] I think we'd just fill in the generics.

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type MyStruct<u32, String>;
    }
}

This way there isn't a separate way of declaring opaque types.

Also, this approach lets us generate the freeing function that gets called whenever the class MyStruct<UInt32, RustString> instance is dropped on the Swift side.

If anyone is reading this and needs support for generics please let me know. I'm waiting until someone needs generics before implementing a first version of support.

BenJeau commented 2 years ago

(I'm not too familiar with the terms opaque/transparent types) but I think my use case is related to this issue.

How would you use a struct defined outside the swift bridge macro? Ideally I would love that I could do this:

use librespot::metadata::Track;

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type RustApp;

        fn get_track(self: &RustApp, track_id: &str) -> Option<Track>;
    }
}

or

use librespot::{
    metadata::{Track, FileFormat},
    core::{spotify_id::{SpotifyId, FileId, SpotifyAudioType}}
};

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type RustApp;
        type Track;
        type FileFormat;
        type SpotifyId;
        type FileId;
        type SpotifyAudioType;

        fn get_track(self: &RustApp, track_id: &str) -> Option<Track>;
    }
}

As I understand, currently I would need to re-define the structs in the ffi module - right? And since the Track struct contains many other structs, I would need to redefine them also, right? https://docs.rs/librespot-metadata/0.3.1/librespot_metadata/struct.Track.html

chinedufn commented 2 years ago

Hey Ben. This actually isn't related to generics.

Feel free to open a new issue with your questions (as well as some context around what you're trying to be able to do) and I'll be more than happy to help you figure out exactly how to do whatever you are trying to do.

chinedufn commented 2 years ago

https://github.com/chinedufn/swift-bridge/pull/81 adds support for generic Rust types.

So far you can pass them around in function arg/returns but you can't call methods on generic Rust types just yet.

chinedufn commented 2 years ago

More generics support https://github.com/chinedufn/swift-bridge/pull/84

All that's remaining to close this issue is supporting self &self and &mut self methods on generic Rust types.