Home > front end >  How to pass out parameter to function from Swift FFI?
How to pass out parameter to function from Swift FFI?

Time:05-08

Let's say I have a function defined in Rust, which looks like this:

#[no_mangle]
pub unsafe extern "C" fn do_something(
    my_value: *mut MyStruct,
    some_param: c_uint,
    content: *mut *mut u8,
    length: *mut c_uint,
    capacity: *mut c_uint,
) -> *mut MyStruct {
    // Do something and obtain an `ffi_result`...

    let heap_data = ffi_result.as_mut_ptr();

    // These values are passed as "out" parameters.
    *length = ffi_result.len() as c_uint;
    *capacity = ffi_result.capacity() as c_uint;
    *content = heap_data;

    // We intentionally "leak" this data to the heap.
    // The caller is responsible for cleaning it up by calling another function.
    std::mem::forget(ffi_result);

    std::boxed::Box::into_raw(value_of_type_my_struct)
}

It takes in a pointer to a struct, a simple integer parameter, several out parameters that can later be used to create an Array and it returns a pointer to a struct.

Now I compile the rust library into a static library for the target aarch64-apple-ios. I set up a XCode project, add the static library as a dependency as explained here with an "Objective-C Bridging Header" where I import the following header file

#ifndef libmy_project_h
#define libmy_project_h

#include <stdint.h>

struct myStruct;

struct myStruct *do_something(struct myStruct *state, int someParam, char **content, int *length, int *capacity);

#endif

Up until this point everything seems to work fine and I have already successfully used this procedure for a whole bunch of other functions. However in this special case I can not figure out how to call this function from swift. I need to call the function from swift and pass content, length and capacity as out parameters so that I can later use the pointers to create an Array in Swift like so.

This is what I tried so far:

var content = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>(UnsafeMutablePointer(bitPattern: 0))
var length = UnsafeMutablePointer<Int32>(bitPattern: 0)
var capacity = UnsafeMutablePointer<Int32>(bitPattern: 0)
let my_struct = do_something(my_struct, Int32(some_param), content, length, capacity)
        
let buffer = UnsafeRawBufferPointer(start: content?.pointee, count: Int(length!.pointee))
var data = Array(repeating: UInt8(0), count: Int(length!.pointee))
data.withUnsafeMutableBytes { arrayPtr in
    arrayPtr.copyBytes(from: buffer)
}

However now when I execute this swift snippet, I get an EXC_BAD_ACCESS error, which I think occurs because the pointers I manually created do not belong to the adress space of my application. How can I create pointers that I can use as out parameters?

P.S. For reference here is the same interop code in C#:

[DllImport("my_dll")]
        private static extern IntPtr do_something(IntPtr state, uint someParam, out IntPtr content, out uint length, out uint capacity);

Which can be called like so:

IntPtr contentPointer;
uint length, capacity;
IntPtr my_struct = do_something(state, myParam, out contentPointer, out length, out capacity);

byte[] rawContent = new byte[length];
Marshal.Copy(contentPointer, rawContent, 0, (int)length);

// Free the data owned by rust with another FFI call:
free_do_something_result(contentPointer, length, capacity);

CodePudding user response:

var length = UnsafeMutablePointer<Int32>(bitPattern: 0)

You need to pass storage for your out-parameters. This is defining a null pointer. When Rust tries to write the result to address 0, it crashes, since you don't have access to write there.

Instead of creating two layers of pointers, create a value of the type you want, and then pass the address (&) of that value; this will add the extra layer of pointer automatically.

// Create storage
var content: UnsafeMutablePointer<CChar>? // Could be NULL, so Optional
var length: Int32 = 0
var capacity: Int32 = 0

// Pass as references
do_something(&content, &length, &capacity)

// Copy the data
let data = Array(UnsafeRawBufferPointer(start: content, count: Int(length)))

content is still a pointer here because the thing being updated is a pointer. You're not providing storage for content, Rust is. But you do need to provide storage for the pointer (and that's what this does).

I can't compile your code because it's missing a lot (an MCVE would be much better here), so I can't test that this is doing exactly what you mean, but it should be close.

In your example, you're leaking the memory, but since your C# calls free_do_something_result (which I assume cleans it up), I assume you're actually doing the same in the Swift.

  • Related