第11.14章: Win32 文件 API

English

概述

在 Windows 上,用户态应用通过 kernel32.dll 与内核打交道。打开文件的两个入口是:

fast_io 用两个类型把这个差异藏起来,与 POSIX 层一一对应:

在 ANSI 与 Wide 之间选择:win32_api_family

Win32 层暴露一个名为 ::fast_io::win32_api_family 的特征类型。它选择在给定 Windows 目标上该用哪一套 API:

实践中你几乎不需要直接接触 win32_api_family:当你传入文件名时,win32_file 会自动查询它。

用 win32_file 打开文件


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;

    // The constructor picks CreateFileW or CreateFileA through
    // ::fast_io::win32_api_family.
    ::fast_io::win32_file wf(u8"hello_win32.txt", ::fast_io::open_mode::out);

    print(wf, "Hello from win32_file!\n");

    ::std::size_t const n{42zu};
    println(wf, "The answer is: ", n);
    // CloseHandle is invoked automatically when `wf` goes out of scope.
}
      

包裹已有的 HANDLE

与 posix_io_observer 一样,你可以对别处取得的 HANDLE 构建非拥有视图:


#include <fast_io.h>
#include <fast_io_device.h>

void write_via_handle(void* h)
{
    using namespace ::fast_io::iomnp;

    // Non-owning: does NOT call CloseHandle on destruction.
    ::fast_io::win32_io_observer observer{
        static_cast<::fast_io::win32_io_observer::native_handle_type>(h)};
    print(observer, "Written through a win32_io_observer\n\n");
}

int main()
{
    ::fast_io::win32_file wf(u8"legacy_handle.txt", ::fast_io::open_mode::out);
    write_via_handle(wf.native_handle());
}
      

读取与 open-at

读取使用 scan。相对于目录句柄打开时,底层使用 NtCreateFile,因为 Win32 API 本身没有 *at 的对应物:


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;

    ::fast_io::win32_file inpf(u8"input.txt", ::fast_io::open_mode::in);
    ::std::size_t value{};
    ::fast_io::io::scan(inpf, value);

    ::fast_io::win32_file outpf(u8"output.txt", ::fast_io::open_mode::out);
    println(outpf, "Read the value: ", value);
}
      

Windows 也有文件描述符

一个常见的误解是 Windows 只有 HANDLE 而没有文件描述符。这是不对的。Windows C 运行时(MSVCRT、UCRT)通过 _fdopen、_open、_read、_write、_close 等函数提供类 POSIX 的文件描述符,它们声明在 <io.h> 与 <fcntl.h> 中。

C 运行时维护一张把文件描述符(小整数)映射到 Win32 HANDLE 的表,你可以在两者之间转换:


#include <fast_io.h>
#include <fast_io_device.h>
#include <io.h>
#include <windows.h>

int main()
{
    using namespace ::fast_io::iomnp;

    // Windows C runtime file descriptor (not a HANDLE!)
    int fd{::_open("data.txt", _O_RDONLY)};
    if (fd == -1) {
        // handle error
    }

    // Convert file descriptor to Win32 HANDLE
    void* h{::_get_osfhandle(fd)};

    // Now you can use Win32 APIs on it
    ::fast_io::win32_io_observer observer{
        static_cast<::fast_io::win32_io_observer::native_handle_type>(h)};
    println(observer, "Using Win32 API on a C runtime fd");

    // Close the file descriptor (this also closes the underlying HANDLE)
    ::_close(fd);
}

这证明Windows 既有文件描述符也有 HANDLE。C 运行时在 Win32 API 之上提供了一层类 POSIX 的包装。当你调用 _read(fd, ...) 时,C 运行时内部调用的是 ReadFile(_get_osfhandle(fd), ...)。

原始的 Win32 I/O API

在使用 fast_io 的包装之前,先了解底层 Win32 API 会有帮助。这些是由 kernel32.dll 导出的函数:

API 函数 用途
CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile) 按名字打开或创建文件(宽字符 / UTF-16),返回 HANDLE。
CreateFileA(...) CreateFileW 的 ANSI 变体,用于 Windows 9x(95、98、ME)。
ReadFile(hFile, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped) 从文件句柄读取字节。返回 BOOL;实际读到的字节数写入 lpNumberOfBytesRead。
WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped) 向文件句柄写入字节。返回 BOOL;实际写入的字节数写入 lpNumberOfBytesWritten。
CloseHandle(hObject) 关闭句柄。永远需要 — 句柄是有限资源。
SetFilePointer(hFile, lDistanceToMove, lpDistanceToMoveHigh, dwMoveMethod) 改变文件指针。dwMoveMethod 是 FILE_BEGIN、FILE_CURRENT 或 FILE_END。对大文件请用 SetFilePointerEx。
GetFileSize(hFile, lpFileSizeHigh) 取得文件大小。对大文件请用 GetFileSizeEx。

这些 Win32 API 不带缓冲 — 每次调用都经 kernel32.dll 进入内核。这使得它们在操作很小或很频繁时很慢。fast_io 的带缓冲类型(如 ibuf_file 与 obuf_file)用用户态缓冲包装这些调用,性能好得多。

即使不用 fast_io 的 I/O,也可以用 win32_file 做 RAII

即使你更愿意直接调用原始 Win32 API(例如 ReadFile 与 WriteFile)而不使用 fast_io 的 print 与 scan,你仍然应该用 ::fast_io::win32_file 做 RAII。它的析构函数保证调用 CloseHandle,所以你永远不会忘记关闭句柄。

当涉及异常或提前返回时,这一点尤其重要 — 没有 RAII,你必须在每一条退出路径上手动调用 CloseHandle,这很容易出错。


#include <fast_io.h>
#include <fast_io_device.h>
#include <windows.h>

int main()
{
    // Open with win32_file for RAII, but use raw Win32 ReadFile/WriteFile
    ::fast_io::win32_file wf(u8"data.bin", ::fast_io::open_mode::out);

    // Get the raw HANDLE
    void* h{wf.native_handle()};

    // Use raw Win32 WriteFile (unbuffered, goes through kernel32.dll)
    char const buffer[]{"Hello from raw WriteFile\n"};
    DWORD written{};
    BOOL ok{::WriteFile(h, buffer, static_cast<DWORD>(sizeof(buffer) - 1), &written, nullptr)};

    if (!ok || written != sizeof(buffer) - 1) {
        // handle error
    }

    // wf's destructor calls CloseHandle(h) automatically when it goes out of scope.
    // No need to manually call CloseHandle() here, even if we return early or throw.
}

同样的原则也适用于读取:


#include <fast_io.h>
#include <fast_io_device.h>
#include <windows.h>

int main()
{
    ::fast_io::win32_file wf(u8"data.bin", ::fast_io::open_mode::in);
    void* h{wf.native_handle()};

    char buffer[256];
    DWORD nread{};
    BOOL ok{::ReadFile(h, buffer, sizeof(buffer), &nread, nullptr)};

    if (ok && nread > 0) {
        // Process the buffer
    }

    // CloseHandle(h) is called automatically
}

要定位,就在原生句柄上使用 SetFilePointerEx:


#include <fast_io.h>
#include <fast_io_device.h>
#include <windows.h>

int main()
{
    ::fast_io::win32_file wf(u8"data.bin",
        ::fast_io::open_mode::in | ::fast_io::open_mode::out);
    void* h{wf.native_handle()};

    LARGE_INTEGER distanceToMove;
    distanceToMove.QuadPart = 100;

    // Seek to offset 100 from the beginning of the file
    BOOL ok{::SetFilePointerEx(h, distanceToMove, nullptr, FILE_BEGIN)};
    if (!ok) {
        // handle error
    }

    // Now read or write from the new position
}

小结