Repository: Viatorus/emio Branch: main Commit: 5d79a14d2969 Files: 147 Total size: 575.3 KB Directory structure: gitextract_uxa4xgnn/ ├── .clang-format ├── .clang-tidy ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── pages.yml ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── cmake/ │ ├── arm-none-eabi-toolchain.cmake │ ├── coverage.cmake │ ├── dev-mode.cmake │ ├── elf_to_size_coverage.py │ ├── folders.cmake │ ├── install-config.cmake │ ├── install-rules.cmake │ ├── lint-targets.cmake │ ├── lint.cmake │ ├── prelude.cmake │ ├── project-is-top-level.cmake │ ├── size-coverage.cmake │ └── variables.cmake ├── docs/ │ ├── API.md │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── DESIGN.md │ ├── DEVELOPING.md │ └── res/ │ └── class_diagram.puml ├── include/ │ └── emio/ │ ├── buffer.hpp │ ├── detail/ │ │ ├── args.hpp │ │ ├── bignum.hpp │ │ ├── bitset.hpp │ │ ├── conversion.hpp │ │ ├── ct_vector.hpp │ │ ├── format/ │ │ │ ├── args.hpp │ │ │ ├── decode.hpp │ │ │ ├── dragon.hpp │ │ │ ├── format_to.hpp │ │ │ ├── formatter.hpp │ │ │ ├── parser.hpp │ │ │ ├── ranges.hpp │ │ │ └── specs.hpp │ │ ├── misc.hpp │ │ ├── parser.hpp │ │ ├── predef.hpp │ │ ├── scan/ │ │ │ ├── args.hpp │ │ │ ├── parser.hpp │ │ │ ├── scan_from.hpp │ │ │ ├── scanner.hpp │ │ │ └── specs.hpp │ │ ├── utf.hpp │ │ ├── validated_string.hpp │ │ └── validated_string_storage.hpp │ ├── emio.hpp │ ├── format.hpp │ ├── formatter.hpp │ ├── iterator.hpp │ ├── ranges.hpp │ ├── reader.hpp │ ├── result.hpp │ ├── scan.hpp │ ├── scanner.hpp │ ├── std.hpp │ └── writer.hpp └── test/ ├── benchmark/ │ ├── CMakeLists.txt │ ├── bench_format.cpp │ └── bench_scan.cpp ├── compile_test/ │ ├── CMakeLists.txt │ └── compile.cpp ├── fuzzy/ │ └── dragon4/ │ ├── .gitignore │ ├── Makefile │ ├── main.cpp │ ├── rust_ref/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── seeds/ │ └── test1 ├── size_test/ │ ├── CMakeLists.txt │ ├── base.cpp │ ├── emio/ │ │ ├── doFormat_a.cpp │ │ ├── format_all.cpp │ │ ├── format_all_and_extra.cpp │ │ ├── format_and_scan_all.cpp │ │ ├── format_and_scan_all_runtime.cpp │ │ ├── format_and_write_int.cpp │ │ ├── format_double.cpp │ │ ├── format_int.cpp │ │ ├── format_int_twice.cpp │ │ ├── format_runtime.cpp │ │ ├── format_to.cpp │ │ ├── format_to_n.cpp │ │ ├── scan_all.cpp │ │ ├── scan_int.cpp │ │ ├── vformat.cpp │ │ └── write_int.cpp │ ├── fmt/ │ │ ├── doFormat_a.cpp │ │ ├── fmt_dragon.cpp │ │ ├── fmt_grisu.cpp │ │ ├── fmt_grisu_and_dragon.cpp │ │ ├── format_all.cpp │ │ ├── format_all_and_extra.cpp │ │ ├── format_int.cpp │ │ ├── format_int_twice.cpp │ │ ├── format_runtime.cpp │ │ ├── format_to.cpp │ │ ├── format_to_n.cpp │ │ └── vformat.cpp │ ├── std/ │ │ ├── locale.cpp │ │ ├── snprintf.cpp │ │ ├── snprintf_and_sscanf.cpp │ │ ├── sscanf.cpp │ │ ├── string.cpp │ │ ├── string_stream.cpp │ │ ├── to_chars.cpp │ │ ├── to_string_double.cpp │ │ └── to_string_int.cpp │ └── stubs.cpp ├── static_analysis/ │ ├── CMakeLists.txt │ └── test_main.cpp └── unit_test/ ├── CMakeLists.txt ├── detail/ │ ├── test_bignum.cpp │ ├── test_bitset.cpp │ ├── test_conversion.cpp │ ├── test_ct_vector.cpp │ ├── test_decode.cpp │ ├── test_dragon.cpp │ └── test_utf.cpp ├── integer_ranges.hpp ├── test_buffer.cpp ├── test_dynamic_format_spec.cpp ├── test_format.cpp ├── test_format_api.cpp ├── test_format_as.cpp ├── test_format_could_fail_api.cpp ├── test_format_emio_vs_fmt.cpp ├── test_format_string.cpp ├── test_format_to_api.cpp ├── test_format_to_n_api.cpp ├── test_formatted_size.cpp ├── test_formatter.cpp ├── test_iterator.cpp ├── test_print.cpp ├── test_ranges.cpp ├── test_reader.cpp ├── test_result.cpp ├── test_scan.cpp ├── test_std.cpp └── test_writer.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ # Run manually to reformat a file: # clang-format -i --style=file Language: Cpp BasedOnStyle: Google IndentPPDirectives: AfterHash IndentCaseLabels: false DerivePointerAlignment: false Standard: c++20 ColumnLimit: 120 AllowShortFunctionsOnASingleLine: Empty AllowShortLambdasOnASingleLine: None IndentRequiresClause: true ================================================ FILE: .clang-tidy ================================================ --- # Enable ALL the things! Except not really # cppcoreguidelines-avoid-magic-numbers/readability-magic-numbers: covers to many simply cases # modernize-use-trailing-return-type: purely stylistic suggestion # readability-identifier-length: not needed # bugprone-easily-swappable-parameters: covers to many simple cases # cppcoreguidelines-macro-usage: reduced to a minimum # bugprone-macro-parentheses: false positive # readability-function-cognitive-complexity: multiple if/else branches necessary to cover all use cases # cppcoreguidelines-non-private-member-variables-in-classes: same as misc-non-private-member-variables-in-classes # misc-non-private-member-variables-in-classes: allowed since classes are intended for single threading # cppcoreguidelines-pro-type-union-access: allowed since it is used because of performance and compile-time reasons # readability-else-after-return: allowed since if no else compile-time flow check fails and no optimization in debug # bugprone-exception-escape: to many false positives # cppcoreguidelines-pro-bounds-pointer-arithmetic: performance in debug # cppcoreguidelines-pro-bounds-constant-array-index:: performance Checks: > bugprone-*, concurrency-*, cppcoreguidelines-*, clang-analyzer-*, misc-*, modernize-*, performance-*, portability-*, readability-*, -clang-analyzer-osx*, -clang-analyzer-llvm*, -clang-analyzer-optin*, -clang-analyzer-unix*, -clang-analyzer-valist*, -clang-diagnostic-ignored-optimization-argument, -cppcoreguidelines-avoid-magic-numbers, -readability-magic-numbers, -modernize-use-trailing-return-type, -readability-identifier-length, -bugprone-easily-swappable-parameters, -cppcoreguidelines-macro-usage, -bugprone-macro-parentheses, -readability-function-cognitive-complexity, -cppcoreguidelines-non-private-member-variables-in-classes, -misc-non-private-member-variables-in-classes, -cppcoreguidelines-pro-type-union-access, -readability-else-after-return, -bugprone-exception-escape, -cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-const-or-ref-data-members, -misc-include-cleaner, WarningsAsErrors: '' CheckOptions: - key: readability-identifier-naming.AbstractClassCase value: 'lower_case' - key: readability-identifier-naming.ClassCase value: 'lower_case' - key: readability-identifier-naming.ClassConstantCase value: 'lower_case' - key: readability-identifier-naming.ClassMemberCase value: 'lower_case' - key: readability-identifier-naming.ClassMethodCase value: 'lower_case' - key: readability-identifier-naming.ConstantCase value: 'lower_case' - key: readability-identifier-naming.ConstantParameterCase value: 'lower_case' - key: readability-identifier-naming.ConstantPointerParameterCase value: 'lower_case' - key: readability-identifier-naming.ConstexprFunctionCase value: 'lower_case' - key: readability-identifier-naming.ConstexprMethodCase value: 'lower_case' - key: readability-identifier-naming.ConstexprVariableCase value: 'lower_case' - key: readability-identifier-naming.EnumCase value: 'lower_case' - key: readability-identifier-naming.EnumConstantCase value: 'lower_case' - key: readability-identifier-naming.FunctionCase value: 'lower_case' - key: readability-identifier-naming.GlobalConstantCase value: 'lower_case' - key: readability-identifier-naming.GlobalConstantPointerCase value: 'lower_case' - key: readability-identifier-naming.GlobalFunctionCase value: 'lower_case' - key: readability-identifier-naming.GlobalPointerCase value: 'lower_case' - key: readability-identifier-naming.GlobalVariableCase value: 'lower_case' - key: readability-identifier-naming.InlineNamespaceCase value: 'lower_case' - key: readability-identifier-naming.LocalConstantCase value: 'lower_case' - key: readability-identifier-naming.LocalConstantPointerCase value: 'lower_case' - key: readability-identifier-naming.LocalPointerCase value: 'lower_case' - key: readability-identifier-naming.LocalVariableCase value: 'lower_case' - key: readability-identifier-naming.MacroDefinitionCase value: 'UPPER_CASE' - key: readability-identifier-naming.MemberCase value: 'lower_case' - key: readability-identifier-naming.MethodCase value: 'lower_case' - key: readability-identifier-naming.NamespaceCase value: 'lower_case' - key: readability-identifier-naming.ParameterCase value: 'lower_case' - key: readability-identifier-naming.ParameterPackCase value: 'lower_case' - key: readability-identifier-naming.PointerParameterCase value: 'lower_case' - key: readability-identifier-naming.PrivateMemberCase value: 'lower_case' - key: readability-identifier-naming.PrivateMemberSuffix value: '_' - key: readability-identifier-naming.PrivateMethodCase value: 'lower_case' - key: readability-identifier-naming.ProtectedMemberCase value: 'lower_case' - key: readability-identifier-naming.ProtectedMemberSuffix value: '_' - key: readability-identifier-naming.ProtectedMethodCase value: 'lower_case' - key: readability-identifier-naming.PublicMemberCase value: 'lower_case' - key: readability-identifier-naming.PublicMethodCase value: 'lower_case' - key: readability-identifier-naming.ScopedEnumConstantCase value: 'lower_case' - key: readability-identifier-naming.StaticConstantCase value: 'lower_case' - key: readability-identifier-naming.StaticVariableCase value: 'lower_case' - key: readability-identifier-naming.StructCase value: 'lower_case' - key: readability-identifier-naming.TemplateParameterCase value: 'CamelCase' - key: readability-identifier-naming.TypeTemplateParameterIgnoredRegexp value: 'expr-type' # see https://github.com/llvm/llvm-project/issues/46097 - key: readability-identifier-naming.TemplateTemplateParameterCase value: 'CamelCase' - key: readability-identifier-naming.TypeAliasCase value: 'lower_case' - key: readability-identifier-naming.TypedefCase value: 'lower_case' - key: readability-identifier-naming.TypeTemplateParameterCase value: 'CamelCase' - key: readability-identifier-naming.UnionCase value: 'lower_case' - key: readability-identifier-naming.ValueTemplateParameterCase value: 'CamelCase' - key: readability-identifier-naming.VariableCase value: 'lower_case' - key: readability-identifier-naming.VirtualMethodCase value: 'lower_case' ... ================================================ FILE: .github/workflows/ci.yml ================================================ name: Continuous Integration on: push: branches: - main pull_request: jobs: checks: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install GCC run: | sudo apt update && sudo apt install -y gcc-11 g++-11 echo "CC=gcc-11" >> $GITHUB_ENV echo "CXX=g++-11" >> $GITHUB_ENV - name: Install cppcheck run: | sudo apt update && sudo apt install -y cppcheck cppcheck --version - name: Install clang-format and clang-tidy run: | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main' sudo apt update && sudo apt install -y clang-format-17 clang-tidy-17 sudo update-alternatives --remove-all clang-format || true sudo update-alternatives --remove-all clang-tidy || true sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-17 1000 sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-17 1000 clang-tidy --version clang-format --version - name: Run clang-format run: cmake -D FORMAT_COMMAND=clang-format -P cmake/lint.cmake - name: Run cppcheck and clang-tidy # Cppcheck's checks are disabled. They are mostly redundant to clang-tidy and there are too many false positives. # But the parsing of cppcheck is kept to be compatible with its compiler frontend. if: ${{ !cancelled() }} run: | cmake --preset=ci-checks cmake --build build/test/static_analysis -j $(nproc) unit-tests: strategy: matrix: env: [ {os: ubuntu-22.04, compiler: gcc-11, std: 20}, {os: ubuntu-22.04, compiler: gcc-12, std: 20}, {os: ubuntu-22.04, compiler: gcc-12, std: 23}, {os: ubuntu-24.04, compiler: gcc-13, std: 20}, {os: ubuntu-24.04, compiler: gcc-13, std: 23}, {os: ubuntu-24.04, compiler: gcc-14, std: 20}, {os: ubuntu-24.04, compiler: gcc-14, std: 23}, {os: ubuntu-22.04, compiler: clang-16, std: 20}, {os: ubuntu-22.04, compiler: clang-16, std: 23}, {os: ubuntu-22.04, compiler: clang-17, std: 20}, {os: ubuntu-22.04, compiler: clang-17, std: 23}, {os: ubuntu-24.04, compiler: clang-18, std: 20}, {os: ubuntu-24.04, compiler: clang-18, std: 23}, {os: ubuntu-24.04, compiler: clang-19, std: 20}, {os: ubuntu-24.04, compiler: clang-19, std: 23} ] build_type: [ Debug, Release ] runs-on: ${{ matrix.env.os }} steps: - uses: actions/checkout@v3 - name: Install Compiler run: | if [[ "$compiler" == "gcc-11" ]]; then sudo apt update && sudo apt install -y gcc-11 g++-11 echo "CC=gcc-11" >> $GITHUB_ENV echo "CXX=g++-11" >> $GITHUB_ENV elif [[ "$compiler" == "gcc-12" ]]; then sudo apt update && sudo apt install -y gcc-12 g++-12 echo "CC=gcc-12" >> $GITHUB_ENV echo "CXX=g++-12" >> $GITHUB_ENV elif [[ "$compiler" == "gcc-13" ]]; then sudo apt update && sudo apt install -y gcc-13 g++-13 echo "CC=gcc-13" >> $GITHUB_ENV echo "CXX=g++-13" >> $GITHUB_ENV elif [[ "$compiler" == "clang-16" ]]; then wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-16 main' sudo apt update && sudo apt install -y clang-16 echo "CC=clang-16" >> $GITHUB_ENV echo "CXX=clang++-16" >> $GITHUB_ENV elif [[ "$compiler" == "clang-17" ]]; then wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main' sudo apt update && sudo apt install -y clang-17 echo "CC=clang-17" >> $GITHUB_ENV echo "CXX=clang++-17" >> $GITHUB_ENV elif [[ "$compiler" == "clang-18" ]]; then wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main' sudo apt update && sudo apt install -y clang-18 echo "CC=clang-18" >> $GITHUB_ENV echo "CXX=clang++-18" >> $GITHUB_ENV elif [[ "$compiler" == "clang-20" ]]; then wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main' sudo apt update && sudo apt install -y clang-20 echo "CC=clang-20" >> $GITHUB_ENV echo "CXX=clang++-20" >> $GITHUB_ENV fi env: compiler: ${{ matrix.env.compiler }} - name: Configure run: cmake --preset=ci-ubuntu -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_CXX_STANDARD=${{ matrix.env.std }} - name: Build Catch2 run: cmake --build build -t Catch2WithMain -j $(nproc) - name: Build run: cmake --build build -j $(nproc) - name: Install run: cmake --install build --prefix prefix - name: Test working-directory: build run: ctest --verbose --output-on-failure coverage: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install GCC run: | sudo apt update && sudo apt install -y gcc-11 g++-11 echo "CC=gcc-11" >> $GITHUB_ENV echo "CXX=g++-11" >> $GITHUB_ENV - name: Install LCov run: sudo apt update && sudo apt install lcov -q -y - name: Configure run: cmake --preset=ci-coverage - name: Build Catch2 run: cmake --build build -t Catch2WithMain -j $(nproc) - name: Build run: cmake --build build/test/unit_test -j $(nproc) - name: Test working-directory: build/test/unit_test run: ctest --output-on-failure - name: Process coverage info run: cmake --build build -t coverage - name: Submit to codecov.io uses: codecov/codecov-action@v3 with: files: build/coverage.info memcheck: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - name: Install GCC run: | sudo apt update && sudo apt install -y gcc-14 g++-14 echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV - name: Install valgrind run: sudo apt update && sudo apt install -y valgrind - name: Configure run: cmake --preset=ci-memcheck - name: Build Catch2 run: cmake --build build -t Catch2WithMain -j $(nproc) - name: Build run: cmake --build build -j $(nproc) - name: Install run: cmake --install build --prefix prefix - name: Test working-directory: build run: | if ! ctest --verbose --output-on-failure -T memcheck; then find Testing/Temporary -name "MemoryChecker.*.log" -exec cat {} + exit 1 fi sanitize: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - name: Install GCC run: | sudo apt update && sudo apt install -y gcc-14 g++-14 echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV - name: Configure run: cmake --preset=ci-sanitize - name: Build Catch2 run: cmake --build build -t Catch2WithMain -j $(nproc) - name: Build run: cmake --build build/test/unit_test -j $(nproc) - name: Test working-directory: build/test/unit_test env: ASAN_OPTIONS: "strict_string_checks=1:\ detect_stack_use_after_return=1:\ check_initialization_order=1:\ strict_init_order=1:\ detect_leaks=1" UBSAN_OPTIONS: print_stacktrace=1 run: ctest --output-on-failure size-test: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install GCC run: | sudo apt update && sudo apt install -y gcc-11 g++-11 echo "CC=gcc-11" >> $GITHUB_ENV echo "CXX=g++-11" >> $GITHUB_ENV - name: Configure run: cmake --preset=ci-size-coverage -DCMAKE_BUILD_TYPE=MinSizeRel - name: Build run: cmake --build build/test/size_test -j $(nproc) - name: Size-Coverage run: cmake --build build -t size-coverage size-test-embedded: strategy: matrix: preset: [ ci-size-coverage-embedded, ci-size-coverage-embedded-nano ] runs-on: ubuntu-22.04 env: DOWNLOAD_LINK: https://developer.arm.com/-/media/Files/downloads/gnu/11.3.rel1/binrel/arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi.tar.xz steps: - uses: actions/checkout@v3 - name: Cache toolchain id: cache-toolchain uses: actions/cache@v4 with: path: arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi key: ${{ env.DOWNLOAD_LINK }} - name: Install GCC ARM none eabi if: steps.cache-toolchain.outputs.cache-hit != 'true' run: | curl -L $DOWNLOAD_LINK | tar xJ - name: Configure run: cmake --preset=${{ matrix.preset }} -DCMAKE_BUILD_TYPE=MinSizeRel - name: Build run: cmake --build build/test/size_test -j $(nproc) - name: Size-Coverage run: cmake --build build -t size-coverage # - name: Coveralls - Doesn't work. # uses: coverallsapp/github-action@master # with: # github-token: ${{ secrets.GITHUB_TOKEN }} # path-to-lcov: build/test/size_test/size-coverage.info ================================================ FILE: .github/workflows/pages.yml ================================================ name: Deploy Github Pages on: push: branches: - main tags: - "[0-9]+.[0-9]+.[0-9]+" jobs: deploy-pages: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@v3 - name: Generate single header file run: | pip install quom if [[ -z "$(git tag --points-at HEAD)" ]]; then DST=trunk else DST=$(git tag) fi mkdir -p web/dist/$DST quom include/emio/emio.hpp web/dist/$DST/emio.hpp - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: branch: gh-pages folder: web/ clean: false ================================================ FILE: .gitignore ================================================ .idea/ .vs/ .vscode/ build/ cmake/open-cpp-coverage.cmake cmake-build-*/ prefix/ CMakeLists.txt.user CMakeUserPresets.json ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) include(cmake/prelude.cmake) project( emio VERSION 0.2.0 DESCRIPTION "Character input/output library for embedded systems." HOMEPAGE_URL "https://github.com/viatorus/emio" LANGUAGES CXX ) include(cmake/project-is-top-level.cmake) include(cmake/variables.cmake) # ---- Declare library ---- add_library(emio_emio INTERFACE) add_library(emio::emio ALIAS emio_emio) set_property( TARGET emio_emio PROPERTY EXPORT_NAME emio ) target_include_directories( emio_emio ${warning_guard} INTERFACE "$" ) target_compile_features(emio_emio INTERFACE cxx_std_20) # ---- Install rules ---- if (NOT CMAKE_SKIP_INSTALL_RULES) include(cmake/install-rules.cmake) endif () # ---- Developer mode ---- if (NOT emio_DEVELOPER_MODE) return() elseif (NOT PROJECT_IS_TOP_LEVEL) message( AUTHOR_WARNING "Developer mode is intended for developers of emio" ) endif () include(cmake/dev-mode.cmake) ================================================ FILE: CMakePresets.json ================================================ { "version": 2, "cmakeMinimumRequired": { "major": 3, "minor": 14, "patch": 0 }, "configurePresets": [ { "name": "cmake-pedantic", "hidden": true, "warnings": { "dev": true, "deprecated": true, "uninitialized": true, "unusedCli": true, "systemVars": false }, "errors": { "dev": true, "deprecated": true } }, { "name": "dev-mode", "hidden": true, "inherits": "cmake-pedantic", "cacheVariables": { "emio_DEVELOPER_MODE": "ON" } }, { "name": "cppcheck", "hidden": true, "cacheVariables": { "CMAKE_CXX_CPPCHECK": "cppcheck;--error-exitcode=13" } }, { "name": "clang-tidy", "hidden": true, "cacheVariables": { "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/*;-warnings-as-errors=*" } }, { "name": "valgrind", "hidden": true, "cacheVariables": { "MEMORYCHECK_COMMAND_OPTIONS": "--error-exitcode=1 --leak-check=full" } }, { "name": "ci-std", "description": "This preset makes sure the project actually builds with at least the specified standard", "hidden": true, "cacheVariables": { "CMAKE_CXX_EXTENSIONS": "OFF", "CMAKE_CXX_STANDARD": "23", "CMAKE_CXX_STANDARD_REQUIRED": "ON" } }, { "name": "flags-unix", "hidden": true, "environment": { "CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wshadow -Wformat=2 -Wundef" } }, { "name": "ci-unix", "generator": "Unix Makefiles", "hidden": true, "inherits": [ "flags-unix", "ci-std" ], "cacheVariables": { "CMAKE_CXX_FLAGS": "$env{CXX_FLAGS}", "CMAKE_BUILD_TYPE": "Release" } }, { "name": "ci-arm-none-eabi", "generator": "Unix Makefiles", "hidden": true, "inherits": [ "flags-unix", "ci-std" ], "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-none-eabi-toolchain.cmake", "CMAKE_CXX_FLAGS": "$env{CXX_FLAGS} -ffunction-sections -fdata-sections -mcpu=cortex-m7 -mthumb -g0 -fno-exceptions -fno-rtti", "CMAKE_EXE_LINKER_FLAGS": "-mcpu=cortex-m7 -mthumb -Wl,--gc-sections", "CMAKE_BUILD_TYPE": "MinSizeRel" } }, { "name": "ci-arm-none-eabi-nano", "generator": "Unix Makefiles", "hidden": true, "inherits": [ "ci-arm-none-eabi" ], "cacheVariables": { "CMAKE_EXE_LINKER_FLAGS": "-mcpu=cortex-m7 -mthumb -specs=nano.specs -Wl,--gc-sections" } }, { "name": "coverage-unix", "inherits": [ "ci-build", "ci-unix" ], "hidden": true, "cacheVariables": { "ENABLE_COVERAGE": "ON", "CMAKE_BUILD_TYPE": "Coverage", "CMAKE_CXX_FLAGS_COVERAGE": "-O0 -g --coverage", "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage", "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage" } }, { "name": "ci-coverage", "inherits": [ "coverage-unix", "dev-mode" ], "cacheVariables": { "COVERAGE_HTML_COMMAND": "" } }, { "name": "ci-sanitize", "inherits": [ "ci-build", "ci-unix", "dev-mode" ], "cacheVariables": { "CMAKE_BUILD_TYPE": "Sanitize", "CMAKE_CXX_FLAGS_SANITIZE": "-O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common" } }, { "name": "ci-build", "binaryDir": "${sourceDir}/build", "hidden": true }, { "name": "ci-ubuntu", "inherits": [ "ci-build", "ci-unix", "dev-mode" ] }, { "name": "ci-memcheck", "inherits": [ "ci-build", "ci-unix", "valgrind", "dev-mode" ] }, { "name": "ci-checks", "inherits": [ "ci-build", "ci-unix", "clang-tidy", "cppcheck", "dev-mode" ] }, { "name": "ci-embedded", "inherits": [ "ci-build", "ci-arm-none-eabi", "dev-mode" ] }, { "name": "ci-size-coverage", "inherits": [ "ci-build", "ci-unix", "dev-mode" ], "cacheVariables": { "CMAKE_CXX_FLAGS": "$env{CXX_FLAGS} -ffunction-sections -fdata-sections -g0 -fno-exceptions -fno-rtti -fno-asynchronous-unwind-tables", "BUILD_SIZE_COVERAGE": "ON", "ENABLE_SIZE_COVERAGE": "ON", "SIZE_COVERAGE_HTML_COMMAND": "", "SIZE_TOOL": "size" } }, { "name": "ci-size-coverage-embedded", "inherits": [ "ci-build", "ci-arm-none-eabi", "dev-mode" ], "cacheVariables": { "BUILD_SIZE_COVERAGE": "ON", "ENABLE_SIZE_COVERAGE": "ON", "SIZE_COVERAGE_HTML_COMMAND": "", "SIZE_TOOL": "arm-none-eabi-size" } }, { "name": "ci-size-coverage-embedded-nano", "inherits": [ "ci-build", "ci-arm-none-eabi-nano", "dev-mode" ], "cacheVariables": { "BUILD_SIZE_COVERAGE": "ON", "ENABLE_SIZE_COVERAGE": "ON", "SIZE_COVERAGE_HTML_COMMAND": "", "SIZE_TOOL": "arm-none-eabi-size" } } ], "buildPresets": [ { "name": "default", "configurePreset": "ci-ubuntu" } ] } ================================================ FILE: LICENSE ================================================ Copyright (c) 2021 - present, Toni Neubert (viatorus/emio) Copyright (c) 2012 - present, Victor Zverovich (fmtlib/fmt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![logo](docs/res/logo.png) [![Continuous Integration](https://github.com/Viatorus/emio/actions/workflows/ci.yml/badge.svg)](https://github.com/Viatorus/emio/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/Viatorus/emio/branch/main/graph/badge.svg?token=7BQFK1PNLX)](https://codecov.io/gh/Viatorus/emio) [![Conan Center](https://img.shields.io/conan/v/emio)](https://conan.io/center/recipes/emio) **em{io}** is a safe and fast high-level and low-level character input/output library for bare-metal and RTOS based embedded systems with a very small binary footprint. ```cpp // High-level std::string str = emio::format("The answer is {}.", 42); // Format argument. int answer{}; emio::result scan_res = emio::scan(str, "The answer is {}.", answer); // Scan input string. if (scan_res) { emio::print("The answer is {}.", answer); // Output to console. } // Without using heap. emio::static_buffer<128> buf{}; emio::format_to(buf, "The answer is {:#x}.", 42).value(); buf.view(); // <- The answer is 0x2a. // Low-level emio::writer wrt{buf}; wrt.write_str(" In decimal: ").value(); wrt.write_int(42).value(); wrt.write_char('.').value(); buf.view(); // <- The answer is 0x2a. In decimal: 42. emio::reader rdr{"17c"}; EMIO_TRY(uint32_t number, rdr.parse_int()); // <- 17 EMIO_TRY(char suffix, rdr.read_char()); // <- c ``` [**This library is in beta status! Please help to make it fly!**](https://github.com/Viatorus/emio/milestone/1) * [API documentation](docs/API.md) * Try emio [online](https://godbolt.org/z/fP7z7MzbG). ## Yet another character input/output library Bare-metal and RTOS based embedded systems do have special requirements which are mostly overlooked by the C++ standard, its implementations and other libraries. Therefore, this library: * has a very small binary footprint **(~38 times smaller than fmtlib!)** * returns a result object instead of throwing an exception * provides a high-level and low-level API which can be used at compile-time Read more about it in the [DESIGN](docs/DESIGN.md) document. ## Including emio in your project - With CMake and fetch content ```cmake FetchContent_Declare( emio GIT_TAG main GIT_REPOSITORY https://github.com/Viatorus/emio.git GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(emio) ``` - Download the [single header file](https://viatorus.github.io/emio/) generated with [Quom](https://github.com/Viatorus/quom) - From [Conan Center](https://conan.io/center/recipes/emio) A compiler supporting C++20 is required. Tested with GCC 11/12/13 and Clang 16/17. ## Contributing See the [CONTRIBUTING](docs/CONTRIBUTING.md) document. ## Licensing em{io} is distributed under the [MIT license](LICENSE). ================================================ FILE: cmake/arm-none-eabi-toolchain.cmake ================================================ cmake_minimum_required(VERSION 3.14.0) set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_VERSION 1) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # do not link executable when testing the compiler. set(TOOLCHAIN_BIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi/bin) set(CMAKE_C_COMPILER ${TOOLCHAIN_BIN_PATH}/arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_BIN_PATH}/arm-none-eabi-g++) set(CMAKE_AR ${TOOLCHAIN_BIN_PATH}/arm-none-eabi-gcc-ar) set(CMAKE_RANLIB ${TOOLCHAIN_BIN_PATH}/arm-none-eabi-gcc-ranlib) ================================================ FILE: cmake/coverage.cmake ================================================ # ---- Variables ---- # We use variables separate from what CTest uses, because those have # customization issues set( COVERAGE_TRACE_COMMAND lcov -c -q -o "${PROJECT_BINARY_DIR}/coverage.info" -d "${PROJECT_BINARY_DIR}" --include "${PROJECT_SOURCE_DIR}/*" CACHE STRING "; separated command to generate a trace for the 'coverage' target" ) set( COVERAGE_HTML_COMMAND genhtml --legend -f -q "${PROJECT_BINARY_DIR}/coverage.info" -p "${PROJECT_SOURCE_DIR}" -o "${PROJECT_BINARY_DIR}/coverage_html" CACHE STRING "; separated command to generate an HTML report for the 'coverage' target" ) # ---- Coverage target ---- add_custom_target( coverage COMMAND ${COVERAGE_TRACE_COMMAND} COMMAND ${COVERAGE_HTML_COMMAND} COMMENT "Generating coverage report" VERBATIM ) ================================================ FILE: cmake/dev-mode.cmake ================================================ include(cmake/folders.cmake) set_property(GLOBAL PROPERTY CTEST_TARGETS_ADDED 1) include(CTest) if (BUILD_TESTING OR BUILD_SIZE_COVERAGE) set(CMAKE_MODULE_PATH CACHE INTERNAL FORCE ) set(FMT_HEADERS CACHE INTERNAL FORCE ) set(FMT_OS OFF CACHE INTERNAL "Path to downloaded Catch2 modules" FORCE ) Include(FetchContent) FetchContent_Declare( fmt GIT_TAG d9bc5f1320332db9a4bf7e103b0813b94e369304 # 9.1.1 - not released GIT_REPOSITORY https://github.com/fmtlib/fmt.git ) FetchContent_MakeAvailable(fmt) endif () if (BUILD_TESTING) Include(FetchContent) FetchContent_Declare( Catch2 GIT_TAG v3.4.0 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(Catch2) endif () if (BUILD_TESTING) add_subdirectory(test/benchmark) add_subdirectory(test/compile_test) add_subdirectory(test/static_analysis) add_subdirectory(test/unit_test) endif () if (BUILD_SIZE_COVERAGE) add_subdirectory(test/size_test) endif () option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF) if (ENABLE_COVERAGE) include(cmake/coverage.cmake) endif () include(cmake/lint-targets.cmake) add_folders(Project) ================================================ FILE: cmake/elf_to_size_coverage.py ================================================ #!/usr/bin/env python3 import argparse import re import subprocess import sys from io import StringIO from pathlib import Path def parse_args(args): parser = argparse.ArgumentParser( description='Creates the size difference of a base file size to multiple other files sizes as lcov info file') parser.add_argument('--size-tool', type=Path, help='path to the GNU size tool', metavar='SIZE_TOOL', required=True) parser.add_argument('base', type=Path, help='the first file is used as base size', metavar='BASE_FILE') parser.add_argument('files', type=Path, nargs='+', help='file(s) to compare', metavar='FILE') parser.add_argument('--output', '-o', type=Path, help='specify the output file', metavar='OUTPUT_FILE', required=True) return parser.parse_args(args) class Generator: def __init__(self, size_tool, base_file, out): self._size_tool = size_tool self._base_file = base_file.absolute() self._base_size = self._get_size(base_file) self._out = out self._out.write("TN:\n") print("{}: {} (base)".format(base_file, self._base_size)) def generate(self, file: Path): file_path = file.absolute() file_size = self._get_size(file) print('{} - {}'.format(file_path, file_size)) if file_size < self._base_size: raise Exception(f'{file} cannot be smaller than {self._base_file}') self._out.write(f'SF:{file_path}\n') for covered in range(self._base_size): self._out.write(f'DA:{covered},1\n') for uncovered in range(self._base_size, file_size): self._out.write(f'DA:{uncovered},0\n') self._out.write('end_of_record\n') def _get_size(self, file: Path): output = subprocess.check_output([str(self._size_tool), '--format=GNU', str(file)], encoding='UTF-8') # text data bss total filename # 348 120 28 496 base # Get total from regex: total = re.search('text\s+data\s+bss\s+total\s+filename\n\s+\d+\s+\d+\s+\d+\s+(\d+)\s+', output) if not total: raise Exception(f'Unexpected output from size: {output}') return int(total[1]) def run(): args = parse_args(sys.argv[1:]) if not args.base.is_file(): raise Exception(f'Base file not found: {args.base}') output = StringIO() generator = Generator(args.size_tool, args.base, output) for file in args.files: if not file.is_file(): raise Exception(f'File not found: {file}') generator.generate(file) args.output.write_text(output.getvalue()) if __name__ == '__main__': run() ================================================ FILE: cmake/folders.cmake ================================================ set_property(GLOBAL PROPERTY USE_FOLDERS YES) # Call this function at the end of a directory scope to assign a folder to # targets created in that directory. Utility targets will be assigned to the # UtilityTargets folder, otherwise to the ${name}Targets folder. If a target # already has a folder assigned, then that target will be skipped. function(add_folders name) get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) foreach (target IN LISTS targets) get_property(folder TARGET "${target}" PROPERTY FOLDER) if (DEFINED folder) continue() endif () set(folder Utility) get_property(type TARGET "${target}" PROPERTY TYPE) if (NOT type STREQUAL "UTILITY") set(folder "${name}") endif () set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets") endforeach () endfunction() ================================================ FILE: cmake/install-config.cmake ================================================ include("${CMAKE_CURRENT_LIST_DIR}/emioTargets.cmake") ================================================ FILE: cmake/install-rules.cmake ================================================ if (PROJECT_IS_TOP_LEVEL) set(CMAKE_INSTALL_INCLUDEDIR include/emio CACHE PATH "") endif () # Project is configured with no languages, so tell GNUInstallDirs the lib dir set(CMAKE_INSTALL_LIBDIR lib CACHE PATH "") include(CMakePackageConfigHelpers) include(GNUInstallDirs) # find_package() call for consumers to find this project set(package emio) install( DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" COMPONENT emio_Development ) install( TARGETS emio_emio EXPORT emioTargets INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" ) write_basic_package_version_file( "${package}ConfigVersion.cmake" COMPATIBILITY SameMajorVersion ARCH_INDEPENDENT ) # Allow package maintainers to freely override the path for the configs set( emio_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}" CACHE PATH "CMake package config location relative to the install prefix" ) mark_as_advanced(emio_INSTALL_CMAKEDIR) install( FILES cmake/install-config.cmake DESTINATION "${emio_INSTALL_CMAKEDIR}" RENAME "${package}Config.cmake" COMPONENT emio_Development ) install( FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" DESTINATION "${emio_INSTALL_CMAKEDIR}" COMPONENT emio_Development ) install( EXPORT emioTargets NAMESPACE emio:: DESTINATION "${emio_INSTALL_CMAKEDIR}" COMPONENT emio_Development ) if (PROJECT_IS_TOP_LEVEL) include(CPack) endif () ================================================ FILE: cmake/lint-targets.cmake ================================================ set( FORMAT_PATTERNS source/*.cpp source/*.hpp include/*.hpp test/*.cpp test/*.hpp example/*.cpp example/*.hpp CACHE STRING "; separated patterns relative to the project source dir to format" ) set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use") add_custom_target( format-check COMMAND "${CMAKE_COMMAND}" -D "FORMAT_COMMAND=${FORMAT_COMMAND}" -D "PATTERNS=${FORMAT_PATTERNS}" -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" COMMENT "Linting the code" VERBATIM ) add_custom_target( format-fix COMMAND "${CMAKE_COMMAND}" -D "FORMAT_COMMAND=${FORMAT_COMMAND}" -D "PATTERNS=${FORMAT_PATTERNS}" -D FIX=YES -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" COMMENT "Fixing the code" VERBATIM ) ================================================ FILE: cmake/lint.cmake ================================================ cmake_minimum_required(VERSION 3.14) macro(default name) if (NOT DEFINED "${name}") set("${name}" "${ARGN}") endif () endmacro() default(FORMAT_COMMAND clang-format) default( PATTERNS source/*.cpp source/*.hpp include/*.hpp test/*.cpp test/*.hpp example/*.cpp example/*.hpp ) default(FIX NO) set(flag --output-replacements-xml) set(args OUTPUT_VARIABLE output) if (FIX) set(flag -i) set(args "") endif () file(GLOB_RECURSE files ${PATTERNS}) set(badly_formatted "") set(output "") string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length) foreach (file IN LISTS files) execute_process( COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" RESULT_VARIABLE result ${args} ) if (NOT result EQUAL "0") message(FATAL_ERROR "'${file}': formatter returned with ${result}") endif () if (NOT FIX AND output MATCHES "\n string_view` - Returns the name of the error code. *Example* ```cpp std::string_view error_msg = emio::to_string(emio::err::invalid_format); // invalid_format ``` ## result `template class result;` - The return type of almost all functions to propagate a value of type `T` on success or an error of type `emio::err` on failure. *constructor(arg)* - Constructable either from T or `emio::err`. `has_value() -> bool` - Checks whether the object holds a value. `has_error() -> bool` - Checks whether the object holds an error. `value() -> T` - Returns the value or throws/terminates if no value is held. `value_or(T) -> T` - Returns the value or returns the passed alternative if no value is held. `assume_value() -> T` - Returns the value without any checks. Invokes undefined behavior if no value is held. `error() -> emio::err` - Returns the error or throws/terminates if no error is held. `assume_error() -> emio::err` - Returns the error without any checks. Invokes undefined behavior if no error is held. There exists two helper macros to simplify the control flow: `EMIO_TRYV(expr)` - Evaluates an expression *expr*. If successful, continues the execution. If unsuccessful, immediately returns from the calling function. `EMIO_TRY(var, expr)` - Evaluates an expression *expr*. If successful, assigns the value to a declaration *var*. If unsuccessful, immediately returns from the calling function. *Example* ```cpp result parse_one(std::string_view sv) { if (sv.empty()) { return emio::err::eof; } if (sv == "1") { return 1; } else { return emio::err::invalid_data; } } emio::result parse(std::string_view sv) { EMIO_TRY(int val, parse_one(sv)); emio::result res = parse_one(sv); if (!res) { return res.error(); } if (res.assume_value() == val) { return emio::success; } return emio::err::invalid_format; } ``` ## Buffer An abstract class which provides functionality for receiving a contiguous memory region of chars to write into. There exist multiple implementation of a buffer, all fulfilling a different use case. Some buffers have an internal cache to provide a contiguous memory if the actually output object doesn't provide on. Additionally, some buffers can be reset to reuse the total capacity of the storage for the next operation. This invalidates any obtaining view! ### memory_buffer - An endless growing buffer with an internal storage for small buffer optimization. *Example* ```cpp emio::memory_buffer buf; emio::result> area = buf.get_write_area_of(50); assert(area); std::string_view view = buf.view(); assert(buf.capacity() >= view.size()); std::string str = buf.str(); buf.reset(); ``` ### span_buffer - A buffer over a specific contiguous range. *Example* ```cpp std::array storage; emio::span_buffer buf{storage}; assert(buf.capacity() == 512); emio::result> area = buf.get_write_area_of(50); assert(area); std::string_view view = buf.view(); std::string str = buf.str(); buf.reset(); ``` ### static_buffer - A buffer containing a fixed-size storage. *Example* ```cpp emio::static_buffer<512> buf{storage}; assert(buf.capacity() == 512); emio::result> area = buf.get_write_area_of(50); assert(area); std::string_view view = buf.view(); std::string str = buf.str(); buf.reset(); ``` ### iterator_buffer - A buffer for all kinds of output iterators (raw-pointers, back_insert_iterator or any other output iterator). - The buffer's with a direct output iterator (e.g. std::string::iterator) do have an internal cache. *Example* ```cpp std::string storage{"filled with something up"}; emio::iterator_buffer buf{std::back_inserter(storage)}; emio::result> area = buf.get_write_area_of(50); assert(area); assert(buf.flush()); std::back_insert_iterator out = buf.out(); ``` ### file_buffer - A buffer over an std::File (file stream) with an internal cache. *Example* ```cpp std::FILE* file = std::fopen("test", "w"); emio::file_buffer buf{file}; emio::result> area = buf.get_write_area_of(50); assert(area); assert(buf.flush()); buf.reset(); ``` ### truncating_buffer - A buffer which truncates the remaining output if the limit of another provided buffer is reached. *Example* ```cpp emio::static_buffer<48> primary_buf{}; emio::truncating_buffer buf{primary_buf, 32}; emio::result> area = buf.get_write_area_of(50); assert(area); assert(buf.flush()); assert(primary_buf.view().size() == 32); // Only 32 bytes are flushed. ``` ## Reader ` class reader;` - A class to read and parse a char sequence like a finite input stream. *constructor(input)* - Constructable from any suitable char sequence. *Example* ```cpp emio::reader input{"1"}; assert(input.cnt_remaining() == 1); assert(input.view_remaining() == "1"); std::string foo{"foo"}; emio::reader input2{foo}; assert(input2.cnt_remaining() == 3); assert(input2.view_remaining() == "foo"); ``` `peek() -> result` - Returns the next char without consuming it. *Example* ```cpp emio::reader input{"abc"}; emio::result res = input.peek(); assert(res == 'a'); ``` `read_char() -> result` - Returns one char. *Example* ```cpp emio::reader input{"abc"}; emio::result res = input.read_char(); assert(res == 'a'); ``` `read_n_char(n) -> result` - Returns *n* chars. *Example* ```cpp emio::reader input{"abc"}; emio::result res = input.read_n_char(3); assert(res == "abc"); ``` `parse_int(base = 10) -> result` - Parses an integer of type *T* with a specific *base*. *Example* ```cpp emio::reader input{"abc"}; emio::result res = input.read_int(16 /* hexadecimal */); assert(res == 0xabc); ``` `read_until/_char/str/any_of/none_of/([predicate,] options) -> result` - Reads n chars until a given *predicate* (delimiter/group/function) applies. - Has *options* to configure what should happen with the predicate and what should happen if EOF is reached. *Example* ```cpp emio::reader get_input() { return emio::reader{"abc"}; } // read_until_char emio::result res = get_input().read_until_char('c'); assert(res == "ab"); // read_until_str emio::result res = get_input().read_until_str("bc"); assert(res == "a"); // read_until_any_of emio::result res = get_input().read_until_any_of("cd"); assert(res == "ab"); // read_until_none_of emio::result res = get_input().read_until_none_of("ab"); assert(res == "ab"); // read_until with predicate emio::result res = get_input().read_until([](char c) { return c != 'a';}); assert(res == "a"); ``` ```cpp // Different options. emio::reader input{"abc 123 Hello"}; // with include_delimiter option emio::result res = input.read_until_str(" ", {.include_delimiter = true}); assert(res == "abc "); // with keep keep_delimiter option emio::result res = input.read_until_str("Hello", {.keep_delimiter = true}); assert(res == "123 "); // with ignore_eof option emio::result res = input.read_until_str("xyz", {.ignore_eof = true}); assert(res == "Hello"); ``` `read_if_match_char/str(c/str) -> result` - Reads one/multiple chars if *c/str* matches the next char/chars. *Example* ```cpp emio::reader input{"abc"}; if (input.read_if_match_char('a')) { emio::result res = input.read_if_match_str("bc"); assert(res == "bc"); } ``` ## Writer `class writer;` - A class to write sequences of characters or other kinds of data into an output buffer. *constructor(buffer)* *Example* ```cpp emio::static_buffer<128> buf; emio::writer output{buf}; ``` - Constructable from a reference to a buffer. `write_char(c) -> result` - Writes a char *c* into the buffer. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_char('a'); // Buffer contains "a" assert(res); ``` `write_char_n(c, n) -> result` - Writes a char *c* *n* times into the buffer. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_char_n('a', 5); // Buffer contains "aaaaa" assert(res); ``` `write_char_escaped(c) -> result` - Writes a char *c* escaped into the buffer. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_char_escaped('\n', 5); // Buffer contains "\\n" assert(res); ``` `write_str(sv) -> result` - Writes a char sequence *sv* into the buffer. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_str("Hello"); // Buffer contains "Hello" assert(res); ``` `write_str_escaped(sv) -> result` - Writes a char sequence *sv* escaped into the buffer. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_str("\t 'and'"); // Buffer contains "\\t \'and\'" assert(res); ``` `write_int(integer, options) -> result` - Writes an *integer* into the buffer. - Has *options* to configure the base and if the alphanumerics should be in lower or upper case. *Example* ```cpp emio::writer output{get_buffer()}; emio::result res = output.write_int(15); // Buffer contains "15" assert(res); res = output.write_int(15, {.base = 16, upper_case = true}); // Buffer contains "15F" assert(res); ``` ## Format The following functions use a format string syntax which is nearly identical to the one used in [fmt](https://fmt.dev/latest/syntax.html), which is similar to [str.format](https://docs.python.org/3/library/stdtypes.html#str.format) in Python. Things that are missing: - chrono syntax (planned) - 'a'/'A' for hexadecimal floating point format (TBD) - UTF-8 support (TBD) - using an identifier as arg_id: `fmt::format("{nbr}", fmt::arg("nbr", 42)` (TBD) - `'L'` options for locale (somehow possible but not with std::locale because of the binary size) The grammar for the replacement field is as follows: ```sass replacement_field ::= "{" [arg_id] [":" format_spec] "}" arg_id ::= integer integer ::= digit+ digit ::= "0"..."9" ``` The grammar for the format specification is as follows: ```sass format_spec ::= [[fill]align][sign]["#"]["0"][width][type] fill ::= align ::= "<" | ">" | "^" sign ::= "+" | "-" | " " width ::= integer type ::= "b" | "B" | "c" | "d" | "o" | "s" | "x" | "X" | "e" | "E" | "f" | "F" | "g" | "G" ``` The syntax of the format string is validated at compile-time. If a validation at runtime is required, the string must be wrapped inside a `runtime_string` object. There is a simple helper function for that: `runtime(string_view) -> runtime_string` Some functions (like `format` or `formatted_size`) are further optimized (simplified) in their return type if the format string is a valid-only format string that could be ensured at compile-time. `format(format_str, ...args) -> string/result` *Example* ```cpp std::string str = emio::format("Hello {}!", 42); assert(str == "Hello 42!"); std::string format_str = "Good by {}!"; emio::result res = emio::format(emio::runtime(format_str), 42); assert(res == "Good by 42!"); ``` - Formats arguments according to the format string, and returns the result as a string. - The return value depends on the type of the format string (valid-only type or not). `format_to(out, format_str, ...args) -> result` - Formats arguments according to the format string, and writes the result to the output iterator/buffer. **Note** If a raw output pointer or simple output iterator is used, no range checking can take place! *Example* ```cpp std::string out; out.resize(10); emio::result res = emio::format_to(out.begin(), "Hello {}!", 42); assert(res); assert(out == "Hello 42!"); ``` `format_to_n(out, n, format_str, ...args) -> result>` - Formats arguments according to the format string, and writes the result to the output iterator/buffer. At most *n* characters are written. *Example* ```cpp std::string out; out.resize(10); emio::result> res = emio::format_to_n(out.begin(), 7, "Hello {}!", 42); assert(res) assert(res->out == "Hello 4"); assert(res->size == 7); ``` `formatted_size(format_str, ...args) -> size_t/result` - Determines the total number of characters in the formatted string by formatting args according to the format string. - The return value depends on the type of the format string (valid-only type or not). *Example* ```cpp size_t size = emio::formatted_size("> {}", 42); assert(size == 4); ``` For each function there exists a function prefixed with v (e.g. `vformat`) which takes `format_args` instead of a format string and arguments. The types are erased and can be used in non-template functions to reduce build-time, hide implementations and reduce the binary size. **Note:** These type erased functions cannot be used at compile-time. `format_args` can be created with: `make_format_args(format_str, ...args) -> internal format_args_storage` - Returns an object that stores a format string with an array of all arguments to format. - Keep in mind that the storage uses reference semantics and does not extend the lifetime of args. It is the programmer's responsibility to ensure that args outlive the return value. *Example* ```cpp emio::result internal_info(const emio::format_args& args) { emio::memory_buffer buf; emio::writer out{buf}; // Prefix message. EMIO_TRYV(out.write_str("INFO: ")); EMIO_TRYV(emio::vformat_to(out, args)); log_message(out.view()); // Forward result. return emio::success; } template void log_info(emio::format_string fmt, const Args&...args) { emio::result res = internal_info(emio::make_format_args(fmt, args...)); // type-erasing takes place res.value(); // Throw on any error. } void do_something(int i) { log_info("Do something started with {}.", i); } int main() { do_something(42); // INFO: Do something started with 42. } ``` ### Dynamic format specification Unlike other libraries, the format specification cannot be changed through extra replacement fields, as it is possible e.g. with fmt to dynamically set the precision to 1 with `fmt::format("{:.{}f}", 3.14, 1);`. With emio it is possible to dynamically define _width_ and _precision_ through a `format_spec` object which is then passed as an argument with the original value to the format function. `format_spec{.width = , .precision = }` - If a spec is not defined inside the struct, the spec of the parsed format string will be applied. *Example* ```cpp emio::format_spec spec{.precision = 1}; emio::format("{}", spec.with(3.141592653)); // 3.1 ``` ### Formatter There exists formatter for builtin types like bool, char, string, integers, floats, void* and non-scoped enums, ranges and tuple like types. Support for other standard types (e.g. chrono duration, optional) is planned. For formatting values of pointer-like types, simply use `emio::ptr(p)`. *Example* ```cpp int* value = get(); emio::format("{}", emio::ptr(value)); ``` Use `is_formattable_v` to check if a type is formattable. A formatter exists of one optional function `validate` and two mandatory functions `parse` and `format`. If `validate` is not present, `parse` must validate the format string. *Example* ```cpp struct foo { int x; }; template <> class emio::formatter { public: /** * Optional static function to validate the format string syntax for this type. * @note If not present, the parse function is invoked for validation. * @param format_rdr The reader over the format string. * @return Success if the format string is valid. */ static constexpr result validate(reader& format_rdr) noexcept { return format_rdr.read_if_match_char('}'); } /** * Function to parse the format specs for this type. * @param format_rdr The reader over the format string. * @return Success if the format string is valid and could be parsed. */ constexpr result parse(reader& format_rdr) noexcept { return format_rdr.read_if_match_char('}'); } /** * Function to format the object of this type according to the parsed format specs. * @param out The output writer. * @param arg The argument to format. * @return Success if the formatting could be done. */ constexpr result format(writer& out, const foo& arg) const noexcept { return wtr.write_int(arg.x); } }; int main() { emio::format("{}", foo{42}); // 42 } ``` It is also possible to reuse existing formatters via inheritance or composition. *Example* ```cpp struct foo { int x; }; template <> class emio::formatter : public emio::format { public: constexpr result format(writer& out, const foo& arg) noexcept { return emio::format::format(wtr, arg.x); } }; int main() { emio::format("{:#x}", foo{42}); // 0x2a } ``` If the `validate` (or if absent the `parse`) function is not constexpr, a runtime format strings must be used. The `format` function don't need to be constexpr if the formatting shouldn't be done at compile-time. For simple type formatting, like formatting an enum class to its underlying integer or to a string, the function `format_as` could be provided. The function must be in the same namespace since ADL is used. *Example* ```cpp namespace foo { enum class bar { foobar, barfoo }; constexpr auto format_as(const bar& w) noexcept { return static_cast>(w); } } ``` ## Print It is possible to directly print to the standard output or other file streams. `print(format_str, ...args) -> void/result` - Formats arguments according to the format string, and writes the result to the standard output stream. - The return value depends on the type of the format string (valid-only type or not). *Example* ```cpp emio::print("{}!", 42); // Outputs: "42!" emio::result res = emio::print(emio::runtime("{}!"), 42); // Outputs: "42!" assert(res); ``` `print(file, format_str, ...args) -> result` - Formats arguments according to the format string, and writes the result to a file stream. *Example* ```cpp emio::result res = emio::print(stderr, "{}!", 42); // Outputs: "42!" to stderr assert(res); ``` `println(format_str, ...args) -> void/result` - Formats arguments according to the format string, and writes the result to the standard output stream with a new line at the end. - The return value depends on the type of the format string (valid-only type or not). *Example* ```cpp emio::println("{}!", 42); // Outputs: "42!" with a line break emio::result res = emio::println(emio::runtime("{}!"), 42); // Outputs: "42!" with a line break assert(res); ``` `println(file, format_str, ...args) -> result` - Formats arguments according to the format string, and writes the result to a file stream with a new line at the end. *Example* ```cpp emio::result res = emio::println(stderr, "{}!", 42); // Outputs: "42!" with a line break to stderr assert(res); ``` For each function there exists a function prefixed with v (e.g. `vprint`) which allow the same functionality as e.g. `vformat(...)` does for `format(...)`. ## Scan The following functions use a format string syntax which is similar to the format syntax of `format`. The grammar for the replacement field is the same. The grammar for the scan specific syntax is as follows: ```sass format_spec ::= ["#"][width][type] type ::= "b" | "B" | "c" | "d" | "o" | "s" | "x" | "X" ``` `#` - for integral types: the alternate form - b/B: `0b` (e.g. 0b10110) - d: nothing (e.g. 9825) - o: leading `0` (e.g. 057) - x/X: `0x` (e.g 0x2fA3) - if `#` is present but not the `type`, the base is deduced from the scanned alternate form. *Example* ```cpp int i; int j; int k; int l; scan("0b101 101 0101 0x101", "{:#} {:#} {:#} {:#}", i, j, k, l); assert(i == 0b101); assert(j == 101); assert(k == 0101); assert(l == 0x101); ``` `width` - specifies the number of characters to include when parsing an argument *Example* ```cpp int i; std::string_view j; int k; scan("125673", "{:2}{:3}{}", i, j, k); assert(i == 12); assert(j == "567"); assert(k == 3); ``` `type` - for integral types: the base to assume - b/B: base 2 (binary) - d: base 10 (decimal) - o: base 8 (octal) - x/X: base 16 (hexadecimal) - c for char - s for string/string_view *Example* ```cpp int i; int j; int k; int l; scan("101 101 101 101", "{:b} {:d} {:o} {:x}", i, j, k, l); assert(i == 0b101); assert(j == 101); assert(k == 0101); assert(l == 0x101); ``` The syntax of the format string is validated at compile-time. If a validation at runtime is required, the string must be wrapped inside a `runtime_string` object. There is a simple helper function for that: `runtime(string_view) -> runtime_string` The API is structured as follows: `scan(input, format_str, ...args) -> result` *Example* ```cpp int32_t i; uint32_t j; emio::result res = emio::scan("-1,2", "{},{}", i, j); assert(res); assert(i == -1); assert(j == 2); ``` - Scans the input string for the given arguments according to the format string. `scan_from(reader, format_str, ...args) -> result` - Scans the content of the reader for the given arguments according to the format string. *Example* ```cpp int32_t i; uint32_t j; emio::reader input{"-1,2..."}; emio::result res = emio::scan_from(input, "{},{}", i, j); assert(res); assert(i == -1); assert(j == 2); assert(input.view_remaining() == "..."); ``` For each function there exists a function prefixed with v (e.g. `vscan`) which takes `scan_args` instead of a format string and arguments. The types are erased and can be used in non-template functions to reduce build-time, hide implementations and reduce the binary size. **Note:** These type erased functions cannot be used at compile-time. `scan_args` can be created with: `make_scan_args(format_str, ...args) -> internal scan_args_storage` - Returns an object that stores a format string with an array of all arguments to scan. - Keep in mind that the storage uses reference semantics and does not extend the lifetime of args. It is the programmer's responsibility to ensure that args outlive the return value. ### Scanner There exists scanner for builtin types like char, string and integers. Support for other types (e.g. float) is planned. Use `is_scanner_v` to check if a type is scannable. A scanner exists of one optional function `validate` and two mandatory functions `parse` and `scan`. If `validate` is not present, `parse` must validate the format string. *Example* ```cpp struct foo { int x; }; template <> class emio::scanner { public: /** * Optional static function to validate the format string syntax for this type. * @note If not present, the parse function is invoked for validation. * @param format_rdr The reader over the format string. * @return Success if the format string is valid. */ static constexpr result validate(reader& format_rdr) noexcept { return format_rdr.read_if_match_char('}'); } /** * Function to parse the format specs for this type. * @param format_rdr The reader over the format string. * @return Success if the format string is valid and could be parsed. */ constexpr result parse(reader& format_rdr) noexcept { return format_rdr.read_if_match_char('}'); } /** * Function to scan the object of this type according to the parsed format specs. * @param input The input reader. * @param arg The argument to scan. * @return Success if the scanning could be done. */ constexpr result scan(reader& input, foo& arg) const noexcept { EMIO_TRYV(input.read_int(arg.x)); return success; } }; int main() { foo f{}; emio::scan("42", "{}", i); // f.x == 42 } ``` It is also possible to reuse existing scanner via inheritance or composition. *Example* ```cpp struct foo { int x; }; template <> class emio::scanner : public emio::scanner { public: constexpr result scan(reader& input, foo& arg) const noexcept { return emio::scanner::scan(input, arg.x); } }; int main() { foo f{}; emio::scan("0x2A", "{:x}", f); // f.x == 42 } ``` If the `validate` (or if absent the `parse`) function is not constexpr, a runtime strings must be used. The `scan` function don't need to be constexpr if the scanning shouldn't be done at compile-time. ================================================ FILE: docs/CODE_OF_CONDUCT.md ================================================ # Code of Conduct * You will be judged by your contributions first, and your sense of humor second. * Nobody owes you anything. ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributing ## Code of Conduct Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document. ## Getting started Helpful notes for developers can be found in the [`DEVELOPING.md`](DEVELOPING.md) document. ================================================ FILE: docs/DESIGN.md ================================================ # Design This is a short design overview of *emio*. **Note:** [fmtlib/fmt](https://github.com/fmtlib/fmt) is an awesome library with great concepts and implementations which are partly reused in this library. All statements made here by comparing *emio* with *fmt* are only objective and shouldn't insult anybody's work. ## Binary footprint The API provides a low level API for reading or writing builtin-types and also a high-level API for parsing (TODO) or formatting, which reuses the low-level API. Additional, even when using the high-level API, only used types increase the binary size. Furthermore, since the parse or format string can be validated at compile-time, the validation can be omitted at runtime. This is e.g. different in *fmt*, which doesn't provide a low-level API. Also, *fmt* instantiates all builtin types (e.g. bool, int64_t, int128_t, long double...) even if only int32_t should be formatted. Last but not least, the format string is validated at compile-time and runtime. Take the following code snippet as example which is compiled and statically linked. ```cpp std::string f = emio::format("{}", 1); ``` If compiled with *fmt*, **191 kBytes** of flash memory is required. If *emio* is used, only **5 kBytes** are requires. This is **38 times** less! Keep in mind that flash memory of many microcontrollers is between 128 kBytes and 2 MBytes. This huge advantage of *emio* comes with a price: *emio* doesn't support all features of *fmt*. But these features are likely not so important for embedded systems. Some missing features are: - no std::locale support (no internationalization) - if a runtime format string is used, validation and parsing happens sequential (performance overhead) - some features cannot be API compatible and have to be done differently (e.g. make_format_args requires the format string or dynamic width and precision is implemented by a wrapper object) ## Result type Instead of C++-exceptions, `emio:result` is used for returning and propagating errors. It is similar to `std::expected`, `boost::outcome` or Rust's `Result`. `emio::result` does either holds the expected value of type T or an unexpected error of type `emio::err`. Through observer methods, the state and the value or the error can be visited. ```cpp emio::result format(...) noexcept; emio::result get_first_digit_of_pi() noexcept { emio::result pi_as_str = format("{}", M_PI); if (pi_as_str) { return pi_as_str->at(0); } return emio::err::invalid_data; } ``` To reduce the if/else chain, emio provides two simple macros (similar to *boost::outcome*) `EMIO_TRY` and `EMIO_TRYV`. The above function could be rewritten into: ```cpp emio::result format(...) noexcept; emio::result get_first_digit_of_pi() noexcept { EMIO_TRY(std::string pi_as_str, format("{}", M_PI)); return pi_as_str.at(0); } ``` The biggest advantage of using a result type is that error handling is still possible even if C++-exceptions are disabled. Unlike other libraries like *fmt*, which mostly terminate deep inside their library if an error occurs, the result object propagates the error back to the callee. The drawback is of course the small performance overhead and the explicit handling of the control flow. ## Performance The current benchmarks show that the formatting is round about 1.5 - 2 times slower than fmtlib and the performance is similar to printf (e.g. for integer types). Scanning on the other hand is around twice as fast then scanf. See the benchmark tests inside the CI for more details. ## Class diagram ![class diagram](res/class_diagram.png) See also [API](API.md) notes. ================================================ FILE: docs/DEVELOPING.md ================================================ # Contributing Here is some wisdom to help you build and test this project as a developer and potential contributor. The project template is generated with [cmake-init](https://github.com/friendlyanon/cmake-init). If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md) guide. ## Install Only two commands are necessary to install this library on your system. ```sh cmake -S . -B build cmake --install build ``` ## Developer mode Build system targets that are only useful for developers of this project are hidden if the `emio_DEVELOPER_MODE` option is disabled. Enabling this option makes tests and other developer targets and options available. Not enabling this option means that you are a consumer of this project and thus you have no need for these targets and options. Developer mode is always set to on in CI workflows. ### Presets This project makes use of [presets][1] to simplify the process of configuring the project. As a developer, you are recommended to always have the [latest CMake version][2] installed to make use of the latest Quality-of-Life additions. You have a few options to pass `emio_DEVELOPER_MODE` to the configure command, but this project prefers to use presets. As a developer, you should create a `CMakeUserPresets.json` file at the root of the project: ```json { "version": 2, "cmakeMinimumRequired": { "major": 3, "minor": 14, "patch": 0 }, "configurePresets": [ { "name": "dev", "binaryDir": "${sourceDir}/build/dev", "inherits": ["dev-mode", "ci-"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } } ], "buildPresets": [ { "name": "dev", "configurePreset": "dev", "configuration": "Debug" } ], "testPresets": [ { "name": "dev", "configurePreset": "dev", "configuration": "Debug", "output": { "outputOnFailure": true } } ] } ``` You should replace `` in your newly created presets file with the name of the operating system you have, which may be `win64` or `unix`. You can see what these correspond to in the [`CMakePresets.json`](CMakePresets.json) file. `CMakeUserPresets.json` is also the perfect place in which you can put all sorts of things that you would otherwise want to pass to the configure command in the terminal. ### Configure, build and test If you followed the above instructions, then you can configure, build and test the project respectively with the following commands from the project root on any operating system with any build system: ```sh cmake --preset=dev cmake --build --preset=dev ctest --preset=dev ``` Please note that both the build and test command accepts a `-j` flag to specify the number of jobs to use, which should ideally be specified to the number of threads your CPU has. You may also want to add that to your preset using the `jobs` property, see the [presets documentation][1] for more details. [1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html [2]: https://cmake.org/download/ ================================================ FILE: docs/res/class_diagram.puml ================================================ @startuml class "scan functions" << (F,#FF7700) >> { scan(input, format_str, args...) -> result } class "format functions" << (F,#FF7700) >> { format(format_str, args...) -> result } class reader class "std::string_view" as sv reader *-- sv abstract class buffer class memory_buffer class static_buffer class span_buffer class iterator_buffer buffer <|-- iterator_buffer buffer <|-- static_buffer buffer <|-- span_buffer buffer <|-- memory_buffer class writer writer o-- buffer iterator_buffer o-- "output iterator" package "output iterator" { class truncating_iterator class "std::vector::iterator" class "const char *" } package "contiguous container" { class "std::array" class "std::string" as s2 class "std::vector" } span_buffer o-- "contiguous container" class format_string class formatter { validate(reader&) [0..1] parse(reader&) format(writer& out, T) } format_string -.> "validate & parse" formatter "format functions" *-.- "format_string" "format functions" -.-> "writer" : "write formatted\noutput" "format functions" -.-> "reader" : read format\nstring "format functions" -.-> "formatter" : parse,\nformat class format_scan_string class scanner { validate(reader&) [0..1] parse(reader&) scan(reader& in, T&) } format_scan_string -.> "validate & parse" scanner "scan functions" *-.- "format_scan_string" "scan functions" -.-> "reader" : read input & \nformat string "scan functions" -.-> "scanner" : parse,\nscan @enduml ================================================ FILE: include/emio/buffer.hpp ================================================ // // Copyright (c) 2021 - present, Toni Neubert // All rights reserved. // // For the license information refer to emio.hpp #pragma once #include #include #include #include #if __STDC_HOSTED__ # include #endif #include #include #include "detail/ct_vector.hpp" #include "result.hpp" namespace emio { /// The default cache size of buffers with an internal cache. inline constexpr size_t default_cache_size{128}; /** * This class provides the basic API and functionality for receiving a contiguous memory region of chars to write into. * @note Use a specific subclass for a concrete instantiation. */ class buffer { public: constexpr buffer(const buffer& other) = delete; constexpr buffer(buffer&& other) = delete; constexpr buffer& operator=(const buffer& other) = delete; constexpr buffer& operator=(buffer&& other) = delete; virtual constexpr ~buffer() noexcept = default; /** * Returns a write area with the requested size on success. * @param size The size the write area should have. * @return The write area with the requested size on success or eof if no write area is available. */ constexpr result> get_write_area_of(size_t size) noexcept { EMIO_TRY(const std::span area, get_write_area_of_max(size)); if (area.size() < size) { used_ -= area.size(); return err::eof; } return area; } /** * Returns a write area which may be smaller than the requested size. * @note This function should be used to support subclasses with a limited internal buffer. * E.g. Writing a long string in chunks. * @param size The size the write area should maximal have. * @return The write area with the requested size as maximum on success or eof if no write area is available. */ constexpr result> get_write_area_of_max(size_t size) noexcept { // If there is enough remaining capacity in the current write area, return it. // Otherwise, request a new write area from the concrete implementation. // There is a special case for fixed size buffers. Since they cannot grow, they simply return the // remaining capacity or EOF, if hitting zero capacity. const size_t remaining_capacity = area_.size() - used_; if (remaining_capacity >= size || fixed_size_ == fixed_size::yes) { if (remaining_capacity == 0 && size != 0) { return err::eof; } const size_t max_size = std::min(remaining_capacity, size); const std::span area = area_.subspan(used_, max_size); used_ += max_size; return area; } EMIO_TRY(const std::span area, request_write_area(used_, size)); used_ += area.size(); return area; } protected: /// Flag to indicate if the buffer's size is fixed and cannot grow. enum class fixed_size : bool { no, yes }; /** * Constructs the buffer. * @brief fixed Flag to indicate if the buffer's size is fixed and cannot grow. */ constexpr explicit buffer(fixed_size fixed = fixed_size::no) noexcept : fixed_size_{fixed} {} /** * Requests a write area of the given size from a subclass. * @param used Already written characters into the current write area. * @param size The requested size of a new write area. * @return The write area with the requested size as maximum on success or eof if no write area is available. */ virtual constexpr result> request_write_area(const size_t used, const size_t size) noexcept { static_cast(used); // Keep params for documentation. static_cast(size); return err::eof; } /** * Sets a new write area in the base class object to use. * @param area The new write area. */ constexpr void set_write_area(const std::span area) noexcept { area_ = area; used_ = 0; } /** * Returns the count of written characters of the current hold write area. * @return The count. */ [[nodiscard]] constexpr size_t get_used_count() const noexcept { return used_; } private: fixed_size fixed_size_{fixed_size::no}; size_t used_{}; std::span area_{}; }; /** * This class fulfills the buffer API by providing an endless growing buffer. * @tparam StorageSize The size of the internal storage used for small buffer optimization. */ template class memory_buffer final : public buffer { public: /** * Constructs and initializes the buffer with the internal storage size. */ constexpr memory_buffer() noexcept : memory_buffer{0} {} /** * Constructs and initializes the buffer with the given capacity. * @param capacity The initial capacity. */ constexpr explicit memory_buffer(const size_t capacity) noexcept { // Request at least the internal storage size. Should never fail. request_write_area(0, std::max(vec_.capacity(), capacity)).value(); } constexpr memory_buffer(const memory_buffer& other) : buffer{}, used_{other.used_ + other.get_used_count()}, vec_{other.vec_} { this->set_write_area({vec_.data() + used_, vec_.data() + vec_.capacity()}); } constexpr memory_buffer(memory_buffer&& other) noexcept : buffer{}, used_{other.used_ + other.get_used_count()}, vec_{std::move(other).vec_} { this->set_write_area({vec_.data() + used_, vec_.data() + vec_.capacity()}); other.reset(); } constexpr memory_buffer& operator=(const memory_buffer& other) { if (&other == this) { return *this; } used_ = other.used_ + other.get_used_count(); vec_ = other.vec_; this->set_write_area({vec_.data() + used_, vec_.data() + vec_.capacity()}); return *this; } constexpr memory_buffer& operator=(memory_buffer&& other) noexcept { if (&other == this) { return *this; } used_ = other.used_ + other.get_used_count(); vec_ = std::move(other).vec_; this->set_write_area({vec_.data() + used_, vec_.data() + vec_.capacity()}); other.reset(); return *this; } constexpr ~memory_buffer() override = default; /** * Obtains a view over the underlying string object. * @return The view. */ [[nodiscard]] constexpr std::string_view view() const noexcept { return {vec_.data(), used_ + this->get_used_count()}; } #if __STDC_HOSTED__ /** * Obtains a copy of the underlying string object. * @return The string. */ [[nodiscard]] std::string str() const { return std::string{view()}; } #endif /** * Resets the buffer's read and write position to the beginning of the internal storage. */ constexpr void reset() noexcept { used_ = 0; vec_.clear(); request_write_area(0, vec_.capacity()).value(); } /** * Returns the number of chars that the buffer has currently allocated space for. * @return The capacity. */ [[nodiscard]] constexpr size_t capacity() const noexcept { return vec_.capacity(); } protected: constexpr result> request_write_area(const size_t used, const size_t size) noexcept override { const size_t new_size = vec_.size() + size; vec_.reserve(new_size); used_ += used; const std::span area{vec_.data() + used_, size}; this->set_write_area(area); return area; } private: size_t used_{}; detail::ct_vector vec_{}; }; /** * This class fulfills the buffer API by using a span over an contiguous range. */ class span_buffer : public buffer { public: /** * Constructs and initializes the buffer with an empty span. */ constexpr span_buffer() : buffer{fixed_size::yes} {}; /** * Constructs and initializes the buffer with the given span. * @param span The span. */ constexpr explicit span_buffer(const std::span span) noexcept : buffer{fixed_size::yes}, span_{span} { this->set_write_area(span_); } constexpr span_buffer(const span_buffer& other) : buffer{fixed_size::yes}, span_{other.span_} { this->set_write_area(span_); get_write_area_of(other.get_used_count()).value(); } // NOLINTNEXTLINE(performance-move-constructor-init): optimized move not possible constexpr span_buffer(span_buffer&& other) noexcept : span_buffer{std::as_const(other)} {} constexpr span_buffer& operator=(const span_buffer& other) { if (&other == this) { return *this; } span_ = other.span_; this->set_write_area(span_); get_write_area_of(other.get_used_count()).value(); return *this; } constexpr span_buffer& operator=(span_buffer&& other) noexcept { *this = std::as_const(other); return *this; } constexpr ~span_buffer() override; /** * Obtains a view over the underlying string object. * @return The view. */ [[nodiscard]] constexpr std::string_view view() const noexcept { return {span_.data(), this->get_used_count()}; } #if __STDC_HOSTED__ /** * Obtains a copy of the underlying string object. * @return The string. */ [[nodiscard]] std::string str() const { return std::string{view()}; } #endif /** * Resets the buffer's read and write position to the beginning of the span. */ constexpr void reset() noexcept { this->set_write_area(span_); } /** * Returns the number of chars that the buffer has space for. * @return The capacity. */ [[nodiscard]] constexpr size_t capacity() const noexcept { return span_.size(); } private: std::span span_; }; // Out-of-line definition because of a GCC bug (93413). Fixed in GCC 13. inline constexpr span_buffer::~span_buffer() = default; /** * This class fulfills the buffer API by providing a fixed-size storage. * @tparam StorageSize The size of the storage. */ template class static_buffer final : private std::array, public span_buffer { public: /** * Constructs and initializes the buffer with the storage. */ constexpr static_buffer() noexcept : span_buffer{std::span{*this}} {} constexpr static_buffer(const static_buffer& other) : static_buffer() { const std::span area = get_write_area_of(other.get_used_count()).value(); detail::copy_n(&*other.begin(), area.size(), area.data()); } // NOLINTNEXTLINE(performance-move-constructor-init): optimized move not possible constexpr static_buffer(static_buffer&& other) noexcept : static_buffer(std::as_const(other)) {} constexpr static_buffer& operator=(const static_buffer& other) { if (&other == this) { return *this; } set_write_area(std::span{*this}); const std::span area = get_write_area_of(other.get_used_count()).value(); detail::copy_n(&*other.begin(), area.size(), area.data()); return *this; } constexpr static_buffer& operator=(static_buffer&& other) noexcept { *this = std::as_const(other); return *this; } constexpr ~static_buffer() override = default; // Note: We inherit from std::array to put the storage lifetime before span_buffer. // Clang will otherwise complain if the storage is a member variable and used during compile-time. }; namespace detail { // Extracts a reference to the container from back_insert_iterator. template Container& get_container(std::back_insert_iterator it) noexcept { using bi_iterator = std::back_insert_iterator; struct accessor : bi_iterator { accessor(bi_iterator iter) : bi_iterator(iter) {} using bi_iterator::container; }; return *accessor{it}.container; } // Helper struct to get the value type of different iterators. template struct get_value_type { using type = typename std::iterator_traits::value_type; }; template struct get_value_type> { using type = typename Container::value_type; }; #if __STDC_HOSTED__ template struct get_value_type> { using type = Char; }; #endif template using get_value_type_t = typename get_value_type::type; template constexpr auto copy_str(InputIt it, InputIt end, OutputIt out) -> OutputIt { while (it != end) { *out++ = static_cast(*it++); } return out; } } // namespace detail /** * This class template is used to create a buffer around different iterator types. */ template class iterator_buffer; /** * This class fulfills the buffer API by using an output iterator and an internal cache. * @tparam Iterator The output iterator type. * @tparam CacheSize The size of the internal cache. */ template requires(std::input_or_output_iterator && std::output_iterator>) class iterator_buffer final : public buffer { public: /** * Constructs and initializes the buffer with the given output iterator. * @param it The output iterator. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): cache_ can be left uninitialized constexpr explicit iterator_buffer(Iterator it) noexcept : it_{it} { this->set_write_area(cache_); } iterator_buffer(const iterator_buffer&) = delete; iterator_buffer(iterator_buffer&&) = delete; iterator_buffer& operator=(const iterator_buffer&) = delete; iterator_buffer& operator=(iterator_buffer&&) = delete; ~iterator_buffer() override = default; /** * Flushes the internal cache to the output iterator. * @return Always succeeds. */ constexpr result flush() noexcept { it_ = detail::copy_str(cache_.data(), cache_.data() + this->get_used_count(), it_); this->set_write_area(cache_); return success; } /** * Flushes and returns the output iterator at the next write position. * @return The output iterator. */ constexpr Iterator out() noexcept { flush().assume_value(); // Will never fail. return it_; } protected: constexpr result> request_write_area(const size_t /*used*/, const size_t size) noexcept override { flush().assume_value(); // Will never fail. const std::span area{cache_}; this->set_write_area(area); if (size > cache_.size()) { return area; } return area.subspan(0, size); } private: Iterator it_; std::array cache_; }; /** * This class fulfills the buffer API by using an output pointer. * @tparam Iterator The output iterator type. */ template requires(std::input_or_output_iterator && std::output_iterator>) class iterator_buffer final : public buffer { public: /** * Constructs and initializes the buffer with the given output pointer. * @param ptr The output pointer. */ constexpr explicit iterator_buffer(OutputPtr* ptr) noexcept : ptr_{ptr} { this->set_write_area({ptr, std::numeric_limits::max()}); } iterator_buffer(const iterator_buffer&) = delete; iterator_buffer(iterator_buffer&&) = delete; iterator_buffer& operator=(const iterator_buffer&) = delete; iterator_buffer& operator=(iterator_buffer&&) = delete; ~iterator_buffer() override = default; /** * Does nothing. Kept for uniformity with other iterator_buffer implementations. * @return Always succeeds. */ constexpr result flush() noexcept { // Nothing. return success; } /** * Returns the output pointer at the next write position. * @return The output pointer. */ constexpr OutputPtr* out() noexcept { return ptr_ + this->get_used_count(); } private: OutputPtr* ptr_; }; /** * This class fulfills the buffer API by using the container of an contiguous back-insert iterator. * @tparam Container The container type of the back-insert iterator. * @tparam Capacity The minimum initial requested capacity of the container. */ template requires std::contiguous_iterator class iterator_buffer, Capacity> final : public buffer { public: /** * Constructs and initializes the buffer with the given back-insert iterator. * @param it The back-insert iterator. */ constexpr explicit iterator_buffer(std::back_insert_iterator it) noexcept : container_{detail::get_container(it)} { request_write_area(0, std::min(container_.capacity(), Capacity)).value(); } iterator_buffer(const iterator_buffer&) = delete; iterator_buffer(iterator_buffer&&) = delete; iterator_buffer& operator=(const iterator_buffer&) = delete; iterator_buffer& operator=(iterator_buffer&&) = delete; ~iterator_buffer() override = default; /** * Flushes the back-insert iterator by adjusting the size. * @return Always succeeds. */ constexpr result flush() noexcept { container_.resize(used_ + this->get_used_count()); return success; } /** * Flushes and returns the back-insert iterator. * @return The back-insert iterator. */ constexpr std::back_insert_iterator out() noexcept { flush().assume_value(); // Will never fail. return std::back_inserter(container_); } protected: constexpr result> request_write_area(const size_t used, const size_t size) noexcept override { const size_t new_size = container_.size() + size; container_.resize(new_size); used_ += used; const std::span area{container_.data() + used_, new_size}; this->set_write_area(area); return area.subspan(0, size); } private: size_t used_{}; Container& container_; }; template iterator_buffer(Iterator&&) -> iterator_buffer>; /** * This class fulfills the buffer API by using a file stream and an internal cache. * @tparam CacheSize The size of the internal cache. */ template class file_buffer final : public buffer { public: /** * Constructs and initializes the buffer with the given file stream. * @param file The file stream. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): cache_ can be left uninitialized constexpr explicit file_buffer(std::FILE* file) noexcept : file_{file} { this->set_write_area(cache_); } file_buffer(const file_buffer&) = delete; file_buffer(file_buffer&&) = delete; file_buffer& operator=(const file_buffer&) = delete; file_buffer& operator=(file_buffer&&) = delete; ~file_buffer() override = default; /** * Flushes the internal cache to the file stream. * @note Does not flush the file stream itself! */ result flush() noexcept { const size_t written = std::fwrite(cache_.data(), sizeof(char), this->get_used_count(), file_); if (written != this->get_used_count()) { return err::eof; } this->set_write_area(cache_); return success; } /** * Resets the buffer's read and write position to the beginning of the file stream. */ constexpr void reset() noexcept { this->set_write_area(cache_); std::fseek(file_, 0, SEEK_SET); } protected: result> request_write_area(const size_t /*used*/, const size_t size) noexcept override { EMIO_TRYV(flush()); const std::span area{cache_}; this->set_write_area(area); if (size > cache_.size()) { return area; } return area.subspan(0, size); } private: std::FILE* file_; std::array cache_; }; /** * This class fulfills the buffer API by using a primary buffer and an internal cache. * Only a limited amount of characters is written to the primary buffer. The remaining characters are truncated. * @tparam CacheSize The size of the internal cache. */ template class truncating_buffer final : public buffer { public: /** * Constructs and initializes the buffer with the given primary buffer and limit. * @param primary The primary buffer. * @param limit The limit. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): cache_ can be left uninitialized. constexpr explicit truncating_buffer(buffer& primary, size_t limit) : primary_{primary}, limit_{limit} { this->set_write_area(cache_); } truncating_buffer(const truncating_buffer&) = delete; truncating_buffer(truncating_buffer&&) = delete; truncating_buffer& operator=(const truncating_buffer&) = delete; truncating_buffer& operator=(truncating_buffer&&) = delete; constexpr ~truncating_buffer() noexcept override = default; /** * Returns the count of the total (not truncated) written characters. * @return The count. */ [[nodiscard]] constexpr size_t count() const noexcept { return used_ + this->get_used_count(); } /** * Flushes the internal cache to the primary buffer. */ [[nodiscard]] constexpr result flush() noexcept { size_t bytes_to_write = get_used_count(); used_ += bytes_to_write; while (written_ < limit_ && bytes_to_write > 0) { EMIO_TRY(const auto area, primary_.get_write_area_of_max(std::min(bytes_to_write, limit_ - written_))); detail::copy_n(detail::begin(cache_), area.size(), area.data()); written_ += area.size(); bytes_to_write -= area.size(); } this->set_write_area(cache_); return success; } protected: constexpr result> request_write_area(const size_t /*used*/, const size_t size) noexcept override { EMIO_TRYV(flush()); const std::span area{cache_}; this->set_write_area(area); if (size > cache_.size()) { return area; } return area.subspan(0, size); } private: buffer& primary_; size_t limit_; size_t written_{}; size_t used_{}; std::array cache_; }; namespace detail { /** * A buffer that counts the number of characters written. Discards the output. * @tparam CacheSize The size of the internal cache. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): cache_ can be left uninitialized. template class counting_buffer final : public buffer { public: // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): cache_ can be left uninitialized. constexpr counting_buffer() noexcept = default; constexpr counting_buffer(const counting_buffer&) = delete; constexpr counting_buffer(counting_buffer&&) noexcept = delete; constexpr counting_buffer& operator=(const counting_buffer&) = delete; constexpr counting_buffer& operator=(counting_buffer&&) noexcept = delete; constexpr ~counting_buffer() noexcept override = default; /** * Calculates the number of Char's that were written. * @return The number of Char's. */ [[nodiscard]] constexpr size_t count() const noexcept { return used_ + this->get_used_count(); } protected: constexpr result> request_write_area(const size_t used, const size_t size) noexcept override { used_ += used; const std::span area{cache_}; this->set_write_area(area); if (size > cache_.size()) { return area; } return area.subspan(0, size); } private: size_t used_{}; std::array cache_; }; } // namespace detail } // namespace emio ================================================ FILE: include/emio/detail/args.hpp ================================================ // // Copyright (c) 2023 - present, Toni Neubert // All rights reserved. // // For the license information refer to emio.hpp #pragma once #include #include #include #include "validated_string_storage.hpp" namespace emio::detail { /** * Type erased argument to validate. */ template