chinedufn / swift-bridge

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

Add `RustVec<T>.as_ptr() -> UnsafePointer<T>` method #214

Open aiongg opened 1 year ago

aiongg commented 1 year ago

I am using protobuf to pass data back and forth between a cross-platform library and client apps on various platforms. I have successfully gotten my code to work without swift-bridge, but I thought I'd give it a go since it would be much easier to automatically generate the Swift Package, and potentially make my code a bit cleaner.

For protobuf, the only data that I pass around is a bucket of bytes. I suppose there are two options:

  1. Do what I did in the manual version and call the native code from Swift with a mutable pointer-to-pointer for taking ownership of the bytes, or
  2. Returning a Vec<u8> from swift as a RustVec<UInt8>, but but then I don't know how to convert that into a Data in Swift for passing back to protobuf.

For option 1, the generated Swift code appears to be wrong:

        #[swift_bridge(associated_to = EngineController, swift_name = "sendCommand")]
        fn send_command(
            &self,
            cmd_input: *const u8,
            len_input: usize,
            cmd_output: *mut *mut u8,
            len_output: *mut usize,
        ) -> i32;
extension EngineControllerRef {
    public func sendCommand(
        _ cmd_input: UnsafePointer<UInt8>,
        _ len_input: UInt,
        _ cmd_output: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>>,
        _ len_output: UnsafeMutablePointer<UInt>) -> Int32 {
        __swift_bridge__$EngineController$send_command(ptr, cmd_input, len_input, cmd_output, len_output)
    }
}
// error
Cannot convert value of type 'UnsafeMutablePointer<UnsafeMutablePointer<UInt8>>'
to expected argument type 'UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>'

In my manual version, I called the function using &UnsafeMutablePointer<UInt8>&, not UnsafeMutablePointer<UnsafeMutablePointer<UInt8>>.

I'm pretty new to Swift, so I'm not sure what all the nuances of these things are, this is just what worked / what didn't work for me.

For reference, here is the working (non swift-bridge) call site corresponding to the same Rust signature as above:

        var lenOutput: UInt = 0
        var cmdOutput: UnsafeMutablePointer<UInt8>?

        let result = bytes.withUnsafeBytes {
            (cmdInput: UnsafeRawBufferPointer) -> Int32 in
            return Rust_khiin_engine_send_command(
                self.engine_ptr,
                cmdInput.baseAddress?.assumingMemoryBound(to: UInt8.self),
                bytes.count,
                &cmdOutput,
                &lenOutput
            )
        }

(Edit: formatting)

chinedufn commented 1 year ago

Thanks for the detailed issue and examples.

I'm not understanding what you mean by Option 2? Can you illustrate what you mean with code like you did for Option 1?


If possible, can you also include a snippet of the code that is using the *mut *mut u8 so that I can better understand your use case?

aiongg commented 1 year ago

I don't have code for Option 2 since I couldn't figure it out, but it would basically be the rust function returning a Vec that I could somehow load into a Data in Swift. Here is the code right now (without swift-bridge), the files aren't very much longer than the code snippets if I were to paste them here, so I figured it's easier just to link directly:

https://github.com/aiongg/khiin-rs/blob/master/swift/EngineBindings/src/lib.rs

https://github.com/aiongg/khiin-rs/blob/master/swift/EngineBindings/EngineController.swift

https://github.com/aiongg/khiin-rs/blob/master/swift/EngineBindings/generated/khiin_swift.h

chinedufn commented 1 year ago

Thanks for the links

I don't have code for Option 2 since I couldn't figure it out

Can you write a snippet showing, roughly, how you would want the bridge module signature to look?

It may sound unnecessary, but I'm asking because I've found that talking in terms of bridge modules reduces cognitive load substantially and gets us on the exact same page.

i.e. something like:

// Rust

#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        // ... the signature that you want to support
    }
}

that I could somehow load into a Data in Swift

Gotcha. It looks like Data's initializer methods just copy bytes from a pointer and length https://developer.apple.com/documentation/foundation/data/1780158-init

So, if you had a way to get the pointer to the first byte in the RustVec<UInt8> (you can already get the length with .len()) would that be enough for you to construct your Data?

aiongg commented 1 year ago

(Sorry I first posted from another device where I was logged in to a different account.)

Sure, well the ideal signature would be bytes-in-bytes-out:

fn send_command(command_bytes: &[u8]) -> Option<Vec<u8>>;

with no length counting or in-out params at all. This is identical to the actual method I use in the cross-platform library, seen here: https://github.com/aiongg/khiin-rs/blob/master/khiin/src/engine.rs#L51

So in that case my FFI code would be one-liner, which would be great.

As I mentioned I'm new to Swift so I'm not positive, but it seems like the pointer and length would be sufficient for a Data.

This would make it very straightforward for anyone else using protobuf to pass arbitrary data between rust and swift without any additional setup, since protobuf is just a serializer/deserializer to bytes.

chinedufn commented 1 year ago

Ok, cool.

In that case, it looks like the signature that you want is already supported, so it sounds like the solution here is to expose a method to get the pointer to the first item in a Vec<T>.

chinedufn commented 1 year ago

Implementation Guide

VecOfSelfAsPtr

We can add a static func VecOfSelfAsPtr(vecPtr: UnsafeMutableRawPointer) -> UnsafePointer<Self> method to the Vectorizable protocol.

https://github.com/chinedufn/swift-bridge/blob/d03f77295fb60bee9f1bbb67da9d1f49bc765966/crates/swift-bridge-build/src/generate_core/rust_vec.swift#L88-L105

Then we can expose this behind RustVec<T>.as_ptr

Near here https://github.com/chinedufn/swift-bridge/blob/fdca9fcc2080a60d30de958fc4ada2516691581e/crates/swift-bridge-build/src/generate_core/rust_vec.swift#L18-L20

Swift Integration Test

We can add a testVecU8AsPtr test where we

  1. Create a RustVec<UInt8>
  2. Push the number 10
  3. Call vec.as_ptr()
  4. Dereference the pointer and assert that the dereferenced value is 10

Here's an example Vec Swift test https://github.com/chinedufn/swift-bridge/blob/b4c5d9bffe2f457822b753a00c93bb3384e8f8ba/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/VecTests.swift#L26-L32

Implementing as_ptr

We can add the implementation for as_ptr near here

https://github.com/chinedufn/swift-bridge/blob/7c1daae88138bb3a9d32f0b0bf731313de95e9cd/src/std_bridge/rust_vec.rs#L67-L85

and here

https://github.com/chinedufn/swift-bridge/blob/fdca9fcc2080a60d30de958fc4ada2516691581e/crates/swift-bridge-build/src/generate_core.rs#L158-L165

Testing

Here's how to run the tests to confirm that the new tests pass:

cargo test --all && ./test-swift-rust-integration.sh
aiongg commented 1 year ago

Wow awesome. I would be up for giving this a shot.

One thing I didn't get on the first read of your answer was how to deal with the &[u8], since the README says &[T] is not yet implemented?

aiongg commented 1 year ago

By the way it looks like we already have as_ptr implemented here:

https://github.com/chinedufn/swift-bridge/blob/7c1daae88138bb3a9d32f0b0bf731313de95e9cd/src/std_bridge/rust_vec.rs#L109-L114