Foreign -> Rust calls
What happens when code in the foreign language makes a call to Rust? The lifting and lowering docs describe this at a high-level, this document will explore the details.
This document only describes non-async Rust calls. See the Async FFI docs for a description of async calls.
The FFI function
For each exported Rust function, an FFI function is generated.
This extern "C"
function is exported in the Rust library and its what's called by the foreign code.
Different bindings use different techniques to call these functions. For example, Python uses ctypes to call into a dynamic library, while Swift links to a static library and calls the C functions directly.
The signature for the FFI function is:
* Arguments:
* The lowered type for each argument of the Rust function
* A RustCallStatus
out pointer
* Returns:
* For non-unit type returns: the lowered type of the return value for the Rust function
* For unit type returns: void
(i.e. no return value, which is special-cased in C
).
RustCallStatus
The last argument for the FFI function is a pointer to a RustCallStatus
struct.
The FFI function writes to this struct to report errors when calling the Rust function.
Here's the layout for this struct:
#[repr(C)]
pub struct RustCallStatus {
pub code: RustCallStatusCode,
pub error_buf: RustBuffer,
}
#[repr(i8)]
#[derive(Debug, PartialEq, Eq)]
pub enum RustCallStatusCode {
Success = 0,
Error = 1,
UnexpectedError = 2,
Cancelled = 3, // Only used for async calls
}
When the foreign code calls the Rust FFI function, it initializes RustCallStatus.code
to RustCallStatus::Success
and RustCallStatus.error_buf
to an empty RustBuffer. After calling the real Rust function, the generated FFI function writes to this struct:
- If the function returns successfully (i.e. it runs normally and returns
Ok
value forResult
return types), then nothing is written. Since the foreign bindings initializecode=RustCallStatus::Success
, this will be interpreted as a successful call. - If a
Result::Err
value is returned then: - The generated Rust code sets
code
toRustCallStatusCode::Error
- The generated Rust code serializes the error into a
RustBuffer
and stores that inerror_buf
. - The foreign bindings are responsible for deserializing that error into an exception value and throwing that exception.
- A placeholder value is returned (e.g.
0
or an emptyRustBuffer
); - If an unexpected error happens, for example a failure when lifting the arguments, then:
- The generated Rust code sets
code
toRustCallStatusCode::InternalError
- The generated Rust code tries to serialize the error message to a
RustBuffer
and store iterror_buf
. However, it's possible this fails in which caseerror_buf
not be written to. - A placeholder value is returned
- The foreign bindings are responsible throwing some sort of UniFFI internal exception. If possible, this exception should contain the error message.
Method calls
Method calls work the same as function calls, except there's an extra argument for the object handle. This means the arguments are:
- The object handle (
void *
). This is cloned before making the call as described in object references. - The lowered type for each Rust argument
- A
RustCallStatus
out pointer
Panic handling
UniFFI uses std::panic::catch_unwind
to try to catch any panics in the Rust code.
The main reason is to prevent them from moving up the call stack into the foreign language's frames, which would almost certainly lead to a crash.
Panics are treated as unexpected errors as described above.
UniFFI can't always catch panics, for example when the Rust panic handler is set to abort
.