ParcelFileDescriptor

Binder IPC typically transfers structured data -- integers, strings, parcelables -- but sometimes you need to pass a file descriptor from one process to another. The ParcelFileDescriptor type makes this possible by wrapping an OwnedFd so that it can be serialized into a Binder Parcel, sent across process boundaries, and deserialized on the other side.

Common use cases include sending pipe endpoints to a service so it can stream data back, sharing access to an open file or socket, and implementing dump() for diagnostic output.

Creating a ParcelFileDescriptor

ParcelFileDescriptor::new() accepts any type that implements Into<OwnedFd>, including std::fs::File, OwnedFd, and the file descriptors returned by rustix::pipe::pipe().

#![allow(unused)]
fn main() {
use std::fs::File;

// From an existing File
let file = File::open("/dev/null").unwrap();
let pfd = rsbinder::ParcelFileDescriptor::new(file);

// A pipe, both ends already wrapped (AOSP `createPipe`). Both are
// O_CLOEXEC.
let (read_pfd, write_pfd) = rsbinder::ParcelFileDescriptor::pipe().unwrap();
}

Since 0.10.0 ParcelFileDescriptor also implements From<std::fs::File> and From<OwnedFd>, so the common concrete cases work with plain conversions — let pfd: ParcelFileDescriptor = file.into();. The generic new() covers any other Into<OwnedFd> source.

Sending a File Descriptor to a Service

A typical pattern is to create a pipe, wrap one end in a ParcelFileDescriptor, send it to a service method, and then read from or write to the other end locally.

The following example is based on the test_parcel_file_descriptor test in the rsbinder test suite:

#![allow(unused)]
fn main() {
use std::io::{Read, Write};

let (read_end, write_pfd) = rsbinder::ParcelFileDescriptor::pipe()?;

// Send the write end to the service; it returns a (duplicated) copy
let result_pfd = service.RepeatParcelFileDescriptor(&write_pfd)?;

// Write through the returned file descriptor
(&result_pfd).write_all(b"Hello")?;

// Read from the original pipe's read end
let mut buf = [0u8; 5];
(&read_end).read_exact(&mut buf)?;
assert_eq!(&buf, b"Hello");
}

Because the service duplicates the descriptor before returning it (see the next section), both the caller and the service hold independent handles to the same underlying pipe.

Duplicating File Descriptors in a Service

When a service receives a ParcelFileDescriptor, it usually needs to duplicate the descriptor before returning it or storing it. This avoids ownership conflicts and ensures each side can close its handle independently.

Since 0.10.0 the type has a built-in ParcelFileDescriptor::try_clone() that duplicates the underlying descriptor with fcntl(F_DUPFD_CLOEXEC) (the duplicate is always close-on-exec, matching AOSP ParcelFileDescriptor::dup), so no hand-rolled dup helper is needed. A service method that repeats a file descriptor back to the caller is then straightforward:

#![allow(unused)]
fn main() {
use rsbinder::ParcelFileDescriptor;

fn RepeatParcelFileDescriptor(
    &self,
    read: &ParcelFileDescriptor,
) -> rsbinder::BinderResult<ParcelFileDescriptor> {
    Ok(read.try_clone()?)
}
}

Working with File Descriptor Arrays

AIDL interfaces can accept and return arrays of ParcelFileDescriptor. The pattern for reversing an array -- a common test case -- illustrates how to combine try_clone() with iterator combinators:

#![allow(unused)]
fn main() {
fn ReverseParcelFileDescriptorArray(
    &self,
    input: &[ParcelFileDescriptor],
    repeated: &mut Vec<Option<ParcelFileDescriptor>>,
) -> rsbinder::BinderResult<Vec<ParcelFileDescriptor>> {
    repeated.clear();
    for fd in input {
        repeated.push(Some(fd.try_clone()?));
    }
    input.iter().rev().map(|fd| Ok(fd.try_clone()?)).collect()
}
}

The repeated output parameter receives a copy of the input in the original order, while the return value contains the input in reverse order. Every descriptor is duplicated so that each Vec owns its own set of file handles.

Reading and Writing

ParcelFileDescriptor implements Read and Write — both by value and by reference, as std::fs::File does — so a descriptor you hold or one a service returned is used directly:

#![allow(unused)]
fn main() {
use std::io::{Read, Write};

let (read_end, write_end) = rsbinder::ParcelFileDescriptor::pipe()?;

// Borrowed: the descriptor stays yours, e.g. to send in a parcel.
(&write_end).write_all(b"payload")?;

let mut buf = [0u8; 7];
(&read_end).read_exact(&mut buf)?;
}

They mean what File's do: reading at end of file is Ok(0) — for a pipe, every writing end is gone — and writing into a pipe whose reader has closed is BrokenPipe. flush() is a no-op, since the only buffer involved is the kernel's.

A pipe holds about 64 KB. Writing more than that blocks until someone drains it, so the usual shape is: create the pipe, send the read end, then fill it — from another thread if the same process is doing both.

For tokio, convert once rather than per call:

#![allow(unused)]
fn main() {
use std::os::fd::OwnedFd;

let file = std::fs::File::from(OwnedFd::from(read_end));
let mut async_file = tokio::fs::File::from_std(file);
}

rsbinder does not wrap that: its tokio feature deliberately does not pull tokio/fs.

File Descriptors over RPC

ParcelFileDescriptor also crosses the socket-based RPC transport (the rpc Cargo feature). Unlike kernel binder, FD passing over RPC is opt-in on both ends and fixed per session: the server declares the modes it accepts with RpcServer::set_supported_fd_modes, and the client requests one before connecting with the fd_mode knob on the RpcClientConfig builder (the fd_mode connect option of the entry API). On the android-14+ wire the mode rides the connection header, as in AOSP; an android-13 (v0) session passes no fds. On the r34 wire, between two rsbinder peers only, RpcSession::negotiate_fd_transport agrees the mode after connecting. Descriptors then travel out-of-band via SCM_RIGHTS, which requires a Unix-domain socket. Service and client code using ParcelFileDescriptor is otherwise unchanged. See RPC Transport for details.

Tips

  • Each side owns its own descriptor. Serializing a ParcelFileDescriptor makes the kernel duplicate the fd for the receiver, so the two handles are independent and close order does not matter.

  • ParcelFileDescriptor is not Clone — it wraps an OwnedFd, which owns the descriptor. Use try_clone(), and use it before storing or re-returning a descriptor you received.

  • try_clone() can fail when the process is out of file descriptors. Propagate it with ? rather than unwrap(); a StatusCode converts into Status automatically.