Crate mfio

source ·
Expand description

mfio

Framework for Async I/O Systems

mfio’s mission is to provide building blocks for efficient I/O systems, going beyond typical OS APIs. Originally built for memflow, it aims to make the following aspects of an I/O chain as simple as possible:

  1. Async
  2. Automatic batching (vectoring)
  3. Fragmentation
  4. Partial success
  5. Lack of color (full sync support)
  6. I/O directly to the stack
  7. Using without standard library

This crate provides core, mostly unopiniated, building blocks for async I/O systems. The biggest design assumption is that this crate is to be used for thread-per-core-like I/O systems.

One could view mfio as programmable I/O, because native fragmentation support allows one to map non-linear I/O space into a linear space. This is incredibly useful for interpretation of process virtual address space on top of physical address space. Async operation allows to queue up multiple I/O operations simultaneously and have them automatically batched up by the I/O implementation. This results in the highest performance possible in scenarios where dispatching a single I/O operation incurs heavy latency. Batching queues up operations and issues fewer calls for the same amount of I/O. Partial success is critical in fragmentable context. Unlike typical I/O interfaces, mfio does not enforce sequence of operations. A single packet may get partially read/written, depending on which parts of the underlying I/O space is available. This works really well with sparse files, albeit differs from the typical “stop as soon as an error occurs” model.

Lack of color is not true sync/async mix, instead, mfio is designed to expose minimal set of data for invoking a built-in runtime, with handles of daisy chaining mfio on top of another runtime. The end result is that mfio is able to provide sync wrappers that efficiently poll async operations to completion, while staying runtime agnostic. We found that a single (unix) file descriptor or (windows) handle is sufficient to connect multiple async runtimes together.

Examples

Read primitive values:

use core::mem::MaybeUninit;
use futures::{Stream, StreamExt};
use mfio::backend::*;
use mfio::io::{PacketIo, Write};
use mfio::traits::*;

let handle = SampleIo::new(vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]);

// mfio includes a lightweight executor
handle.block_on(async {
    // Read a single byte
    let byte = handle.read::<u8>(3).await?;
    assert_eq!(2, byte);

    // Read an integer
    let int = handle.read::<u32>(0).await?;
    assert_eq!(u32::from_ne_bytes([0, 1, 1, 2]), int);
    Ok(())
})

Read primitive values synchronously:

use core::mem::MaybeUninit;
use futures::{Stream, StreamExt};
use mfio::backend::*;
use mfio::io::{PacketIo, Write};
use mfio::traits::sync::*;

let handle = SampleIo::new(vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]);

// Read a single byte
let byte = handle.read::<u8>(3)?;
assert_eq!(2, byte);

// Read an integer
let int = handle.read::<u32>(0)?;
assert_eq!(u32::from_ne_bytes([0, 1, 1, 2]), int);

Read structures:

use bytemuck::{Pod, Zeroable};
use core::mem::MaybeUninit;
use futures::{pin_mut, Stream, StreamExt};
use mfio::backend::*;
use mfio::io::{PacketIo, Write};
use mfio::traits::*;

#[repr(C, packed)]
#[derive(Eq, PartialEq, Default, Pod, Zeroable, Clone, Copy, Debug)]
struct Sample {
    first: usize,
    second: u32,
    third: u8,
}

let sample = Sample {
    second: 42,
    ..Default::default()
};

let mut handle = SampleIo::new(bytemuck::bytes_of(&sample).into());

// mfio objects can also be plugged into existing executor.
// `Null` is compatible with every executor, but waking must be done externally.
// There is `Tokio`, compatible with tokio runtime.
// There is also `AsyncIo` - for smol, async-std and friends.
Null::run_with_mut(&mut handle, |handle| async move {
    // Read value
    let val = handle.read(0).await?;
    assert_eq!(sample, val);
    Ok(())
})
.await

Safety

By default mfio is conservative and does not enable invoking undefined behavior. However, with a custom opt-in config switch, enabled by passing --cfg mfio_assume_linear_types to the rust compiler, mfio is able to provide significant performance improvements, at the cost of potential for invoking UB in safe code*.

With mfio_assume_linear_types config enabled, mfio wrappers will prefer storing data on the stack, and if a future waiting for I/O operations to complete is cancelled, a panic! may get triggered. Moreover, if a future waiting for I/O operations to complete gets forgotten using mem::forget, undefined behavior may be invoked, because use-after-(stack)-free safeguards are discarded.

*NOTE: Pin<P> includes a drop guarantee, making this claim technically invalid. In the future releases of mfio, the config switch will be removed and most I/O will be done through stack (see https://github.com/memflow/mfio/issues/2).

Re-exports

Modules

Macros