Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

275 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nanopore

A minimalist single-threaded async runtime built on completion-based I/O: io_uring on Linux, and on Windows a mix of I/O Rings (the io_uring-style submission/completion ring on Windows 11 / Server 2022) and classic overlapped I/O where a ring operation isn't available. It exposes async filesystem, network, timer, buffered-I/O, and channel primitives over a small, dependency-light core.

Design

  • Thread-per-core / single-threaded. Each Runtime owns all of its task and I/O state in an Rc<State> and is !Send. There is no work-stealing scheduler and no cross-thread synchronization on the hot path. Blocking work is offloaded to a shared ThreadPool.
  • Completion-based, not readiness-based. Unlike epoll-style runtimes, the kernel owns the buffer for the duration of an operation. Buffers travel with the request (Request → backend → Response), so the API never lets the kernel write into caller-borrowed memory.
  • Interface-owned buffers. The buffered I/O layer (AsyncReader/AsyncWriter) owns its IoBuffer and stores in-flight completions in its own state. Cancelling a future (e.g. via timeout) parks the in-flight op rather than losing data — the next call resumes it. The design follows Zig 0.15's std.Io.Reader/Writer with std::io naming conventions.
  • Cross-platform. Both backends expose the same Poller/Request/Response surface, so the public API is identical on Linux and Windows.

Quick start

use nanopore::Runtime;
use nanopore::time::sleep;
use std::time::Duration;

let rt = Runtime::new().unwrap();
rt.block_on(async {
    println!("waiting...");
    sleep(Duration::from_secs(1)).await;
    println!("done");
});

Copying a file

use nanopore::Runtime;
use nanopore::fs::AsyncFile;
use nanopore::io::{AsyncReader, AsyncWriter, copy};

let rt = Runtime::new().unwrap();
rt.block_on(async {
    let src = AsyncFile::open("src.txt").await.unwrap();
    let dst = AsyncFile::create("dst.txt").await.unwrap();

    let mut reader = AsyncReader::new(&src);
    let mut writer = AsyncWriter::new(&dst);

    // `copy` moves the reader's buffer directly into the writer — no userspace copy.
    copy(&mut reader, &mut writer).await.unwrap();
});

A TCP echo server

use nanopore::Runtime;
use nanopore::net::AsyncTcpListener;
use nanopore::io::{AsyncReader, AsyncWriter};

let rt = Runtime::new().unwrap();
rt.block_on(async {
    let listener = AsyncTcpListener::bind(("127.0.0.1", 8080)).await.unwrap();
    let (client, _addr) = listener.accept().await.unwrap();

    let mut reader = AsyncReader::new(&client);
    let mut writer = AsyncWriter::new(&client);

    let mut buf = Vec::new();
    reader.read_to_end(&mut buf).await.unwrap();
    writer.write_all(&buf).await.unwrap();
    writer.flush().await.unwrap();
});

Tasks are spawned with Runtime::spawn (returning a JoinHandle) or, from inside the runtime, with handle().spawn(...).

Direct buffer access

These three primitives let you work against the interface-owned buffers directly, without bouncing bytes through a temporary Vec:

  • AsyncReader::buffer / fill_buf — borrow the bytes the kernel filled, in place.
  • AsyncReader::consume — mark how many of those bytes you actually used.
  • AsyncWriter::unfilled_mut — borrow the writer's spare capacity and fill it directly, skipping the userspace copy that write_all performs.

This loop uppercases a file while it streams through. The payload never lands in a caller-owned Vec, but transforming from the read buffer into the write buffer still costs one userspace copy per byte:

use nanopore::Runtime;
use nanopore::fs::AsyncFile;
use nanopore::io::{AsyncReader, AsyncWriter};

let rt = Runtime::new().unwrap();
rt.block_on(async {
    let src = AsyncFile::open("src.txt").await.unwrap();
    let dst = AsyncFile::create("dst.txt").await.unwrap();

    let mut reader = AsyncReader::new(&src);
    let mut writer = AsyncWriter::new(&dst);

    loop {
        // Borrow the bytes the kernel already read into the reader's buffer.
        let chunk = reader.fill_buf().await.unwrap();
        if chunk.is_empty() {
            break; // end of stream
        }

        // Reserve spare capacity in the writer and transform straight into it.
        let spare = writer.unfilled_mut(chunk.len());
        let n = spare.len();
        for (slot, &b) in spare.iter_mut().zip(chunk) {
            slot.write(b.to_ascii_uppercase());
        }

        if n == 0 {
            // Writer buffer is full — drain it and retry the same chunk.
            writer.flush().await.unwrap();
        } else {
            // Mark exactly what we moved as consumed.
            reader.consume(n);
        }
    }
    writer.flush().await.unwrap();
});

unfilled_mut hands back &mut [MaybeUninit<u8>] and commits the slice to the buffer immediately, so the kernel's next drain sends precisely what you wrote into it.

When you only read or only write, the data touches exactly one buffer — the very memory the kernel reads from or writes into — so there is no userspace copy at all.

Scanning consumes the read buffer in place (counting newlines, here) without ever copying bytes out:

use nanopore::Runtime;
use nanopore::fs::AsyncFile;
use nanopore::io::AsyncReader;

let rt = Runtime::new().unwrap();
let lines = rt.block_on(async {
    let file = AsyncFile::open("src.txt").await.unwrap();
    let mut reader = AsyncReader::new(&file);

    let mut lines = 0usize;
    loop {
        let chunk = reader.fill_buf().await.unwrap();
        if chunk.is_empty() {
            break;
        }
        let n = chunk.len();
        lines += chunk.iter().filter(|&&b| b == b'\n').count();
        reader.consume(n);
    }
    lines
}).unwrap();

Producing writes computed bytes straight into the writer's buffer — the same memory the kernel later drains:

use nanopore::Runtime;
use nanopore::fs::AsyncFile;
use nanopore::io::AsyncWriter;

let rt = Runtime::new().unwrap();
rt.block_on(async {
    let file = AsyncFile::create("dst.bin").await.unwrap();
    let mut writer = AsyncWriter::new(&file);

    let mut next = 0usize;
    while next < 256 {
        let spare = writer.unfilled_mut(256 - next);
        if spare.is_empty() {
            writer.flush().await.unwrap(); // buffer full — drain and continue
            continue;
        }
        for slot in spare.iter_mut() {
            slot.write(next as u8);
            next += 1;
        }
    }
    writer.flush().await.unwrap();
});

For the read-then-write case, copy is itself zero-copy: rather than copying buffer to buffer like the uppercase loop above, it moves the reader's buffer into the write request, so the bytes pass from one kernel operation to the next untouched.

Cargo features

Feature Default Description
fs Async filesystem APIs (AsyncFile).
net Async TCP/UDP/Unix socket APIs.
time sleep, timeout, and timer futures.
io Buffered I/O layer: IoBuffer, AsyncReader/AsyncWriter, Source/Sink, io::copy, and the data-carrying Request/Response variants.
nightly Unstable std features (abort_unwind, tcp_linger, doc_cfg, Windows metadata APIs) and spawn_blocking_detached.
debug-force-ioring / debug-no-ioring Windows-only knobs to force or disable the I/O-ring backend for testing.

Build a subset with, e.g.:

cargo build --no-default-features --features fs,time

Modules

  • runtimeRuntime construction and event-loop control (block_on, run_once, run_loop).
  • task — task handles, spawning, the thread-local handle(), and cooperative yielding.
  • fsAsyncFile (implements Source/Sink positionally, plus sync std::io::Seek).
  • netAsyncTcpListener/AsyncTcpStream, UDP, and Unix-domain sockets.
  • timesleep, timeout, and related futures.
  • io — completion-based Request/Response protocol and the buffered reader/writer.
  • sync — async channels: oneshot, mpsc, mpmc.
  • threadpoolThreadPool for spawn_blocking (a global pool is the default).

Requirements

  • Rust 1.85 (pinned via rust-toolchain.toml; some APIs are gated behind the nightly feature).
  • Linux with a kernel new enough for the io_uring operations used, or Windows.

About

A minimalist async runtime based on io_uring (Linux) and I/O rings (Windows)

Topics

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages