uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

Store return values in RustCallStatus

Open bendk opened this issue 1 year ago • 2 comments

FFI function calls currently use 2 methods to return results:

  • The return value of the function is used for successful results
  • The RustCallStatus out pointer. It has the code field which we check if the call was successful and the error_buf field, which stores error results.

Instead, we could add a union field to RustCallStatus which stores either the return value or the error buf. Maybe we should rename the struct to RustCallResult to represent the new functionality:

#[repr(C)]
pub struct RustCallResult {
    pub code: RustCallStatusCode,
    // Payload data, which varies depending on the code
    // * For RustCallStatusCode::Success, this is the return value of the function.  For void
    //   returns, none of the union fields will be set.
    // * For RustCallStatusCode::Error, `payload.buf` will contain the error value serialized into
    //   a RustBuffer
    // * For RustCallStatusCode::InternalError, `payload.buf` should contain an error message serialized
    //   a RustBuffer.  In extreme cases, where we fail to allocate the error message buf, this
    //   will be an empty Rust buffer.
    //
    // FFI function callers who get a `RustCallResult` back are responsible for extracting the
    // correct value from the `RustCallResultPayload`.  They are also responsible for freeing
    // pointers, RustBuffers, etc.
    pub payload: RustCallResultPayload,
}

#[repr(C)]
union RustCallResultPayload {
    u8: u8,
    // ... more numeric types
    f64: f64,
    ptr: *mut std::ffi::c_void,
    buf: RustBuffer,
}

This would simplify several aspects of bindings generation:

  • C-like languages make a strict distinction between functions that return values and those that don't. This means we can't generate code like {{ return_type }} return_value = scaffolding_call(...);, since it would be illegal for void returns. Instead we need to create a branch in the template logic and handle both cases separately, which gets messy quickly. By storing the return value in RustCallStatus we can eliminate this branch in most places and simplify the code.
  • Python cannot return structs like RustBuffer from callback functions. This means that callback methods must return their values via an out-pointer. If we return values via RustCallStatus then we could use the same FFI for Rust calls and callback interface calls.
  • In general, it seems simpler to return everything in one place than in two places.

I don't think this should give us a significant performance hit, but we should measure that.

bendk avatar Jun 18 '24 00:06 bendk