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

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

Open
aiongg opened this issue Apr 24, 2023 · 8 comments
Open

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

aiongg opened this issue Apr 24, 2023 · 8 comments
Labels
good first issue Good for newcomers

Comments

@aiongg
Copy link
Contributor

aiongg commented Apr 24, 2023

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
Copy link
Owner

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
Copy link
Contributor Author

aiongg commented Apr 24, 2023

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
Copy link
Owner

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
Copy link
Contributor Author

aiongg commented Apr 24, 2023

(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
Copy link
Owner

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
Copy link
Owner

Implementation Guide

VecOfSelfAsPtr

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

public protocol Vectorizable {
associatedtype SelfRef
associatedtype SelfRefMut
static func vecOfSelfNew() -> UnsafeMutableRawPointer;
static func vecOfSelfFree(vecPtr: UnsafeMutableRawPointer)
static func vecOfSelfPush(vecPtr: UnsafeMutableRawPointer, value: Self)
static func vecOfSelfPop(vecPtr: UnsafeMutableRawPointer) -> Optional<Self>
static func vecOfSelfGet(vecPtr: UnsafeMutableRawPointer, index: UInt) -> Optional<SelfRef>
static func vecOfSelfGetMut(vecPtr: UnsafeMutableRawPointer, index: UInt) -> Optional<SelfRefMut>
static func vecOfSelfLen(vecPtr: UnsafeMutableRawPointer) -> UInt
}

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

Near here

public func pop () -> Optional<T> {
T.vecOfSelfPop(vecPtr: ptr)
}

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

func testRustVecU8Pop() throws {
let vec = RustVec<UInt8>()
vec.push(value: 123)
let popped = vec.pop()
XCTAssertEqual(popped, 123)
XCTAssertEqual(vec.len(), 0)
}

Implementing as_ptr

We can add the implementation for as_ptr near here

#[export_name = concat!("__swift_bridge__$Vec_", stringify!($ty), "$get")]
#[doc(hidden)]
pub extern "C" fn _get(
vec: *mut Vec<$ty>,
index: usize,
) -> crate::option::$option_ty {
let vec = unsafe { &*vec };
if let Some(val) = vec.get(index) {
crate::option::$option_ty {
val: *val,
is_some: true,
}
} else {
crate::option::$option_ty {
val: $unused_none,
is_some: false,
}
}
}

and here

public static func vecOfSelfPop(vecPtr: UnsafeMutableRawPointer) -> Optional<Self> {{
let val = __swift_bridge__$Vec_{rust_ty}$pop(vecPtr)
if val.is_some {{
return val.val
}} else {{
return nil
}}
}}

Testing

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

cargo test --all && ./test-swift-rust-integration.sh

@chinedufn chinedufn changed the title Pointer-to-pointer? Add RustVec<T>.as_ptr() -> UnsafePointer<T> method Apr 24, 2023
@chinedufn chinedufn added the good first issue Good for newcomers label Apr 24, 2023
@aiongg
Copy link
Contributor Author

aiongg commented Apr 24, 2023

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
Copy link
Contributor Author

aiongg commented Apr 24, 2023

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

#[export_name = concat!("__swift_bridge__$Vec_", stringify!($ty), "$as_ptr")]
#[doc(hidden)]
pub extern "C" fn _as_ptr(vec: *mut Vec<$ty>) -> *const $ty {
let vec = unsafe { &*vec };
vec.as_ptr()
}

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

No branches or pull requests

2 participants