开场白:衣柜里藏着什么

哥们儿,今天咱们聊点刺激的——你知道你写的 Rust 异步代码在 Windows 上是怎么跑起来的吗?

想象一下,你家里有个衣柜,平时看着挺正常的,但每到晚上就会传出奇怪的声音。你不敢打开看,因为你知道里面藏着个小恶魔。但神奇的是,这个小恶魔每天晚上都在帮你洗衣服、叠被子,干得还挺利索。

这就是 \Device\Afd 在 Rust 异步生态里的角色——一个微软官方不承认、不文档化、但全世界都在偷偷用的"地下工作者"。没有它,Tokio、async-std 这些异步运行时在 Windows 上基本就歇菜了。

为什么需要这玩意儿?先从餐厅说起

传统做法:一个服务员对一桌客人

假设你开了家餐厅,最简单的做法是:来一桌客人,雇一个服务员专门伺候这一桌。客人点菜的时候,服务员就站旁边等着;上菜的时候,服务员端着盘子等厨房出餐;吃完了,服务员收拾桌子。

这就是阻塞式 I/O + 多线程的模式。如果你有一百万个网络连接(一百万桌客人),那就得雇一百万个服务员。工资发不起不说,光是这些服务员互相挤来挤去(上下文切换),餐厅就乱套了。

聪明做法:一个大堂经理盯着所有桌

更聪明的做法是:雇一个大堂经理,手里拿个对讲机。哪桌客人需要服务了,厨房那边喊一声,大堂经理立刻派最近的服务员过去处理。服务员处理完了,继续待命,不用傻站着等。

这就是异步 I/O的核心思想:

  • Linux/BSD 用的是 epoll/kqueue(大堂经理的对讲机)
  • Windows 本来应该用 IOCP(完成端口),但这玩意儿和 Rust 的 Future 模型八字不合

Windows 的尴尬:对讲机频道不对

问题来了:Linux 的 epoll 是"就绪通知"——“3 号桌的客人准备好点菜了,你过去吧”。

而 Windows 的 IOCP 是"完成通知"——“3 号桌的菜已经上完了,你去收钱吧”。

听起来差不多?但对于 Rust 的借用检查器来说,这是两个完全不同的世界:

  • epoll 模式:你可以随时问"这个 socket 准备好了吗?",然后自己决定要不要读数据
  • IOCP 模式:你得先把读数据的缓冲区交给系统,等系统读完了再通知你。这期间缓冲区不能动(pinned),而且如果你中途不想要了(Drop),还得想办法取消操作

这就像你点了外卖,epoll 是外卖员到楼下给你打电话"我到了",你下去拿;IOCP 是外卖员直接把外卖塞进你家门缝里,你得保证门缝一直开着,而且不能中途改主意说"我不要了"。

解决方案:和恶魔做交易

既然 Windows 官方的 API 不给力,那就得自己动手了。这时候,\Device\Afd 闪亮登场。

AFD 是什么?

AFD 全称 Auxiliary Function Driver(辅助功能驱动),是 Windows 网络栈的底层实现。你平时用的 WinSock API,其实只是 AFD 上面的一层"化妆品"。

就像你去银行办业务,柜台小姐姐(WinSock)态度很好,但只能办理标准业务。如果你想搞点特殊操作,就得直接找后台的系统管理员(AFD)。问题是,这个管理员不对外公开,你得偷偷摸摸从侧门进去。

召唤恶魔的三步仪式

第一步:解锁禁忌咒语

Windows 有一堆以 Nt*Rtl* 开头的内部函数,微软官方警告说"别用这些,会出事的"。但没办法,要用 AFD 就得先解锁这些函数。

// 动态加载 ntdll.dll,获取内部函数(伪代码示意)
// 实际使用 windows-sys crate 会更安全
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());
}

这就像你在游戏里找到了隐藏的作弊码,虽然官方不推荐,但确实能用。

第二步:打开地狱之门

用刚才解锁的 NtCreateFile 函数,打开 \Device\Afd 这个特殊设备:

// 打开 AFD 设备(伪代码示意)
// 实际实现需要大量的结构体和参数设置
use std::ptr;

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

    // NtCreateFile 需要 UNICODE_STRING、OBJECT_ATTRIBUTES 等复杂结构
    // 这里简化展示核心思路
    let status = NtCreateFile(
        &mut afd_handle,
        GENERIC_READ | GENERIC_WRITE,
        // ... 还需要 10+ 个参数
    );
}

这一步相当于你在《暗黑破坏神》里打开了通往地狱的传送门。

第三步:签订契约

通过 I/O Control(IOCTL)告诉 AFD 你想干什么:

// 先获取 socket 的"真名"(base handle)
use std::os::windows::io::AsRawSocket;

unsafe {
    // 通过 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,
    );

    // 发起 AFD 轮询操作
    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,
        // ... 输出缓冲区参数
    );
}

这一步就是和恶魔签合同:你告诉它"帮我盯着这几个 socket,哪个有动静了就通知我"。

最终效果:把 IOCP 改造成 epoll

有了 AFD 之后,你可以:

  1. 把 AFD 句柄注册到 IOCP 完成端口
  2. 对每个 socket 发起 IOCTL_AFD_POLL 操作(这个操作本身是异步的)
  3. 当 socket 就绪时,IOCP 会收到通知
  4. 你就可以像 Linux 的 epoll 一样,用"就绪通知"的方式处理 I/O 了

这就像你把外卖员改造了一下:现在他到楼下会先给你打电话,而不是直接往门缝里塞。

实战:Rust 生态怎么用的

Tokio 的秘密武器:mio

Tokio 底层用的是 mio 这个库,mio 在 Windows 上就是靠 AFD 实现的。

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

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

    // 看起来很简单,但底层:
    // 1. mio 创建了 AFD 句柄
    // 2. 把这个 TcpStream 的 socket 注册到 AFD
    // 3. 当数据到达时,AFD 通过 IOCP 通知 mio
    // 4. mio 唤醒对应的 Future
    // 5. 你的代码继续执行

    // 发送 HTTP 请求(write_all 来自 AsyncWriteExt)
    stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await?;

    // 读取响应(read 来自 AsyncReadExt)
    let mut buf = vec![0u8; 1024];
    let n = stream.read(&mut buf).await?;
    println!("读到 {} 字节", n);

    // 打印前 100 个字节
    println!("{}", String::from_utf8_lossy(&buf[..n.min(100)]));

    Ok(())
}

async-std 和 smol:另一条路

async-stdsmol 用的是 polling 库,这个库之前依赖 wepoll(一个 C 语言的 AFD 封装),现在正在迁移到纯 Rust 实现。

Node.js 也在用

没错,Node.js 底层的 libuv 也用了 AFD。所以这个"小恶魔"不仅支撑着 Rust 生态,还支撑着整个 JavaScript 服务器端生态。

微软要是敢把 AFD 砍了,全世界的 Node.js 开发者都得跳起来。

常见坑与对策

坑 1:Wine 不支持

Wine(在 Linux 上运行 Windows 程序的兼容层)很长时间都不支持 AFD,导致用 Tokio 的程序在 Wine 上跑不起来。

对策:最新版本的 Wine 已经加上了 AFD 支持,但如果你要兼容老版本 Wine,可能需要回退到用线程池的方案。

坑 2:UWP 应用可能有问题

UWP(Windows 通用应用平台)对使用未公开 API 有限制,理论上可能会被拒绝上架。

对策:如果你要开发 UWP 应用,最好准备一个备用方案(比如用线程池模拟异步)。

坑 3:调试困难

因为 AFD 没有官方文档,出了问题很难排查。你只能靠逆向工程和社区经验。

对策

  • 多看 miopolling 的源码
  • 遇到奇怪问题先搜 GitHub issues
  • 实在不行就用 WinDbg 抓内核调用栈(需要一定功力)

总结:三个要点

  1. 为什么用 AFD:因为 Windows 没有像 Linux epoll 那样的"就绪通知"API,而官方的 IOCP 是"完成通知",和 Rust 的 Future 模型不兼容。

  2. 怎么用 AFD:通过 NtCreateFile 打开 \Device\Afd 设备,用 IOCTL_AFD_POLL 发起轮询操作,把结果接入 IOCP 完成端口。

  3. 风险在哪:AFD 是未公开 API,理论上微软可以随时改动。但实际上 Node.js、Rust 生态都在用,微软不太可能动它。

下一步可以做什么

  1. 看源码:去 GitHub 上翻 mio 的 Windows 后端实现,看看具体是怎么调用 AFD 的
  2. 写个 demo:尝试用 windows-sys crate 直接调用 AFD,感受一下底层的"肮脏"
  3. 对比其他平台:研究一下 Linux 的 epoll 和 BSD 的 kqueue,理解为什么它们更适合 Rust 的异步模型

记住:技术没有完美的,只有"能跑就行"的。AFD 虽然是个"小恶魔",但它确实让 Rust 异步在 Windows 上活了下来。有时候,和恶魔做交易也不是什么坏事——只要你知道自己在做什么。


参考资料

关键词提示:如果你在搜索引擎找相关资料,试试这些关键词:

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