From 3c47a629beabbbcdf40de9cae77f3417fc88ece5 Mon Sep 17 00:00:00 2001 From: chodak166 Date: Sun, 23 Aug 2026 19:09:16 +0200 Subject: [PATCH] Add initial boost::leaf examples within cmake/vcpkg project --- .dockerignore | 4 + CMakeLists.txt | 31 ++++++++ Dockerfile | 39 ++++++++++ README.md | 78 ++++++++++++++++++++ docker-compose.yml | 8 ++ scripts/run_all.sh | 11 +++ src/leaf_mutable_block.cpp | 102 ++++++++++++++++++++++++++ src/leaf_on_error_example.cpp | 130 +++++++++++++++++++++++++++++++++ src/leaf_universal_handler.cpp | 106 +++++++++++++++++++++++++++ vcpkg.json | 8 ++ 10 files changed, 517 insertions(+) create mode 100644 .dockerignore create mode 100644 CMakeLists.txt create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 scripts/run_all.sh create mode 100644 src/leaf_mutable_block.cpp create mode 100644 src/leaf_on_error_example.cpp create mode 100644 src/leaf_universal_handler.cpp create mode 100644 vcpkg.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2fe1fce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +build/ +.git/ +answer.md +README.md diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ba3570e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.21) +project(leaf_examples LANGUAGES CXX) + +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Boost CONFIG COMPONENTS leaf) +if(NOT TARGET Boost::leaf) + find_package(Boost REQUIRED) + set(LEAF_TARGET Boost::headers) +else() + set(LEAF_TARGET Boost::leaf) +endif() + +set(EXAMPLES + leaf_on_error_example + leaf_mutable_block + leaf_universal_handler +) + +foreach(example IN LISTS EXAMPLES) + add_executable(${example} src/${example}.cpp) + target_link_libraries(${example} PRIVATE ${LEAF_TARGET}) +endforeach() + +install( + TARGETS ${EXAMPLES} + RUNTIME DESTINATION bin +) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..61b9583 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1 + +FROM debian:bookworm AS build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + git \ + ca-certificates \ + curl \ + zip \ + unzip \ + tar \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +ENV VCPKG_ROOT=/opt/vcpkg +RUN git clone --depth 1 https://github.com/microsoft/vcpkg.git ${VCPKG_ROOT} \ + && ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics + +WORKDIR /app +COPY vcpkg.json CMakeLists.txt ./ +COPY src ./src +COPY scripts ./scripts + +RUN cmake -S . -B build -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build \ + && cmake --install build --prefix /out + +FROM debian:bookworm-slim + +WORKDIR /app +COPY --from=build /out/bin/ ./ +COPY scripts/run_all.sh ./ + +ENTRYPOINT ["/app/run_all.sh"] diff --git a/README.md b/README.md index e69de29..0b3b441 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,78 @@ +# Boost.LEAF Diagnostic Propagation Examples + +Three small C++ programs demonstrating how [Boost.LEAF](https://boostorg.github.io/leaf/) +propagates rich diagnostic information together with an error code, without throwing +exceptions and without exceptions needing to be enabled in the codebase. + +- `src/leaf_on_error_example.cpp` — distinct context structs attached per scope with + `leaf::on_error`; handlers matched by the exact set of available types. +- `src/leaf_mutable_block.cpp` — a single mutable `diagnostic_context` struct, re-registered + with `leaf::on_error` as the operation progresses, so the handler receives the latest + snapshot of all fields. +- `src/leaf_universal_handler.cpp` — handler selection: specific handlers first, generic + "universal" handlers later, plus a final fallback. Shows how LEAF picks the best match. + +## Core idea + +LEAF returns errors as values of type `leaf::result` instead of throwing exceptions. +At each point where an error can occur, arbitrary diagnostic values (file names, user info, +chunk numbers, timestamps, ...) are registered with `leaf::on_error(value)`. If +`leaf::new_error(...)` is returned, all registered values are captured into the current +error context, and the top level uses `leaf::try_handle_all(try_block, handlers...)` to let +the handlers inspect whichever context types were active when the error was created. + +Handler matching is done purely on types: a handler requesting +`(write_error const&, file_context const&, user_context const&)` will only run if the error +carries exactly those registered values; a more general handler is used otherwise. This +gives exception-like separation between "raising" a failure and "dealing with" it, but with +explicit `result` values, no exceptions, and no RTTI dependency. + +The examples intentionally return distinct exit codes from the handlers so each one +reports which handler actually matched. + +## Local build + +Requirements: CMake >= 3.21, a C++17 compiler, and [vcpkg](https://vcpkg.io) (git clone + +bootstrap). The dependency is header-only `boost-leaf`. + +```bash +git clone https://github.com/microsoft/vcpkg.git +./vcpkg/bootstrap-vcpkg.sh + +cmake -S . -B build \ + -DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Run one of the examples: + +```bash +./build/leaf_on_error_example +``` + +If you already have Boost (>= 1.75) installed at the system level, plain +`cmake -S . -B build` also works; the CMake project picks `Boost::leaf` from a vcpkg +toolchain when available and falls back to the system header target otherwise. + +## Docker + +```bash +docker build -t leaf-examples . +docker run --rm leaf-examples +``` + +The first (build) stage installs the toolchain, clones vcpkg, downloads `boost-leaf` +through the `vcpkg.json` manifest and builds all three examples. The second (run) +stage copies the binaries into a slim image and executes them one after another, +printing the exit code of each. + +## Docker Compose (v2) + +```bash +docker compose up --build +``` + +runs the same build and then starts a container that executes all three examples. +`docker compose build` builds without running; `docker compose run --rm leaf-examples` +works too. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7a0b07a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + leaf-examples: + build: + context: . + dockerfile: Dockerfile + network: host + image: local/leaf-examples + container_name: leaf-examples diff --git a/scripts/run_all.sh b/scripts/run_all.sh new file mode 100755 index 0000000..32602ba --- /dev/null +++ b/scripts/run_all.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -u + +apps="leaf_on_error_example leaf_mutable_block leaf_universal_handler" + +for app in ${apps}; do + printf '\n=== %s ===\n' "${app}" + "./${app}" + printf '(exit code: %s)\n' "$?" +done +exit 0 diff --git a/src/leaf_mutable_block.cpp b/src/leaf_mutable_block.cpp new file mode 100644 index 0000000..8da586c --- /dev/null +++ b/src/leaf_mutable_block.cpp @@ -0,0 +1,102 @@ +// leaf_mutable_block.cpp +#include +#include +#include +#include +#include +#include + +namespace leaf = boost::leaf; + +struct error_tag {}; +struct open_error {}; +struct write_error {}; + +struct diagnostic_context { + std::string file; + std::optional user; + std::optional privileges; + std::optional chunk; + std::optional chunk_size; + std::optional timestamp; +}; + +leaf::result write_chunk(int chunk, std::size_t size) +{ + if (chunk == 3) + return leaf::new_error(write_error{}); + + return {}; +} + +leaf::result process_file(std::string path, std::string user) +{ + error_tag tag; + diagnostic_context diag; + + diag.file = path; + diag.user = std::move(user); + diag.privileges = 0600; + + [[maybe_unused]] auto tag_ctx = leaf::on_error(tag); + // on_error captures a snapshot of diag, not a reference, so any + // fields mutated later would be invisible unless re-registered. + [[maybe_unused]] auto diag_ctx = leaf::on_error(diag); + + if (diag.file.empty()) + return leaf::new_error(open_error{}); + + for (int i = 0; i != 5; ++i) { + diag.chunk = i; + diag.chunk_size = 4096; + diag.timestamp = std::time(nullptr); + // Re-registering publishes the updated snapshot; the newest + // registration wins for the handler. + [[maybe_unused]] auto refresh = leaf::on_error(diag); + + auto r = write_chunk(i, 4096); + if (!r) + return r; + } + + return {}; +} + +int main() +{ + return leaf::try_handle_all( + []() -> leaf::result { + BOOST_LEAF_CHECK(process_file("data.bin", "alice")); + return 0; + }, + + [](error_tag const&, diagnostic_context const& d) { + // d is the newest snapshot taken before the error was raised, + // which is why chunk/chunk_size/timestamp are filled in here. + std::cerr << "operation failed: file=" << d.file; + + if (d.user) + std::cerr << ", user=" << *d.user; + + if (d.privileges) + std::cerr << ", privileges=" << *d.privileges; + + if (d.chunk) + std::cerr << ", chunk=" << *d.chunk; + + if (d.chunk_size) + std::cerr << ", chunk_size=" << *d.chunk_size; + + if (d.timestamp) + std::cerr << ", time=" << *d.timestamp; + + std::cerr << '\n'; + return 1; + }, + + [] { + std::cerr << "operation failed before diagnostic context existed\n"; + return 2; + } + ); +} diff --git a/src/leaf_on_error_example.cpp b/src/leaf_on_error_example.cpp new file mode 100644 index 0000000..6d81bc1 --- /dev/null +++ b/src/leaf_on_error_example.cpp @@ -0,0 +1,130 @@ +// leaf_on_error_example.cpp +#include +#include +#include +#include +#include + +namespace leaf = boost::leaf; + +struct error_tag {}; +struct open_error {}; +struct write_error {}; + +struct file_context { + std::string path; +}; + +struct user_context { + std::string name; + int privileges; +}; + +struct chunk_context { + int chunk; + std::size_t size; + std::time_t timestamp; +}; + +leaf::result write_chunk(int chunk, std::size_t size) +{ + chunk_context cc{chunk, size, std::time(nullptr)}; + // Registers cc to be captured only if an error unwinds this scope. + // On success the registration is discarded. + [[maybe_unused]] auto ctx = leaf::on_error(cc); + + if (chunk == 3) + // The context registered above rides along with this error + // even though handlers live several frames up. + return leaf::new_error(write_error{}); + + return {}; +} + +leaf::result process_file(std::string path, std::string user) +{ + error_tag tag; + file_context fc{std::move(path)}; + user_context uc{std::move(user), 0600}; + + [[maybe_unused]] auto tag_ctx = leaf::on_error(tag); + [[maybe_unused]] auto file_ctx = leaf::on_error(fc); + [[maybe_unused]] auto user_ctx = leaf::on_error(uc); + + // write_chunk knows nothing about the contexts registered here; + // LEAF attaches them automatically to any error it propagates. + for (int i = 0; i != 5; ++i) { + auto r = write_chunk(i, 4096); + if (!r) + return r; + } + + return {}; +} + +int main() +{ + return leaf::try_handle_all( + []() -> leaf::result { + // Try block and handlers must return the same type. + BOOST_LEAF_CHECK(process_file("data.bin", "alice")); + return 0; + }, + + // A handler runs only if the error carries every type it + // requests; handlers below with fewer parameters match the + // same errors when richer contexts were never registered. + [](write_error const&, + error_tag const&, + file_context const& f, + user_context const& u, + chunk_context const& c) { + std::cerr << "write failed: " + << "file=" << f.path + << ", user=" << u.name + << ", chunk=" << c.chunk + << ", size=" << c.size + << ", time=" << c.timestamp + << '\n'; + return 1; + }, + + [](open_error const&, + error_tag const&, + file_context const& f, + user_context const& u) { + std::cerr << "open failed: " + << "file=" << f.path + << ", user=" << u.name + << '\n'; + return 2; + }, + + [](error_tag const&, + file_context const& f, + user_context const& u, + chunk_context const& c) { + std::cerr << "unknown error: " + << "file=" << f.path + << ", user=" << u.name + << ", chunk=" << c.chunk + << '\n'; + return 3; + }, + + [](error_tag const&, + file_context const& f, + user_context const& u) { + std::cerr << "unknown error: " + << "file=" << f.path + << ", user=" << u.name + << '\n'; + return 4; + }, + + [] { + std::cerr << "unknown error before file context\n"; + return 5; + } + ); +} diff --git a/src/leaf_universal_handler.cpp b/src/leaf_universal_handler.cpp new file mode 100644 index 0000000..166c8ff --- /dev/null +++ b/src/leaf_universal_handler.cpp @@ -0,0 +1,106 @@ +// leaf_universal_handler.cpp +#include +#include +#include +#include + +namespace leaf = boost::leaf; + +struct error_tag {}; +struct early_error {}; +struct unknown_stage_error {}; + +struct operation_context { + std::string operation; + std::string user; +}; + +struct stage_context { + std::string stage; +}; + +leaf::result stage_two() +{ + // stage_context exists only in this scope; handlers above cannot + // see it unless an error escapes while this registration is active. + stage_context sc{"stage_two"}; + [[maybe_unused]] auto ctx = leaf::on_error(sc); + + return leaf::new_error(unknown_stage_error{}); +} + +leaf::result run(bool fail_early) +{ + error_tag tag; + operation_context oc{"universal_handler", "alice"}; + + [[maybe_unused]] auto tag_ctx = leaf::on_error(tag); + [[maybe_unused]] auto op_ctx = leaf::on_error(oc); + + if (fail_early) + return leaf::new_error(early_error{}); + + return stage_two(); +} + +int execute(bool fail_early) +{ + return leaf::try_handle_all( + [fail_early]() -> leaf::result { + BOOST_LEAF_CHECK(run(fail_early)); + return 0; + }, + + // Specific handler: runs only for early_error. + [](early_error const&, + error_tag const&, + operation_context const& op) { + std::cerr << "early error: " + << "operation=" << op.operation + << ", user=" << op.user + << '\n'; + return 1; + }, + + // Universal handler: no specific error type, so it matches any + // error that carries error_tag + operation_context. This overload + // is selected when the error also carries stage_context (i.e. it + // came from stage_two). LEAF picks the handler whose parameter + // list matches the most context available. + [](error_tag const&, + operation_context const& op, + stage_context const& st) { + std::cerr << "universal handler with stage: " + << "operation=" << op.operation + << ", user=" << op.user + << ", stage=" << st.stage + << '\n'; + return 2; + }, + + // Same universal shape minus stage_context: matches errors that + // never passed through stage_two. + [](error_tag const&, + operation_context const& op) { + std::cerr << "universal handler without stage: " + << "operation=" << op.operation + << ", user=" << op.user + << '\n'; + return 3; + }, + + // Fallback for errors carrying nothing the other handlers accept + // (e.g. thrown exceptions not produced by new_error). + [] { + std::cerr << "fallback handler\n"; + return 4; + } + ); +} + +int main() +{ + execute(false); + execute(true); + return 0; +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..849610c --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/vcpkg.schema.json", + "name": "leaf-examples", + "version": "1.0.0", + "dependencies": [ + "boost-leaf" + ] +}