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
ParcelFileDescriptormakes the kernel duplicate the fd for the receiver, so the two handles are independent and close order does not matter. -
ParcelFileDescriptoris notClone— it wraps anOwnedFd, which owns the descriptor. Usetry_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 thanunwrap(); aStatusCodeconverts intoStatusautomatically.