Ch11.17: Relationship Between File Layers
Why This Chapter Matters
So far we have met six families of file types: wine, nt,
win32, posix, c, and filebuf.
They look like separate, parallel APIs — but they are not. They form a
stack, and every layer is implemented in terms of the one below it.
Understanding this stack is the key to using fast_io well. It explains
why iobuf_file “just works” on every platform, why you can
freely mix layers, and what native_file really means.
The Layer Hierarchy
From the lowest layer (closest to the kernel) to the highest layer (closest to the user):
- Layer 1:
wine(lowest, Wine translation layer) - Layer 2:
nt/zw(NT kernel interface, Nt* and Zw* functions are identical in user space) - Layer 3:
win32(Win32 API) - Layer 4:
posix(POSIX file descriptors) - Layer 5:
c(C stdlib FILE*) - Layer 6:
filebuf(C++ stdlib, highest)
Written as a chain, with the arrow meaning “is built on top of”:
wine (1) => nt/zw (2) => win32 (3) => posix (4) => c (5) => filebuf (6)
Each layer wraps the one below it. When that is not possible (because the layer below does not expose a usable API), the layer calls the underlying system call directly and wraps that.
Layer Diagram
A concrete picture of the stack:
+------------------------------------------------+
| std::filebuf (C++ stdlib) | <- filebuf_file (Layer 6)
+------------------------------------------------+
|
v
+------------------------------------------------+
| FILE* (C stdlib) | <- c_file (Layer 5)
+------------------------------------------------+
|
v
+------------------------------------------------+
| POSIX file descriptors (int fd) | <- posix_file (Layer 4)
+------------------------------------------------+
|
v
+------------------------------------------------+
| Win32 HANDLE (CreateFileW / CreateFileA) | <- win32_file (Layer 3)
+------------------------------------------------+
|
v
+------------------------------------------------+
| NT HANDLE (NtCreateFile / ZwCreateFile) | <- nt_file (Layer 2)
+------------------------------------------------+
|
v
+------------------------------------------------+
| Wine host fd (Wine translating NT to host) | <- wine_file (Layer 1)
+------------------------------------------------+
|
v
[ kernel: ntoskrnl.exe ]
Each layer corresponds to a real system component:
-
filebuf — the C++ standard library (
libstdc++,libc++, or MSVC’s STL). On Windows these ship aslibstdc++-6.dll(MinGW),libc++.dll/c++.dll(LLVM libc++ —libc++.dllfor${CPU}-windows-gnu,c++.dllfor${CPU}-windows-msvc), ormsvcp{version}.dll(MSVC, e.g.msvcp140.dll). -
c — the C standard library (libc:
glibc,musl,msvcrt.dll/ucrt).FILE*,fopen,fread,fprintf, andprintflive here. -
posix — POSIX syscalls (
open,read,write,close). On Linux/macOS/FreeBSD, libc wraps these syscalls. On Windows, there is no native POSIX layer. -
win32 — kernel32.dll on Windows.
CreateFileW,ReadFile,WriteFile, and other Win32 file APIs live here. This is the “traditional” Windows API. -
nt — ntdll.dll on Windows.
NtCreateFile,NtReadFile,NtWriteFile, and other native NT APIs live here. This is the real Windows kernel interface — kernel32.dll itself is built on top of ntdll.dll. -
zw — Also in ntdll.dll.
ZwCreateFile,ZwReadFile,ZwWriteFile, etc. In user space,Nt*andZw*functions are identical — they both call into the kernel. The difference is only in the kernel:Nt*functions validate parameters (for calls from user mode), whileZw*functions skip parameter validation (for calls from trusted kernel mode code).fast_ioprovides::fast_io::zw_fileas an alias for::fast_io::nt_filesince they behave the same in user space. - wine — Wine’s translation layer. When running Windows programs on Linux/macOS, Wine translates NT syscalls into host POSIX syscalls.
So when you call std::fstream, the call chain on Windows is:
C++ stdlib → MSVCRT/ucrt (FILE*) → kernel32.dll
(CreateFileW) → ntdll.dll (NtCreateFile) → kernel.
On Linux, it is: C++ stdlib → glibc/musl (FILE*) →
open()/read() syscalls → kernel.
fast_io lets you skip layers. If you want zero overhead, use
native_file to call the kernel directly. If you need compatibility with
legacy code, use c_file or filebuf_file. You choose the
layer that fits your needs.
On Linux, FreeBSD, and macOS, only the posix branch is used. On
Windows, the nt and win32 branches are used. The
wine branch is only used when running under Wine on a POSIX host.
Platform availability:
wine_fileis only available when compiling for Wine (running on a POSIX host).nt_fileandwin32_fileare only available on Windows (and Wine).posix_fileis available on all POSIX-compliant systems (Linux, macOS, FreeBSD, etc.) and also on Windows when using a POSIX compatibility layer.c_fileandfilebuf_fileare available everywhere, since they rely on the C and C++ standard libraries.
Important: These relationships are not defined by
fast_io. They are defined by the operating system and the compiler’s
C/C++ runtime vendors. For example, on POSIX systems, the C stdlib’s
FILE* is implemented on top of POSIX file descriptors by the libc
implementation (glibc, musl, etc.), and the C++ stdlib’s
::std::filebuf is implemented on top of FILE* by the C++
runtime library. fast_io merely summarizes and wraps these existing
relationships — it does not invent new ones.
The Golden Rule of Wrapping
fast_io provides a wrapper for every layer, and each
wrapper does one of two things:
-
Wraps the API of the layer below. For example,
c_fileis built on top ofposix_filebecause the C stdlib on POSIX is itself built on top of POSIX file descriptors. -
Calls the lower syscall directly when the runtime below does not
expose a usable API. This is often the case going from
c_filedown toposix_file, because libc does not publicly expose its internal fd — sofast_iogoes straight to the syscall and wraps the result.
Converting Between Layers
When you need to move between layers, the direction determines the method:
-
Lower to higher (e.g.,
posix_file→c_file→filebuf_file): use::std::move. Ownership of the handle is transferred upward. -
Higher to lower (e.g.,
filebuf_file→c_file→posix_file): usestatic_castto the lower layer’sio_observer. This gives you a non-owning view of the underlying handle.
Cross-layer conversions are also allowed. You don’t need to convert
one layer at a time. For example, you can move directly from nt_file (the
lowest Windows layer) to filebuf_file (the highest layer), skipping the
intermediate win32_file, posix_file, and c_file
layers:
// Cross-layer conversion: skip intermediate layers
::fast_io::nt_file nf(u8"example.txt", ::fast_io::open_mode::out);
// Move directly from nt_file (layer 1) to filebuf_file (layer 6)
::fast_io::filebuf_file fbf(::std::move(nf), ::fast_io::open_mode::out);
// This works because filebuf_file's constructor accepts any lower-layer file type
Similarly, you can static_cast from any higher layer directly to any lower
layer’s observer:
// Direct cast from filebuf_file to nt_io_observer (skipping intermediate layers)
::fast_io::filebuf_file fbf(/* ... */);
auto nt_iob{static_cast<::fast_io::nt_io_observer>(fbf)};
HANDLE handle{nt_iob.native_handle()}; // access the raw NT handle
This flexibility lets you jump directly to the layer you need without tedious step-by-step conversions.
nt_file and win32_file
Although nt_file (layer 2) and win32_file (layer 3) are
conceptually different layers from an API perspective, they both use the same
HANDLE type at the OS level. If you need to convert between their observers,
just use static_cast:
// Convert between nt_io_observer and win32_io_observer
::fast_io::nt_io_observer nt_iob(/* ... */);
auto win32_iob{static_cast<::fast_io::win32_io_observer>(nt_iob)};
// The reverse also works
auto nt_iob2{static_cast<::fast_io::nt_io_observer>(win32_iob)};
This is different from true layer transitions (like posix_file →
c_file), where the handle type actually changes and must be wrapped in
a new abstraction.
// Standard layer-by-layer conversion
::fast_io::posix_file pf(u8"example.txt", ::fast_io::open_mode::out);
::fast_io::c_file cf(::std::move(pf), ::fast_io::open_mode::out);
// Higher to lower: static_cast to observer for a non-owning view
auto piob{static_cast<::fast_io::posix_io_observer>(cf)};
int fd{piob.fd}; // access the raw handle
What Does native_xxx Mean?
::fast_io::native_file is an alias for the lowest unbuffered layer
that fast_io wants to use on the current platform. It is chosen at
compile time:
-
Linux, FreeBSD, macOS, … →
::fast_io::native_file=::fast_io::posix_file -
Windows NT family (NT, 2000, XP, 7, 10, 11) →
::fast_io::native_file=::fast_io::nt_file -
Windows 9x family (95, 98, ME) →
::fast_io::native_file=::fast_io::win32_file
The same aliasing rule applies to the observers: native_io_observer is
posix_io_observer on POSIX systems, nt_io_observer on
Windows NT.
The buffered type basic_iobuf_file is then defined in terms of
basic_native_file:
template<::std::integral char_type>
using basic_iobuf_file = ::fast_io::basic_iobuf<
::fast_io::basic_native_file<char_type>>;
So when you write ::fast_io::iobuf_file f(u8"x.txt", open_mode::out);,
you are getting buffered I/O on top of whichever layer fast_io considers
native for your platform.
Mixing Layers
Because every layer exposes the same print / scan interface
through concepts, you can mix them freely:
#include <fast_io.h>
#include <fast_io_device.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
// Start at the bottom: a POSIX file descriptor.
::fast_io::posix_file pf(u8"mixed.txt", ::fast_io::open_mode::out);
println(pf, u8"Line 1 via posix_file");
// Move it up to a C FILE*.
::fast_io::c_file cf(::std::move(pf), ::fast_io::open_mode::out);
println(cf, u8"Line 2 via c_file");
// Move it up again to a C++ std::filebuf.
::fast_io::filebuf_file fbf(::std::move(cf), ::fast_io::open_mode::out);
println(fbf, u8"Line 3 via filebuf_file");
// All three lines end up in the same file, in order.
}
Each ::std::move promotes the handle one layer upward. Ownership is
transferred, not duplicated.
Building an std::fstream from a Syscall-Level Handle
Because every layer can be constructed from the one below it via ::std::move,
you can start at the syscall level and ascend through every layer until you reach a
fully working ::std::fstream:
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
using namespace ::fast_io::iomnp;
// Start at the syscall level (posix on Linux/macOS, nt/win32 on Windows).
::fast_io::posix_file pf(u8"syscall.txt", ::fast_io::open_mode::out);
// Move up: posix -> c
::fast_io::c_file cf(::std::move(pf), ::fast_io::open_mode::out);
// Move up: c -> filebuf
::fast_io::filebuf_file fbf(::std::move(cf), ::fast_io::open_mode::out);
// Transfer the filebuf into an std::ofstream
::std::ofstream fout;
*fout.rdbuf() = ::std::move(*fbf.fb);
// Now you can use the fstream like normal C++.
fout << "Hello from a syscall-originated fstream!\n";
}
Each ::std::move promotes the handle one layer upward. Ownership is
transferred, not duplicated. This is not a parlor trick — it is the natural
consequence of every layer being interoperable with the one next to it.
Getting the Raw File Descriptor Out of an iobuf_file
Because the layers are transparent, you can start at the top and descend all the
way back down to a raw int fd. The idiomatic way is through
static_cast to the appropriate observer type:
#include <fast_io.h>
#include <fast_io_device.h>
int main()
{
using namespace ::fast_io::iomnp;
// iobuf_file is buffered I/O on top of native_file.
::fast_io::iobuf_file bf(u8"demo.txt", ::fast_io::open_mode::out);
println(bf, u8"Buffered write");
// Flush so everything hits the underlying file before we peek at it.
::fast_io::flush(bf);
// Step 1: obtain a posix_io_observer from the iobuf_file.
// On POSIX systems, iobuf_file's native file is posix_file,
// so this static_cast is lossless.
auto piob{static_cast<::fast_io::posix_io_observer>(bf)};
// Step 2: pull the raw int fd out of the observer.
int const fd{piob.fd};
print("The underlying fd is: ", fd);
// The fd is still owned by `bf`; we only borrowed a view.
}
On Windows the same idea works, but you descend to
::fast_io::nt_io_observer (or ::fast_io::win32_io_observer)
and the raw handle is a void* rather than an int.
The Complete Layer Reference
| Layer | Owning type | Observer type | Wraps |
|---|---|---|---|
| wine | ::fast_io::wine_file |
::fast_io::wine_io_observer |
Wine host fd |
| nt | ::fast_io::nt_file |
::fast_io::nt_io_observer |
NtCreateFile / ZwCreateFile |
| win32 | ::fast_io::win32_file |
::fast_io::win32_io_observer |
CreateFileW / CreateFileA |
| posix | ::fast_io::posix_file |
::fast_io::posix_io_observer |
open / openat |
| c | ::fast_io::c_file |
::fast_io::c_io_observer |
FILE* / fopen |
| filebuf | ::fast_io::filebuf_file |
::fast_io::filebuf_io_observer |
::std::filebuf* |
Key Takeaways
FILE* behaves the way it does, why
mmap is faster than fread, and how different APIs relate to
each other under the hood.
-
The six layers form a strict hierarchy:
wine => nt => win32 => posix => c => filebuf. -
Each layer wraps the one below it. When the layer below does not expose a usable
API,
fast_iocalls the kernel directly and wraps that instead. -
fast_ioprovides a wrapper for every layer and lets you mix layers together freely. -
native_xxxis the layerfast_iowants at the lowest level on the current platform:posix_fileon POSIX systems,nt_fileon Windows NT,win32_fileon Windows 9x. -
You can build an
::std::fstreamfrom a syscall-level handle by moving through each layer:posix_file→c_file→filebuf_file→::std::ofstream::rdbuf(). -
You can descend in the other direction too: a
static_castto the appropriatexxx_io_observergives you a non-owning view, and the observer exposes the raw native handle.