开场白:衣柜里藏着什么
哥们儿,今天咱们聊点刺激的——你知道你写的 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 之后,你可以:
- 把 AFD 句柄注册到 IOCP 完成端口
- 对每个 socket 发起
IOCTL_AFD_POLL操作(这个操作本身是异步的) - 当 socket 就绪时,IOCP 会收到通知
- 你就可以像 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-std 和 smol 用的是 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 没有官方文档,出了问题很难排查。你只能靠逆向工程和社区经验。
对策:
- 多看
mio和polling的源码 - 遇到奇怪问题先搜 GitHub issues
- 实在不行就用 WinDbg 抓内核调用栈(需要一定功力)
总结:三个要点
为什么用 AFD:因为 Windows 没有像 Linux
epoll那样的"就绪通知"API,而官方的 IOCP 是"完成通知",和 Rust 的Future模型不兼容。怎么用 AFD:通过
NtCreateFile打开\Device\Afd设备,用IOCTL_AFD_POLL发起轮询操作,把结果接入 IOCP 完成端口。风险在哪:AFD 是未公开 API,理论上微软可以随时改动。但实际上 Node.js、Rust 生态都在用,微软不太可能动它。
下一步可以做什么
- 看源码:去 GitHub 上翻
mio的 Windows 后端实现,看看具体是怎么调用 AFD 的 - 写个 demo:尝试用
windows-syscrate 直接调用 AFD,感受一下底层的"肮脏" - 对比其他平台:研究一下 Linux 的
epoll和 BSD 的kqueue,理解为什么它们更适合 Rust 的异步模型
记住:技术没有完美的,只有"能跑就行"的。AFD 虽然是个"小恶魔",但它确实让 Rust 异步在 Windows 上活了下来。有时候,和恶魔做交易也不是什么坏事——只要你知道自己在做什么。
参考资料:
关键词提示:如果你在搜索引擎找相关资料,试试这些关键词:
- “Rust Windows async internals”
- “IOCP vs epoll”
- “Device Afd polling”
- “Tokio Windows backend”
- “mio Windows implementation”