Ch20.13: fast_io Is a Freestanding I/O Library

中文(简体)

1. The Claim

fast_io is an I/O library that works with no operating system underneath it. Not "the formatting part is header-only, so you can use it if you avoid the interesting bits" — the whole thing, including containers, formatting, scanning and transcoding, with the operating-system-dependent pieces isolated behind type parameters that the target supplies.

This page is the payoff of the chapter, so it is written as four design decisions, each one the direct answer to a failure the previous pages measured:

Failurefast_io's answer
One hidden global heap (20.7) The allocator is a type parameter, and sealed into the container type
Error handling needs an OS (20.9) Herbceptions on the return path, with the policy chosen by the allocator
Message tables cost flash (20.10) A build knob that reports [domain](code), and overridable domains
The library headers assume a console (20.6) The device is a type too — a scatter write, or nothing at all

2. The Layer Split Is Visible in the Headers

fast_io has three umbrella headers, and the split between them is the freestanding/hosted boundary made explicit: fast_io_freestanding.h for formatting and scanning into buffers, fast_io_hosted.h for devices, files and consoles, and fast_io.h for both.

The same split appears in the containers, and it is enforced by the preprocessor rather than by documentation. ::fast_io::containers::vector<T, Alloc> is always available; the convenience alias ::fast_io::vector<T>, which picks the platform's default heap, exists only when there is a hosted platform:

#if ((__STDC_HOSTED__ == 1 && (!defined(_GLIBCXX_HOSTED) || _GLIBCXX_HOSTED == 1) && \
	  !defined(_LIBCPP_FREESTANDING)) ||                                             \
	 defined(FAST_IO_ENABLE_HOSTED_FEATURES))

namespace fast_io
{

template <typename T, typename Alloc = ::fast_io::native_global_allocator>
using vector = ::fast_io::containers::vector<T, Alloc>;

So on a freestanding target, ::fast_io::vector does not exist, and the compiler says so in the clearest possible way — a name that is not found, with ::fast_io::containers::vector suggested next to it. There is no silent fallback to a hidden heap, because there is no name to fall back to. You write ::fast_io::containers::vector<int, my_arena_allocator>, and the allocator is part of the type.

3. The Heap Comes From the Target

The default allocator is selected by exactly the condition you would hope for. When the platform is hosted it is c_malloc_allocator (or the NT heap, or mimalloc, or whatever was configured); when it is freestanding it is freestanding_global_allocator:

using native_global_allocator = generic_allocator_adapter<
#if defined(FAST_IO_USE_CUSTOM_GLOBAL_ALLOCATOR)
	custom_global_allocator
#elif ...
	c_malloc_allocator
#else
	freestanding_global_allocator
#endif
	>;

And freestanding_global_allocator is only declared in the library. Its three functions have no definition anywhere in fast_io:

class freestanding_global_allocator
{
public:
static ::fast_io::allocation_least_result allocate_aligned_conditional_zero_at_least_die(::std::size_t alignment, ::std::size_t n, bool zero) noexcept;
static ::fast_io::allocation_least_result allocate_aligned_conditional_zero_at_least_try(::std::size_t alignment, ::std::size_t n, bool zero) FAST_IO_HERBCEPTIONS_THROWS;
static void deallocate_aligned_n(void* p, ::std::size_t alignment, ::std::size_t n) noexcept;
};

That is the whole freestanding allocator contract: allocate-with-alignment, optionally zeroed, returning {pointer, count}, or throw a herbception, or die. You implement it for your target — a bump arena, a slab, a per-CPU pool, whatever the target has — and every fast_io container and every formatting routine that needs scratch memory uses exactly that, and nothing else. A library that cannot link without you providing a heap is a library that cannot allocate behind your back.

4. What the Generated Code Requires: Nothing

Here is a program that formats into a stack buffer and uses a container with a caller-supplied arena. It is one translation unit, it includes only fast_io_core.h and the vector, and it never touches a file, a console or a locale:

#include <fast_io_core.h>
#include <fast_io_dsal/vector.h>

template <::std::size_t extent>
class arena {
public:
  alignas(::std::max_align_t) static inline ::std::byte storage[extent]{};
  static inline ::std::size_t used{};
  static ::fast_io::allocation_least_result
  allocate_aligned_conditional_zero_at_least_die(::std::size_t alignment,
                                                 ::std::size_t n, bool) noexcept {
    ::std::size_t const start{(used + alignment - 1u) / alignment * alignment};
    if (start + n > extent) { ::fast_io::fast_terminate(); }
    used = start + n;
    return {storage + start, n};
  }
  static void deallocate(void *) noexcept {}
  static void deallocate_aligned_n(void *, ::std::size_t, ::std::size_t) noexcept {}
};

using heap_a = ::fast_io::generic_allocator_adapter<arena<4096>>;

int render(int value) throws {
  char buffer[64];
  ::fast_io::containers::vector<int, heap_a> v;
  v.push_back(value);
  ::fast_io::basic_obuffer_view<char> out{buffer, buffer + sizeof(buffer)};
  ::fast_io::operations::print_freestanding<false>(out, "v[0] = ", v[0], "\n");
  return static_cast<int>(out.size());
}

int main() {
  try { return render(12345); }
  catch throws (::std::error) { return -1; }
}
$ g++ -std=c++26 -fherbceptions -O2 -I fast_io/include -c core2.cc -o core2.o
$ nm -u core2.o
  [0 undefined symbols]
$ size core2.o
   1444	      0	   4104	   5548	   15ac	core2.o

Zero. No malloc, no memcpy, no __cxa_*, no unwinder, no write, no locale, no __gxx_personality_v0. The 1444 bytes of code and 4 KiB of arena are the entire program: integer formatting, a growable container, and an error channel. The only thing that object file needs from the world is a stack.

And when the arena is exhausted, the failure policy is written in the arena: the _die flavour above calls fast_terminate() (19.2); the _try flavour reports ::std::errc::not_enough_memory on the throws channel instead (19.9). Choosing which one is a compile-time decision per container type, not a global switch, and that is what makes a fast_io container safe to hand to a subsystem that promised not to allocate.

5. Why This Is the Library to Use

A hosted program benefits from all of this without knowing it: no hidden allocation, no locale, no iostream initialization, no exceptions on the failure path, and one formatting engine that writes straight into the destination instead of building a temporary std::string per argument (17.8 shows what the + version costs). Then, the day the code has to run on a board, nothing changes: the same headers, the same print, the same containers, with the allocator and the device supplied by the target.

That is the practical form of the argument this chapter has been making. The standard library's I/O and containers were designed for a hosted program and will never be freestanding (20.6), because their interface is the hidden heap and the thrown exception. fast_io's interface is the allocator and the device, so it works on both sides of the boundary — and that is why this book uses it for everything, rather than treating it as a faster <iostream>.