Opening: What’s Hiding in the Closet

Hey friend, let’s talk about something wild today—do you know how your Rust async code actually runs on Windows?

Imagine you have a closet at home. Looks normal during the day, but at night it makes weird noises. You’re too scared to open it because you know there’s a little demon inside. But here’s the thing: this demon does your laundry and folds your clothes every night, and does a pretty good job at it.

That’s exactly what \Device\Afd is in the Rust async ecosystem—an unofficial, undocumented, but universally-used “underground worker” that Microsoft won’t acknowledge. Without it, async runtimes like Tokio and async-std would be dead in the water on Windows.

Why Do We Need This Thing? Let’s Start with a Restaurant

Traditional Approach: One Waiter Per Table

Imagine you run a restaurant. The simplest approach: one table of customers, one dedicated waiter. When customers order, the waiter stands there waiting; when food comes out, the waiter holds the plate waiting for the kitchen; when they’re done eating, the waiter cleans up.

This is blocking I/O + multithreading. If you have a million network connections (a million tables), you need a million waiters. Not only can’t you afford the payroll, but all these waiters bumping into each other (context switching) would turn your restaurant into chaos.

Smart Approach: One Floor Manager Watching All Tables

A smarter approach: hire one floor manager with a walkie-talkie. When any table needs service, the kitchen shouts through the radio, and the floor manager immediately dispatches the nearest waiter. After handling it, the waiter goes back on standby instead of standing around waiting.

This is the core idea of async I/O:

  • Linux/BSD uses epoll/kqueue (the floor manager’s walkie-talkie)
  • Windows should use IOCP (completion ports), but this thing doesn’t mesh well with Rust’s Future model

Windows’ Awkwardness: Wrong Radio Channel

Here’s the problem: Linux’s epoll is “readiness notification"—“Table 3’s customers are ready to order, go over there.”

Windows’ IOCP is “completion notification"—“Table 3’s food has been served, go collect payment.”

Sounds similar? But for Rust’s borrow checker, these are two completely different worlds:

  • epoll mode: You can ask anytime “is this socket ready?” then decide whether to read data
  • IOCP mode: You must hand over the read buffer to the system first, wait for the system to finish reading, then notify you. During this time the buffer can’t move (pinned), and if you change your mind midway (Drop), you need to figure out how to cancel the operation

It’s like ordering food delivery: epoll is the delivery person calling you when they arrive downstairs “I’m here,” and you go down to get it; IOCP is the delivery person shoving the food through your mail slot—you need to keep the slot open, and you can’t change your mind and say “I don’t want it anymore.”

The Solution: Making a Deal with the Devil

Since Windows’ official API doesn’t cut it, we need to roll our own. This is where \Device\Afd makes its grand entrance.

What is AFD?

AFD stands for Auxiliary Function Driver—it’s the low-level implementation of Windows’ network stack. The WinSock API you normally use is just a layer of “makeup” on top of AFD.

It’s like going to the bank: the teller (WinSock) is nice but can only handle standard transactions. If you want to do something special, you need to go directly to the backend system administrator (AFD). Problem is, this administrator isn’t public-facing—you need to sneak in through the side door.

Three-Step Ritual to Summon the Demon

Step 1: Unlock the Forbidden Spells

Windows has a bunch of internal functions starting with Nt* and Rtl*. Microsoft officially warns “don’t use these, bad things will happen.” But there’s no choice—to use AFD, you need to unlock these functions first.

// Dynamically load ntdll.dll to get internal functions (pseudo-code for illustration)
// In practice, using windows-sys crate is safer
use windows_sys::Win32::System::LibraryLoader::{LoadLibraryA, GetProcAddress};

unsafe {
    let ntdll = LoadLibraryA(b"ntdll.dll\0".as_ptr());
    let nt_create_file = GetProcAddress(ntdll, b"NtCreateFile\0".as_ptr());
}

It’s like finding hidden cheat codes in a game—officially not recommended, but they work.

Step 2: Open the Gates of Hell

Use the NtCreateFile function you just unlocked to open the special \Device\Afd device:

// Open AFD device (pseudo-code for illustration)
// Actual implementation requires lots of structs and parameter setup
use std::ptr;

unsafe {
    let mut afd_handle = ptr::null_mut();
    let path = r"\Device\Afd";

    // NtCreateFile needs UNICODE_STRING, OBJECT_ATTRIBUTES and other complex structures
    // Simplified here to show core concept
    let status = NtCreateFile(
        &mut afd_handle,
        GENERIC_READ | GENERIC_WRITE,
        // ... needs 10+ more parameters
    );
}

This step is like opening a portal to hell in Diablo.

Step 3: Sign the Contract

Use I/O Control (IOCTL) to tell AFD what you want:

// First get the socket's "true name" (base handle)
use std::os::windows::io::AsRawSocket;

unsafe {
    // Get base handle through SIO_BASE_HANDLE
    let mut base_handle: usize = 0;
    let mut bytes_returned: u32 = 0;

    WSAIoctl(
        socket.as_raw_socket(),
        SIO_BASE_HANDLE,
        std::ptr::null_mut(),
        0,
        &mut base_handle as *mut _ as *mut _,
        std::mem::size_of::<usize>() as u32,
        &mut bytes_returned,
        std::ptr::null_mut(),
        None,
    );

    // Initiate AFD polling operation
    let mut poll_info = AfdPollInfo {
        timeout: i64::MAX,
        handles: vec![base_handle],
        events: AFD_POLL_RECEIVE | AFD_POLL_SEND,
    };

    DeviceIoControl(
        afd_handle,
        IOCTL_AFD_POLL,
        &poll_info as *const _ as *const _,
        std::mem::size_of_val(&poll_info) as u32,
        // ... output buffer parameters
    );
}

This step is signing a contract with the demon: you tell it “watch these sockets for me, notify me when there’s activity.”

Final Result: Transform IOCP into epoll

With AFD, you can:

  1. Register the AFD handle to an IOCP completion port
  2. Issue IOCTL_AFD_POLL operations for each socket (this operation itself is async)
  3. When a socket is ready, IOCP receives notification
  4. You can handle I/O using “readiness notification” just like Linux’s epoll

It’s like you’ve modified the delivery person: now they call you when they arrive downstairs instead of shoving food through your mail slot.

Real World: How Rust Ecosystem Uses It

Tokio’s Secret Weapon: mio

Tokio uses the mio library under the hood, and mio on Windows relies on AFD.

use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to server
    let mut stream = TcpStream::connect("example.com:80").await?;

    // Looks simple, but under the hood:
    // 1. mio creates AFD handle
    // 2. Registers this TcpStream's socket to AFD
    // 3. When data arrives, AFD notifies mio through IOCP
    // 4. mio wakes up the corresponding Future
    // 5. Your code continues executing

    // Send HTTP request (write_all from AsyncWriteExt)
    stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await?;

    // Read response (read from AsyncReadExt)
    let mut buf = vec![0u8; 1024];
    let n = stream.read(&mut buf).await?;
    println!("Read {} bytes", n);

    // Print first 100 bytes
    println!("{}", String::from_utf8_lossy(&buf[..n.min(100)]));

    Ok(())
}

async-std and smol: Another Path

async-std and smol use the polling library, which previously depended on wepoll (a C language AFD wrapper), and is now migrating to a pure Rust implementation.

Node.js Uses It Too

That’s right, Node.js’s underlying libuv also uses AFD. So this “little demon” not only supports the Rust ecosystem but also the entire JavaScript server-side ecosystem.

If Microsoft dared to remove AFD, Node.js developers worldwide would riot.

Common Pitfalls and Solutions

Pitfall 1: Wine Doesn’t Support It

Wine (the compatibility layer for running Windows programs on Linux) didn’t support AFD for a long time, causing Tokio programs to fail on Wine.

Solution: Latest Wine versions have added AFD support, but if you need to support older Wine versions, you might need to fall back to thread pool solutions.

Pitfall 2: UWP Apps Might Have Issues

UWP (Windows Universal App Platform) has restrictions on using undocumented APIs, theoretically might get rejected from the store.

Solution: If you’re developing UWP apps, best to prepare a backup plan (like simulating async with thread pools).

Pitfall 3: Debugging is Hard

Since AFD has no official documentation, troubleshooting issues is difficult. You can only rely on reverse engineering and community experience.

Solutions:

  • Read mio and polling source code
  • Search GitHub issues for weird problems first
  • If all else fails, use WinDbg to capture kernel call stacks (requires some skill)

Summary: Three Key Points

  1. Why use AFD: Because Windows doesn’t have a “readiness notification” API like Linux’s epoll, and the official IOCP is “completion notification,” incompatible with Rust’s Future model.

  2. How to use AFD: Open the \Device\Afd device through NtCreateFile, issue polling operations with IOCTL_AFD_POLL, and feed results into IOCP completion ports.

  3. Where’s the risk: AFD is an undocumented API, theoretically Microsoft could change it anytime. But in practice, Node.js and Rust ecosystems both use it, so Microsoft is unlikely to touch it.

Next Steps You Can Take

  1. Read source code: Check out mio’s Windows backend implementation on GitHub to see exactly how AFD is called
  2. Write a demo: Try calling AFD directly using the windows-sys crate to feel the low-level “dirtiness”
  3. Compare other platforms: Study Linux’s epoll and BSD’s kqueue to understand why they’re better suited for Rust’s async model

Remember: technology isn’t perfect, only “good enough to work.” AFD might be a “little demon,” but it really did keep Rust async alive on Windows. Sometimes, making a deal with the devil isn’t such a bad thing—as long as you know what you’re doing.


References:

Keyword hints: If you’re searching for related materials, try these keywords:

  • “Rust Windows async internals”
  • “IOCP vs epoll”
  • “Device Afd polling”
  • “Tokio Windows backend”
  • “mio Windows implementation”