All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
HiyokoMTP transfers files between Android and Mac via MTP. The obvious approach — use libmtp — doesn't work well on macOS. So I wrote a custom MTP implementation. Here's why, and what that looks like.
The libmtp problem on macOS
libmtp is the standard C library for MTP. On Linux, it works well. On macOS, it conflicts with IOKit's USB ownership model.
macOS claims USB devices at the system level. libmtp tries to claim them again at the library level. The result: connection failures, device not found errors, crashes. Unreliable enough to be unusable for a shipping product.
The alternative: nusb
nusb is a pure Rust USB library that works with macOS's IOKit properly. Instead of using libmtp, I implement the MTP protocol directly over USB using nusb.
[dependencies]
nusb = "0.1"
MTP basics in Rust
MTP runs over USB bulk transfer endpoints. The protocol is request-response: send an operation request, receive a response, transfer data.
use nusb::transfer::RequestBuffer;
async fn send_mtp_operation(
interface: &nusb::Interface,
op_code: u16,
params: &[u32],
) -> Result<Vec<u8>, AppError> {
// Build MTP container
let container = build_operation_container(op_code, params);
// Send on bulk-out endpoint
interface.bulk_out(BULK_OUT_EP, container).await?;
// Receive response on bulk-in endpoint
let response = interface
.bulk_in(BULK_IN_EP, RequestBuffer::new(512))
.await?;
parse_mtp_response(&response)
}
File transfer
Large file transfers use chunked bulk reads:
async fn download_file(
interface: &nusb::Interface,
object_handle: u32,
output: &mut impl Write,
) -> Result<(), AppError> {
// Request file data
send_mtp_operation(interface, MTP_OP_GET_OBJECT, &[object_handle]).await?;
// Read data container header
let header = read_data_container_header(interface).await?;
let total_size = header.data_length;
let mut received = 0usize;
// Stream data in chunks
while received < total_size {
let chunk = interface
.bulk_in(BULK_IN_EP, RequestBuffer::new(65536))
.await?;
output.write_all(&chunk)?;
received += chunk.len();
}
Ok(())
}
What this enables
A custom MTP stack means full control over the protocol. Smart resume (check partial transfers, skip completed files), parallel transfers (multiple MTP sessions), conflict detection — all buildable because the stack is yours.
libmtp gives you a C API. A custom stack gives you whatever you need.
Is it worth it?
For a Mac-focused app: yes. The libmtp reliability issues on macOS make it a non-starter. The custom stack took time to build but is stable and fast.
For a cross-platform app where Linux support matters: libmtp on Linux is fine. Use it there, custom on macOS.
TL;DR: libmtp conflicts with macOS's IOKit USB ownership model — don't use it on Mac. Instead, use nusb (pure Rust) and implement the MTP protocol directly over bulk USB transfers. More work upfront, but you get full control: smart resume, parallel sessions, conflict detection.
If this was useful, a ❤️ helps more than you'd think — thanks!
Top comments (0)