Byte buffers (&[u8])
A function argument typed &[u8] (or [ByRef] bytes in UDL) lets
Rust borrow a foreign-owned byte buffer for the duration of the
FFI call — no copy is made. This is useful for large payloads
(images, network frames, audio buffers) that would otherwise be
duplicated on every call.
For owned bytes, or for any case the constraints below disallow,
use Vec<u8> / bytes instead.
Usage
In Rust, declare the parameter as a borrow in the usual way:
#[uniffi::export]
pub fn parse_frame(data: &[u8]) -> Frame {
// `data` borrows the foreign buffer; valid for this call only.
...
}
In UDL, mark the parameter [ByRef] bytes:
namespace example {
Frame parse_frame([ByRef] bytes data);
};
Constraints
- Direction.
&[u8]only flows foreign → Rust. A function may take it as an argument; it cannot be returned, used in a callback result, or used as a trait-method return value. - Argument position only.
&[u8]cannot be nested inside a record, sequence, map,Option, orResult. The borrow lives only as long as the FFI call, so structures that could outlive the call are rejected. - Lifetime. The slice is valid only inside the body of the Rust
function. Don't store it, don't hand it to a spawned task, and
don't capture it in async work that outlives the call. If you need
the data afterwards, copy it (
data.to_vec()).
Foreign-language types
Each binding maps [ByRef] bytes to a natural zero-copy type:
| Binding | Foreign type | Note |
|---|---|---|
| Kotlin | java.nio.ByteBuffer |
Must be a direct buffer (ByteBuffer.allocateDirect(...)). |
| Swift | Data |
The call runs inside Data.withUnsafeBytes. |
| Python | bytes or bytearray |
Zero-copy either way; the object's stable internal pointer is used in both cases. |
| Ruby | String(BINARY) |
Uses String with Encoding::BINARY. |
Passing a non-direct ByteBuffer from Kotlin raises an
IllegalArgumentException at the call site, matching JNA's
getDirectBufferPointer requirement.
Mutable buffers (&mut [u8] / [ByMutRef] bytes)
A synchronous function can take &mut [u8] (or [ByMutRef] bytes in
UDL) to borrow a foreign-owned buffer and write to it. The writes land in
the caller's buffer — no copy in or out.
#[uniffi::export]
pub fn fill(buf: &mut [u8]) {
for (i, b) in buf.iter_mut().enumerate() {
*b = i as u8;
}
}
namespace example {
void fill([ByMutRef] bytes buf);
};
Every &[u8] rule above still holds. Two more:
- Synchronous only.
&mut [u8]/[ByMutRef]in anasyncfunction fails to compile (proc-macro) or generate (UDL). An async call can resume on another thread after the caller has moved on and freed the buffer. - Pass a writable buffer. Each binding needs a mutable buffer type and rejects a read-only one.
- Aliasing is the caller's responsibility. Rust borrows the buffer for
the duration of the call, so the usual aliasing rules apply across the FFI:
- Don't pass the same buffer, or overlapping slices, into Rust from separate threads at the same time.
- Don't mutate or free the buffer from another thread while a Rust function is borrowing it.
Each binding maps a mutable argument to a writable type:
| Binding | Foreign type | Note |
|---|---|---|
| Kotlin | direct java.nio.ByteBuffer |
The same direct buffer as [ByRef]; writes land in it. |
| Swift | inout Data |
The call runs inside Data.withUnsafeMutableBytes. |
| Python | bytearray |
bytes can't be used as a mutable argument. |
Ruby doesn't support &mut [u8].