C++ Day · Pavia · 17 October 2026
Sane C++ Libraries
Sane C++ Libraries
Old School C++for the agentic age
Cross-platform APIs and agent-assisted development.
Sane C++ Libraries 02 / 34
About this project Sane C++ Libraries is mypersonal project.
It is not an NVIDIA product.
Stefano CristianoPrincipal Research Scientist at NVIDIA
Sane C++ Libraries / Introduction 03 / 34
What is Sane C++ Libraries? A cross-platformsystems-programming base layer.
macOS Windows Linux
Independent libraries for common runtime facilities. Designed to compose, from one library to a whole application.
Files · processes · networking · async I/O With its own self-hosted C++ build system.
Sane C++ Libraries / Context 04 / 34
Project history 2022 Private project Repository begins.
December 2023 Open source Fast builds, readable code, easy integration.
Transition AI assisted Increasing delegation of implementation.
January 2026 → Agent driven “I no longer manually write the code.”
Sane C++ Libraries 05 / 34
Libraries and dependencies Common runtime facilities with explicit dependencies.
Show dependency graph → 28 libraries · Draft / MVP / Usable
System facilities
Optional containers / reflection / serialization · keep your STL if you prefer
D DraftM MVPU Usable No dependencies One level Two levelsArrows: library → dependency · transitive edges omitted
Sane C++ Libraries 06 / 34
Easy to integrate: single file libraries The same Process API with three integration choices.
Single file SC.cpp SC-build
Single file · main.cpp
#define SANE_CPP_IMPLEMENTATION
#include "SaneCppProcess.h"
int main()
{
SC::Process git;
if (!git.exec({"git", "--version"})) return 1;
return git.getExitStatus();
}macOS · command line
clang++ main.cpp -o demoSelf-contained Process header beside main.cpp. Tested with this compiler’s default and with C++14.
SC.cpp · main.cpp
#include "SaneCppProcess.h"
int main()
{
SC::Process git;
if (!git.exec({"git", "--version"})) return 1;
return git.getExitStatus();
}Windows · Developer Command Prompt
cl /permissive- /I SC\Includes main.cpp SC\SC.cpp /Fe:demo.exeSC is the repository checkout. MSVC links the system libraries through the source’s pragma directives.
SC-build.cpp · build definition
// sc-build-version: main
#include "SaneCppBuild.h"
SC::Result SC::Build::configure(
Definition& d, const Parameters& p)
{
Project project = {"Demo"};
SC_TRY(project.addFiles(".", "main.cpp"));
SC_TRY(addSaneCppLibraries(project, p));
return d.addProject(move(project));
}main.cpp · application
#include "SaneCppProcess.h"
int main()
{
SC::Process git;
if (!git.exec({"git", "--version"}))
return 1;
return git.getExitStatus();
}macOS · command line
./SC-build.sh compile Demo DebugCopy SC-build.sh into the project. The version marker selects Sane; first run downloads/caches it.
Sane C++ Libraries 07 / 34
Result and SC_TRY Success / failure Portable identity Platform diagnostics
Caller · output arguments carry the data
Result readConfig(FileDescriptor& file) {
char storage[4096];
Span<char> bytes;
SC_TRY(file.read(storage, bytes));
// Use bytes only after success.
return Result(true);
}SC_TRY · branch hints omitted
#define SC_TRY(expression) \
do { \
auto _exprResConv = \
SC::Result::Explicit(expression); \
if (!_exprResConv) \
return _exprResConv; \
} while (false)Evaluate once. On failure, return immediately. On success, continue.
call → Result → continue / return error
FileError.h · result branch · enum excerpt
enum class FileError : uint32_t {
InvalidHandle = 1,
UnsupportedPathEncoding,
InvalidPath,
// More errors omitted.
};
constexpr ResultCategory
FileResultCategory{10};Return and inspect an error without parsing text
Result lowerLevel() {
return Result::Error(
FileResultCategory,
FileError::InvalidPath);
}
Result higherLevel() {
SC_TRY(lowerLevel());
return Result(true);
}Caller · structured identity survives SC_TRY
const Result result = higherLevel();
if (result.isError(FileResultCategory, FileError::InvalidPath)) {
// Ask for a different path, or report this specific failure.
}macOS / Linux EADDRINUSE
Windows WSAEADDRINUSE
→ SocketError::AddressInUse Socket result · native diagnostics retained in the typed result
ResultSocket result = server.bind(address);
if (result.contextKind == SocketErrorContextKind::NativeError) {
console.print("OS error: {}\n", result.context.nativeError);
}
if (result.isError(SocketError::AddressInUse)) {
// Offer a different port, independent of the operating system.
}
Result portable = result; // Category + error code only.Keep ResultSocket when you need the native code and operation detail.
Sane C++ Libraries / API / Process 08 / 34
Process: pipes and redirected I/O Process · namespace and Result-returning wrapper omitted
Process first, second;
ProcessChain chain;
SmallString<128> captured;
SC_TRY(chain.pipe(first, {"cat"}));
SC_TRY(chain.pipe(second, {"grep", "Sane"}));
SC_TRY(chain.exec(captured, "Hello\nSane C++\n"));
// stdout, stdinSynchronous I/O · equivalent Bash pipeline printf 'Hello\nSane C++\n' | cat | grep Sane
provided stdin → cat → grep Sane → captured stdout
Sane C++ Libraries / API / Socket 09 / 34
Socket: caller-provided outputs Socket · synchronous TCP accept · port chosen by caller
SocketDescriptor listener, client;
SocketIPAddress address;
SC_TRY(address.fromAddressPort("127.0.0.1", port));
SC_TRY(listener.create(address.getAddressFamily()));
SocketServer server(listener);
SC_TRY(server.bind(address));
SC_TRY(server.listen(8));
SC_TRY(server.accept(address.getAddressFamily(), client));client ↑ caller-owned output accept fills that descriptor. Result reports success or failure.
Sane C++ Libraries / API / Files 10 / 34
Files: storage, traversal and changes Read Iterate Watch
File · one read
FileDescriptor file;
SC_TRY(file.open(absolutePath, FileOpen::Read));
char storage[4096];
Span<char> bytes;
SC_TRY(file.read(storage, bytes));char storage[4096] → Span<char> { address, extent }
A C array converts to a typed Span. For a vector: {storage.data(), storage.size()}.
FileSystemIterator · inspect is application code
FileSystemIterator::FolderState levels[16];
FileSystemIterator walk;
walk.options.recursive = true;
SC_TRY(walk.init(rootPath, levels));
while (walk.enumerateNext())
{
inspect(walk.get().path);
}
SC_TRY(walk.checkErrors());FileSystemWatcher · console supplied by application
FileSystemWatcher::ThreadRunner runner;
FileSystemWatcher watcher;
FileSystemWatcher::FolderWatcher folder;
SC_TRY(watcher.init(runner));
folder.notifyCallback = [&](const auto& notification) {
StringPath path;
if (notification.getFullPath(path))
console.print("Changed: {}\n", path.view());
};
SC_TRY(watcher.watch(folder, directoryPath));Print the affected path. ThreadRunner here; event-loop delivery is also available.
Sane C++ Libraries 11 / 34
Caller-owned storage in Async Requests, buffers and callback state must remain stable until completion.
Read request · setup and error cleanup omitted
AsyncFileRead read;
char buffer[4096];
read.buffer = buffer; // Span<char>: address + extent
read.callback = [&](auto& result) // AsyncFileRead::Result
{
Span<char> bytes;
if (result.get(bytes))
received = bytes.sizeInBytes() == 4;
};
SC_TRY(read.start(loop));
SC_TRY(loop.run());read is a caller-owned request.
buffer converts to Span<char>. Its size stays part of the contract.
bytes views the completed portion.
loop.run() dispatches the callback while this scope stays alive.
Sane C++ Libraries / API / Control flow 12 / 34
Await and Fibers Sane C++ supports the major async paradigms: callbacks, fibers and coroutines.
Await · coroutines Fibers · stackful
Await · inside AwaitTask · connected socket, loop and caller buffer supplied
AwaitSocketReceiveResult received;
SC_CO_TRY(co_await await.receive(socket, buffer, received));
SC_CO_TRY(co_await await.sendAll(socket, received.data));
co_return Result(true);Receive one chunk, then echo it. received.data is a Span over the supplied buffer.
AsyncFibers · inside a fiber task · io is AsyncFiberIO
AsyncFiberSocketReceiveResult received;
SC_TRY(io.receive(socket, buffer, received));
SC_TRY(io.sendAll(socket, received.data));
return Result(true);The same I/O, expressed as sequential code on a caller-supplied fiber stack.
AsyncExecution · one-shot vibe-coding prototype Built with Codex during this conference, inspired by Alberto Barbati’s earlier talk.
Sane C++ Libraries 13 / 34
AsyncStreams: file → compression → socket Endpoints initialized with the same pool and caller-owned queues
AsyncReadableFileStream<AsyncEventLoop> source;
AsyncZLibTransformStreamT<AsyncEventLoop> compressor;
AsyncWritableSocketStream<AsyncEventLoop> destination;
// File, socket, compression and queue setup omitted.
AsyncPipeline pipeline;
pipeline.source = &source;
pipeline.transforms[0] = &compressor;
pipeline.sinks[0] = &destination;
SC_TRY(pipeline.pipe());
SC_TRY(pipeline.start());readable fileinput bytes
zlib transformcompressed bytes
writable socketpartial writes
bytes bytes bytes completed buffers return to the shared pool ↶ Slow the consumer Conceptual flow · fixed I/O pool; compression workspace is separate.
Sane C++ Libraries / API / HTTP 14 / 34
HTTP: WebSockets and file uploads
SC-build bootstrap → async web server → two browsers sharing a WebSocket canvas
Sane C++ Libraries / API / Plugin 15 / 34
Plugin: loading a typed interface Plugin · initialized registry and error wrapper omitted
SC_TRY(registry.loadPlugin(id, compiler, sysroot, hostPath));
PluginDynamicLibrary* module = registry.findPlugin(id);
ITransform* api = nullptr;
SC_TRY_MSG(module && module->queryInterface(api), "interface");
const int value = api->transform(2);
SC_TRY(registry.unloadPlugin(id));Application-defined interface
struct ITransform {
static constexpr auto InterfaceHash =
PluginHash("ITransform");
Function<int(int)> transform;
};A typed interface across a dynamically loaded module boundary.
Sane C++ Libraries / Design 16 / 34
Design principles: then and now Original motivation With coding agents
Fast compile times → Frequent verification loops
Bloat free → Less code to inspect and load
Simple and readable code → Fewer conventions to reconstruct
Easy to integrate → Use and inspect the source in the app
Implementation choices: no required STL, exceptions or RTTI; no forced build system or third-party build dependencies.
Personal experience and expectations · reevaluate as models change.
Sane C++ Libraries / Memory 17 / 34
No hidden allocations
The entire library set is structured around caller-provided storage.
File · caller-provided bytes
char storage[4096];
Span<char> bytes;
SC_TRY(file.read(storage, bytes));
FileSystemIterator · caller-provided traversal state
FileSystemIterator::FolderState levels[16];
FileSystemIterator walk;
SC_TRY(walk.init(rootPath, levels));
Async · caller-provided request and buffer
char storage[4096];
AsyncFileRead read;
read.buffer = storage; // Span<char>
Where allocation is available
Only these areas allow allocation without a fixed budget. Each also has a bounded path.
Memory + Containers Allocate by default.
FixedAllocator / Array<T, N>
Await Explicit coroutine-frame allocator.
allocator.createFixed(storage)
Fibers Explicit scheduler / stack allocation.
allocator.createFixed(storage)
Sane C++ Libraries / Lifetimes 18 / 34
Grouping the lifetime of a compile job SC-build-inspired · one job = one owner
struct Slot
{
// One compile job owns this whole group.
AsyncProcessExit exited;
AsyncFileRead stdoutRead, stderrRead;
PipeDescriptor stdoutPipe, stderrPipe;
SmallVector<char, 4096> stdoutBuffer, stderrBuffer;
bool processFinished = false;
bool stdoutFinished = false, stderrFinished = false;
// Reuse the group only after its LAST completion.
bool reusable() const
{ return processFinished
&& stdoutFinished && stderrFinished; }
};FIRST: start monitoring a compile job.
Keep its monitor, reads, pipes and buffers together.
LAST: process exit + both streams finished.
Only then can the whole slot serve another job.
Completion of one request does not end the group’s lifetime.
Resource design · SC-build inspired 19 / 34
Lifetimes in async systems
A finished process can still have unread output. Keep the whole slot alive until all three completions.
process stdout stderr
launch exit callback
EOF
last bytes EOF
Slot process monitor stdout reader + buffer stderr reader + buffer Keep the whole group
Slot reusable when process exit ∧ stdout EOF ∧ stderr EOF
process exit stdout EOF stderr EOF ACTIVE SLOT
Compiler launched. The slot owns the process and both read requests.
Sane C++ Libraries / Architecture 21 / 34
Independence and deliberate duplication File File.cpp · setFileFlags · simplified
// Private descriptor-flag helper (excerpt).
int oldFlags = ::fcntl(fd, F_GETFL);
// Check errors; retry EINTR (omitted).
const int newFlags = enabled
? oldFlags | O_NONBLOCK
: oldFlags & ~O_NONBLOCK;
// If changed: fcntl(fd, F_SETFL, newFlags).
// Check errors; retry EINTR (omitted).Socket SocketDescriptorPosix.inl · same operation
// Private descriptor-flag helper (excerpt).
int oldFlags = ::fcntl(fd, F_GETFL);
// Check errors; retry EINTR (omitted).
const int newFlags = enabled
? oldFlags | O_NONBLOCK
: oldFlags & ~O_NONBLOCK;
// If changed: fcntl(fd, F_SETFL, newFlags).
// Check errors; retry EINTR (omitted).small private helper ≠ new public dependency
Both own their descriptor policy. Socket need not depend on File for a few flag operations.
Common .inl fragments are another mechanism for per-library private implementations.
Sane C++ Libraries / Evidence 22 / 34
Compilation cost: one file and many files What we compiled One consumer 100 consumers
Sane · excerpt from measured consumer
#include "SaneCppAsyncStandalone.h"
// Loop and pipe setup omitted.
read.buffer = buffer;
read.callback = onRead;
if (not read.start(loop)) return 5;
if (not loop.run()) return 8;Asio · includes from measured consumer
#include <asio/io_context.hpp>
#include <asio/readable_pipe.hpp>
#include <asio/writable_pipe.hpp>
#include <asio/connect_pipe.hpp>
#include <asio/buffer.hpp>
// async_read_some(...); loop.run();libuv · measured consumer excerpt
#include <uv.h>
// Loop, pipe and callbacks omitted.
uv_read_start(
reinterpret_cast<uv_stream_t*>(
&readPipe), allocate, onRead);
uv_run(&loop, UV_RUN_DEFAULT);ON THE FIRST BUILD Compile implementations from source Sane: SANE_CPP_IMPLEMENTATION Asio: ASIO_SEPARATE_COMPILATION libuv: full static library built from source
ON EACH EDIT consumer.cpp → consumer.o → link Reuse implementation objects / libuv archive. Header-visible templates / inline code remain.
One async pipe example, one changed translation unit. All three read four bytes.
One consumer · compile + link after an edit
Bars: compressed playback of measured medians.
Library First build from source One consumer edit + link
M1 Pro · median of 5 fresh builds / 12 edits · earlier baseline First build scope differs: libuv includes CMake + its full 37-file C library.
↓ included by all 100 consumers
Shared-header edit · 100 consumers + link · four compiler jobs
Bars: compressed playback of measured medians.
Library Fresh build · 4 jobs Edit one .cpp Edit header · 4 jobs Edit header · serial
M1 Pro · three rounds · measured compile + link wall time · implementation reused on edits Controlled example: the same working pipe program repeated in 100 files. Raw reports include ranges.
Sane C++ Libraries / Verification 23 / 34
Cross-platform verification loop
One working folder, three build environments The macOS host owns one SC working folder. Linux and Windows guests mount that same folder. The agent invokes the native macOS command and remote commands in both virtual machines, then uses their results for the next edit.
macOS host
One working folder SC source · edit once
macOS Native build + tests working folder agent → local command
Linux VM Shared-folder mount /media/psf/… agent → SSH command
Windows VM Shared-folder mount //Mac/Developer/… agent → SSH command
Same files, different mount paths. No commit needed to transfer an edit.
Linux · SSH macOS · ./SC.sh Windows · Bash / SSH
One agent invokes the host and both guests, then feeds the test results into the next edit.
One source tree on the host, mounted into both guests. Each OS builds its own configuration.
A real development thread: a fixture correction, followed by checks on all three hosts.
Linux via SSH · macOS locally · Windows Bash via SSH — against the same working folder.
Sane C++ Libraries / Verification 24 / 34
Automation keeps agents on rails agentchange
01 Dependencies Detect accidental new library coupling
02 Formatting Mechanical consistency
03 Single-file generation Two generators agree; artifacts compile
04 Platform tests Debug / Release; crypto ASan / UBSan
05 Compatibility STL interop; public headers without runtime linkage
06 Build-system tests SC-build regression fixtures and examples
07 Coverage Measure exercised paths
08 Documentation Build, artifacts and website trigger
The agent can propose a change. Repeatable checks test whether it fits the project.
Sane C++ Libraries / Persistent decisions 25 / 34
Architecture Decision Records SaneCppLibraries/ ├ Libraries/ ├ Tests/ └ Architecture/ ├ Global/ └ Async/ async-0002-… .md
ASYNC-0002 Accepted
Keep submit, poll and dispatch separate CONTEXT GUI and frame loops need control over when callbacks run.
DECISION Expose submitRequests, blockingPoll and dispatchCompletions alongside run().
PREVENTS Collapsing the phases into run() and breaking GUI integration.
An ADR records a decision, its constraints, rationale and consequences.
Find it in Architecture/<library>/. It is persistent context for a new agent.
The API shows three phases. The ADR explains why all three must remain.
Sane C++ Libraries / Agents 26 / 34
Skills: APIs, design and simplification Skills/ sane-cpp-libraries/ SKILL.md sane-cpp-style/ SKILL.md sane-cpp-simplify/ SKILL.md
sane-cpp-libraries Use Sane’s APIs Types, contracts, setup and examples.
sane-cpp-style Apply the design elsewhere Caller-owned storage, grouped lifetimes and independent APIs.
sane-cpp-simplify Simplify existing code Propose substantial changes. Preserve contracts. Implement selected proposals and verify the result.
name + description → SKILL.md → relevant references
Three repository skills: use the APIs, carry the design, simplify the implementation.
The API skill helps an agent use the actual library contracts.
The style skill carries the architectural approach into your own libraries.
The simplification skill maintains a proposal board, decisions, evidence and validation.
Sane C++ Libraries / Agents 27 / 34
Simplification: a persistent proposal dashboard Browse proposals, review evidence and risks, then select the next batch.
Sane C++ Libraries / Agents 28 / 34
Skill evaluation
Sane C++ Libraries / Contributing 29 / 34
AI contributions: credit the problem-solving What I ask contributors for An actual contribution
For casual contributions, I value a precise problem description more than a proposed code patch.
Bug report Reproduction steps, environment, expected and actual behavior.
Feature request The use case, why it matters, and how it fits the project.
An issue or PR can provide that input. Code is one possible solution; I decide the implementation and whether the change belongs.
Core contributors already share the project context.
ISSUE #31 · cozis A failed upgrade takes down the server “the server should keep running and keep serving every other connection.” Expected behavior · original issue excerpt Reproducer disables AF_ALG on Linux.
→ MAINTAINER + AGENT Implement, verify, merge My agent wrote the fix from the report. Failed upgrades return an error; the server keeps running.
MERGED COMMIT · EXCERPT AsyncWebServer: Contain WebSocket upgrade failures Fixes #31 Co-authored-by: Francesco Cozzuto
Francesco Cozzuto / cozis · original issue ↗
Sane C++ Libraries / Next step 30 / 34
Try Sane C++ Libraries Scan for the repository Stefano Cristiano Questions?
Sane C++ Libraries / Backup / SC-build 31 / 34
SC-build: first native backend build
First successful run of the self-hosted native C++ backend
Sane C++ Libraries / Backup / SC-build 32 / 34
SC-build: bootstrap and global installation
Download SC-build.sh → make it executable → clone and install Sane globally
Sane C++ Libraries / Backup / AsyncExecution 33 / 34
AsyncExecution: sender composition
A prototype built with Codex at C++ Day 2026 · 17 October.
Experimental owning facade · ex = SC::AsyncExecutionProtocol
AsyncExecution app;
SC_TRY(app.create(2));
auto sendEncoded = [&](StringSpan file, uint16_t port)
{
return app.readFile(file)
| ex::continues_on(app.getPoolScheduler())
| ex::then(base64Encode)
| ex::let_value([&, port](auto& message) noexcept {
return app.sendTCP("127.0.0.1", port, move(message));
});
};
SC_TRY(app.run(ex::when_all(
sendEncoded("first.txt", 19092),
sendEncoded("second.txt", 19093))));
Inspired by Alberto Barbati’s C++26 Execution Control talk. C++ Day 2026 ↗
Sane C++ Libraries / Backup / AsyncExecution 34 / 34
AsyncExecution: two joined transfers
File I/O on the pool · Base64 on a worker · TCP through Async
first.txt readFilePool file I/O
Base64 continues_on → thenPool worker
TCP :19092 let_value → sendTCPNative Async requests
second.txt readFilePool file I/O
Base64 continues_on → thenPool worker
TCP :19093 let_value → sendTCPNative Async requests
when_allJoin both transfers
Verified on macOS, Linux and Windows Debug + Release: 319 assertions each.
Both payloads independently decoded and checked.
Missing-file error and sibling cancellation checked.
From experiment to supported adapter? async-execution ↗
Possible next step: a proper specification.
Evaluate support alongside fibers and coroutines.
C++26 draft N5046 protocols · stdexec reference provider · native standard-library senders unavailable locally