#async-write #async-read #async-io #macro-derive #stdio #std

no-std derive-io

derive macros for std::io::{Read,Write}, tokio::io::{AsyncRead,AsyncWrite} and more

12 releases (4 breaking)

0.5.0 Jun 23, 2025
0.4.1 Jun 22, 2025
0.3.3 Jun 21, 2025
0.2.2 May 29, 2025
0.1.1 May 28, 2025

#322 in Rust patterns

Download history 408/week @ 2025-05-24 811/week @ 2025-05-31 899/week @ 2025-06-07 1396/week @ 2025-06-14 2459/week @ 2025-06-21 1253/week @ 2025-06-28 962/week @ 2025-07-05

6,200 downloads per month
Used in 12 crates (3 directly)

MIT/Apache

40KB
622 lines

derive-io

Crates.io Documentation License Build Status

A Rust crate that provides derive macros for implementing sync and async I/O traits on structs and enums (including Tokio, stdlib I/O, and more).

Supported traits

Features

  • Derive most common I/O traits for structs and enums
  • Support for both named and tuple structs
  • Support for enums with multiple variants
  • Support for split read/write streams (ie: two fields provide the read/write halves)
  • Support for generic types
  • Support for duck typing (ie: implementing traits using a method with a "similar" interface)
  • Individual methods can be overridden with custom implementations
  • Support for as_ref or deref attribute on fields to delegate to the inner type
    • Note: for traits requiring a pinned-self (ie: async read/write), the holder type and the outer type must both be Unpin!
  • Pin safety: internal pin projection never allows a &mut to escape, thus upholding any Pin guarantees.

as_ref/deref delegation

Most I/O traits are implemented correctly for Box<dyn (trait)> (that is: they are implemented for Box<T> where T: ?Sized). However, some traits have accidental or intentional additional Sized requirements which prevent automatic delegation from working. Generally this is only required for AsFileDescriptor and AsSocketDescriptor, as most other traits are implemented for themselves on Box<T> where T: Trait + ?Sized.

To uphold Pin safety guarantees, both the inner and outer types must be Unpin.

The as_ref attribute can be used to delegate to the inner type's unwrapped type as_ref/as_mut implementation. The deref attribute can be used to delegate to the inner type's pointee via Deref/DerefMut.

use derive_io::{AsyncRead, AsyncWrite, AsFileDescriptor};

#[cfg(unix)]
trait MyStream: tokio::io::AsyncRead + tokio::io::AsyncWrite 
    + std::os::fd::AsFd + std::os::fd::AsRawFd + Unpin {}
#[cfg(windows)]
trait MyStream: tokio::io::AsyncRead + tokio::io::AsyncWrite 
    + std::os::windows::io::AsHandle + std::os::windows::io::AsRawHandle + Unpin {}

#[derive(AsyncRead, AsyncWrite, AsFileDescriptor)]
pub struct DelegateAsRef {
    #[read]
    #[write]
    // This won't work with #[descriptor] because `AsRawFd` is not implemented for
    // `Box<dyn AsRawFd>`.
    #[descriptor(as_ref)]
    stream: Box<dyn MyStream>,
}

#[derive(AsyncRead, AsyncWrite, AsFileDescriptor)]
pub struct DelegateDeref {
    #[read]
    #[write]
    // This won't work with #[descriptor] because `AsRawFd` is not implemented for
    // `Box<dyn AsRawFd>`. This won't work with #[descriptor(as_ref)] because
    // `as_ref` and `as_mut` on a `Pin` gives you a `Box`.
    #[descriptor(deref)]
    stream: std::pin::Pin<Box<dyn MyStream>>,
}

Overrides

#[read(<function>=<override>)] and #[write(<function>=<override>)] may be specified to redirect a method to a custom implementation.

duck delegation

duck delegation uses non-trait impl methods defined on a type to implement the trait (i.e. "duck typing"). This is useful for when you want to implement a trait for a type that doesn't implement the trait directly, but has methods that are similar to the trait

#[read(duck)] and #[write(duck)] may be specified on the outer type or an inner field.

When using duck delegation, specify the methods to delegate to using the #[duck(...)] attribute:

use derive_io::{AsyncRead, AsyncWrite};
use std::task::{Context, Poll};

#[derive(AsyncRead, AsyncWrite)]
#[duck(poll_read, poll_write, poll_flush, poll_shutdown, poll_write_vectored, is_write_vectored)]
#[read(duck)]
#[write(duck)]
pub struct DuckType {
    inner: tokio::net::TcpStream,
}

impl DuckType {
    pub fn poll_read(
        &mut self,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        todo!()
    }

    // ... poll_write, poll_flush, poll_shutdown, poll_write_vectored, is_write_vectored, etc
}

Examples

Tokio

use tokio::net::*;
use derive_io::{AsyncRead, AsyncWrite, AsSocketDescriptor};

#[derive(AsyncRead, AsyncWrite, AsSocketDescriptor)]
pub enum TokioStreams {
    Tcp(#[read] #[write] #[descriptor] TcpStream),
    #[cfg(unix)]
    Unix(#[read] #[write] #[descriptor] UnixStream),
    Split{ 
        #[read] #[descriptor(as_ref)] read: tokio::net::tcp::OwnedReadHalf, 
        #[write] write: tokio::net::tcp::OwnedWriteHalf,
    },
}

Generic types are supported. The generated implementations will automatically add a where clause to the impl block for each stream type.

use derive_io::{AsyncRead, AsyncWrite};

#[derive(AsyncRead, AsyncWrite)]
pub struct Generic<S> { // where S: AsyncRead + AsyncWrite
    #[read]
    #[write]
    stream: S,
}

Override one method in the write implementation:

use derive_io::{AsyncRead, AsyncWrite};

#[derive(AsyncRead, AsyncWrite)]
pub struct Override {
    #[read]
    #[write(poll_write=override_poll_write)]
    stream: tokio::net::TcpStream,
}

pub fn override_poll_write(
    stm: std::pin::Pin<&mut tokio::net::TcpStream>,
    cx: &mut std::task::Context<'_>,
    buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
    todo!()
}

Dependencies

~0–7.5MB
~56K SLoC