diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45e9356..49811f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,14 +34,15 @@ jobs: - uses: actions/checkout@v4 - name: dependencies run: | - pip3 install colorama + python3 -m venv env + env/bin/pip install colorama - name: libdwarf run: | cd .. cpptrace/ci/setup-prerequisites.sh - name: build run: | - python3 ci/build-in-all-configs.py --${{matrix.compiler}} --default-config + env/bin/python ci/build-in-all-configs.py --${{matrix.compiler}} --default-config build-windows: runs-on: windows-2022 strategy: @@ -95,14 +96,15 @@ jobs: - uses: actions/checkout@v4 - name: dependencies run: | - pip3 install colorama + python3 -m venv env + env/bin/pip install colorama - name: libdwarf run: | cd .. cpptrace/ci/setup-prerequisites.sh - name: build run: | - python3 ci/build-in-all-configs.py --${{matrix.compiler}} + env/bin/python ci/build-in-all-configs.py --${{matrix.compiler}} build-windows-all-configurations: runs-on: windows-2022 needs: build-windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f81803..cc8da81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,10 +42,11 @@ jobs: cpptrace/ci/setup-prerequisites.sh - name: dependencies run: | - pip3 install colorama + python3 -m venv env + env/bin/pip install colorama - name: build and test run: | - python3 ci/test-all-configs.py --${{matrix.compiler}} --default-config + env/bin/python ci/test-all-configs.py --${{matrix.compiler}} --default-config test-windows: runs-on: windows-2022 strategy: @@ -106,10 +107,11 @@ jobs: cpptrace/ci/setup-prerequisites.sh - name: dependencies run: | - pip3 install colorama + python3 -m venv env + env/bin/pip install colorama - name: build and test run: | - python3 ci/test-all-configs.py --${{matrix.compiler}} + env/bin/python ci/test-all-configs.py --${{matrix.compiler}} test-windows-all-configurations: runs-on: windows-2022 strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index d12247d..79fcfde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) +- [v0.5.0](#v050) - [v0.4.1](#v041) - [v0.4.0](#v040) - [v0.3.1](#v031) @@ -10,6 +11,30 @@ - [v0.1.1](#v011) - [v0.1](#v01) +# v0.5.1 + +Fixes: +- Fix MSVC warning treated as error for 32-bit windows +- Fix MSVC issue with min/max macros +- Fix potential null dereference issue identified by eyalgolan1337 + +# v0.5.0 + +New: +- Traces with source code snippets with `cpptrace::stacktrace::print_with_snippets` +- Added `cpptrace::get_snippet` utility +- Added `cpptrace::can_signal_safe_unwind` utility +- Added `stacktrace_frame::get_object_info` + +Changes: +- The library is now compiled with position-independent code by default + +Fixes: +- Fixed issue with `_dl_find_object` implementation + +Misc: +- Various refactoring, cleanup, and improvements + # v0.4.1 Changes: diff --git a/CMakeLists.txt b/CMakeLists.txt index 851c1e9..e251dc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ set(package_name "cpptrace") project( cpptrace - VERSION 0.4.1 + VERSION 0.5.1 DESCRIPTION "Simple, portable, and self-contained stacktrace library for C++11 and newer " HOMEPAGE_URL "https://github.com/jeremy-rifkin/cpptrace" LANGUAGES C CXX @@ -288,6 +288,8 @@ target_compile_features( PRIVATE cxx_std_11 ) +target_compile_definitions(${target_name} PRIVATE NOMINMAX) + if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") SET(CMAKE_C_ARCHIVE_FINISH " -no_warning_for_no_symbols -c ") SET(CMAKE_CXX_ARCHIVE_FINISH " -no_warning_for_no_symbols -c ") diff --git a/README.md b/README.md index ccfb0ed..2efa46b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
[![Community Discord Link](https://img.shields.io/badge/Chat%20on%20the%20(very%20small)-Community%20Discord-blue?labelColor=2C3239&color=7289DA&style=flat&logo=discord&logoColor=959DA5)](https://discord.gg/frjaAZvqUZ)
-[![Try on Compiler Explorer](https://img.shields.io/badge/-Compiler%20Explorer-brightgreen?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAACXBIWXMAAACwAAAAsAEUaqtpAAABSElEQVQokYVTsU7DMBB9QMTCEJbOMLB5oF0tRfUPIPIJZctYJkZYu3WMxNL+ARUfQKpImcPgDYnsXWBgYQl61TkYyxI3Wef37j3fnQ/6vkcsikY9AbiWq0mpbevDBmLRqDEAA4CEHMADgFRwrwDmch6X2i73RCFVHvC/WCeCMAFpC2AFoPPu5x4md4rnAN4luS61nYWSgauNU8ydkr0bLTMYAoIYtWqxM4LtEumeERDtfUjlMDrp7L67iddyyJtOvUIu2rquVn4iiVSOKXYhiMSJWLwUJZLuQ2CWmVldV4MT11UmXgB8fr0dX3WP6VHMiVrscim6Da2mJxffzwSU2v6xWzSKmzQ4cUTOaCBTvWgU14xkzjhckKm/q3wnrRAcAhksxMZNAdxEf0fRKI6E8zqT1C0X28ccRpqAUltW5pu4sxv5Mb8B4AciE3bHMxz/+gAAAABJRU5ErkJggg==&labelColor=2C3239&style=flat&label=Try+it+on&color=30C452)](https://godbolt.org/z/5sEszzEPE) +[![Try on Compiler Explorer](https://img.shields.io/badge/-Compiler%20Explorer-brightgreen?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAACXBIWXMAAACwAAAAsAEUaqtpAAABSElEQVQokYVTsU7DMBB9QMTCEJbOMLB5oF0tRfUPIPIJZctYJkZYu3WMxNL+ARUfQKpImcPgDYnsXWBgYQl61TkYyxI3Wef37j3fnQ/6vkcsikY9AbiWq0mpbevDBmLRqDEAA4CEHMADgFRwrwDmch6X2i73RCFVHvC/WCeCMAFpC2AFoPPu5x4md4rnAN4luS61nYWSgauNU8ydkr0bLTMYAoIYtWqxM4LtEumeERDtfUjlMDrp7L67iddyyJtOvUIu2rquVn4iiVSOKXYhiMSJWLwUJZLuQ2CWmVldV4MT11UmXgB8fr0dX3WP6VHMiVrscim6Da2mJxffzwSU2v6xWzSKmzQ4cUTOaCBTvWgU14xkzjhckKm/q3wnrRAcAhksxMZNAdxEf0fRKI6E8zqT1C0X28ccRpqAUltW5pu4sxv5Mb8B4AciE3bHMxz/+gAAAABJRU5ErkJggg==&labelColor=2C3239&style=flat&label=Try+it+on&color=30C452)](https://godbolt.org/z/c6TqTzqcf) Cpptrace is a simple, portable, and self-contained C++ stacktrace library supporting C++11 and greater on Linux, macOS, and Windows including MinGW and Cygwin environments. The goal: Make stack traces simple for once. @@ -93,6 +93,9 @@ Additional notable features: - Utilities for demangling - Utilities for catching `std::exception`s and wrapping them in traced exceptions - Signal-safe stack tracing +- Source code snippets in traces + +![Snippets](res/snippets.png) ## CMake FetchContent Usage @@ -101,7 +104,7 @@ include(FetchContent) FetchContent_Declare( cpptrace GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git - GIT_TAG v0.4.1 # + GIT_TAG v0.5.1 # ) FetchContent_MakeAvailable(cpptrace) target_link_libraries(your_target cpptrace::cpptrace) @@ -139,14 +142,18 @@ thrown, and providing an API for safe tracing from signal handlers. # In-Depth Documentation +## Prerequisites + +> [!IMPORTANT] +> Debug info (`-g`/`/Z7`/`/Zi`/`/DEBUG`/`-DBUILD_TYPE=Debug`/`-DBUILD_TYPE=RelWithDebInfo`) is required for complete +> trace information. + ## `namespace cpptrace` `cpptrace::generate_trace()` can be used to generate a stacktrace object at the current call site. Resolved frames can be accessed from this object with `.frames` and also the trace can be printed with `.print()`. Cpptrace also provides a method to get lightweight raw traces, which are just vectors of program counters, which can be resolved at a later time. -**Note:** Debug info (`-g`/`/Z7`/`/Zi`/`/DEBUG`) is generally required for good trace information. - All functions are thread-safe unless otherwise noted. ### Stack Traces @@ -155,6 +162,9 @@ The core resolved stack trace object. Generate a trace with `cpptrace::generate_ `cpptrace::stacktrace::current()`. On top of a set of helper functions `struct stacktrace` allows direct access to frames as well as iterators. +`cpptrace::stacktrace::print` can be used to print a stacktrace. `cpptrace::stacktrace::print_with_snippets` can be used +to print a stack trace with source code snippets. + ```cpp namespace cpptrace { // Some type sufficient for an instruction pointer, currently always an alias to std::uintptr_t @@ -184,6 +194,9 @@ namespace cpptrace { void print() const; void print(std::ostream& stream) const; void print(std::ostream& stream, bool color) const; + void print_with_snippets() const; + void print_with_snippets(std::ostream& stream) const; + void print_with_snippets(std::ostream& stream, bool color) const; std::string to_string(bool color = false) const; void clear(); bool empty() const noexcept; @@ -554,7 +567,7 @@ include(FetchContent) FetchContent_Declare( cpptrace GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git - GIT_TAG v0.4.1 # + GIT_TAG v0.5.1 # ) FetchContent_MakeAvailable(cpptrace) target_link_libraries(your_target cpptrace::cpptrace) @@ -570,7 +583,7 @@ information. ```sh git clone https://github.com/jeremy-rifkin/cpptrace.git -git checkout v0.4.1 +git checkout v0.5.1 mkdir cpptrace/build cd cpptrace/build cmake .. -DCMAKE_BUILD_TYPE=Release @@ -606,7 +619,7 @@ you when installing new libraries. ```ps1 git clone https://github.com/jeremy-rifkin/cpptrace.git -git checkout v0.4.1 +git checkout v0.5.1 mkdir cpptrace/build cd cpptrace/build cmake .. -DCMAKE_BUILD_TYPE=Release @@ -624,7 +637,7 @@ To install just for the local user (or any custom prefix): ```sh git clone https://github.com/jeremy-rifkin/cpptrace.git -git checkout v0.4.1 +git checkout v0.5.1 mkdir cpptrace/build cd cpptrace/build cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/wherever @@ -661,7 +674,8 @@ The typical dependencies for cpptrace are: Note: Newer libdwarf requires `-lzstd`, older libdwarf does not. -If you are linking statically, you will additionally need to specify `-DCPPTRACE_STATIC_DEFINE`. +> [!IMPORTANT] +> If you are linking statically, you will additionally need to specify `-DCPPTRACE_STATIC_DEFINE`. Dependencies may differ if different back-ends are manually selected. @@ -703,7 +717,7 @@ make install cd ~/scratch/cpptrace-test git clone https://github.com/jeremy-rifkin/cpptrace.git cd cpptrace -git checkout v0.4.1 +git checkout v0.5.1 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=On -DCPPTRACE_USE_EXTERNAL_LIBDWARF=On -DCMAKE_PREFIX_PATH=~/scratch/cpptrace-test/resources -DCMAKE_INSTALL_PREFIX=~/scratch/cpptrace-test/resources @@ -723,7 +737,7 @@ cpptrace and its dependencies. Cpptrace is available through conan at https://conan.io/center/recipes/cpptrace. ``` [requires] -cpptrace/0.4.1 +cpptrace/0.5.1 [generators] CMakeDeps CMakeToolchain diff --git a/include/cpptrace/cpptrace.hpp b/include/cpptrace/cpptrace.hpp index 83cf3e4..e47e785 100644 --- a/include/cpptrace/cpptrace.hpp +++ b/include/cpptrace/cpptrace.hpp @@ -118,7 +118,7 @@ namespace cpptrace { return *this; } bool has_value() const noexcept { - return raw_value != std::numeric_limits::max(); + return raw_value != (std::numeric_limits::max)(); } T& value() noexcept { return raw_value; @@ -133,7 +133,7 @@ namespace cpptrace { std::swap(raw_value, other.raw_value); } void reset() noexcept { - raw_value = std::numeric_limits::max(); + raw_value = (std::numeric_limits::max)(); } bool operator==(const nullable& other) const noexcept { return raw_value == other.raw_value; @@ -142,7 +142,7 @@ namespace cpptrace { return raw_value != other.raw_value; } constexpr static nullable null() noexcept { - return { std::numeric_limits::max() }; + return { (std::numeric_limits::max)() }; } }; diff --git a/res/snippets.png b/res/snippets.png new file mode 100644 index 0000000..04f6b00 Binary files /dev/null and b/res/snippets.png differ diff --git a/src/binary/object.hpp b/src/binary/object.hpp index 6da981c..1b2a3b2 100644 --- a/src/binary/object.hpp +++ b/src/binary/object.hpp @@ -44,14 +44,7 @@ namespace detail { frame.object_path = buffer; } } - auto base = get_module_image_base(frame.object_path); - if(base.has_value()) { - frame.object_address = address - - to_frame_ptr(result.dlfo_link_map->l_addr) - + base.unwrap_value(); - } else { - base.drop_error(); - } + frame.object_address = address - to_frame_ptr(result.dlfo_link_map->l_addr); } return frame; } diff --git a/src/snippets/snippet.cpp b/src/snippets/snippet.cpp index d9ec4dc..660d375 100644 --- a/src/snippets/snippet.cpp +++ b/src/snippets/snippet.cpp @@ -9,6 +9,7 @@ #include #include "../utils/common.hpp" +#include "../utils/utils.hpp" namespace cpptrace { namespace detail { @@ -36,7 +37,7 @@ namespace detail { } // else load file file.seekg(0, std::ios::beg); - contents.resize(size); + contents.resize(to(size)); if(!file.read(&contents[0], size)) { // error ... } diff --git a/src/symbols/symbols_with_dbghelp.cpp b/src/symbols/symbols_with_dbghelp.cpp index ee4881b..2277002 100644 --- a/src/symbols/symbols_with_dbghelp.cpp +++ b/src/symbols/symbols_with_dbghelp.cpp @@ -325,6 +325,13 @@ namespace dbghelp { // TODO: Handle backtrace_pcinfo calling the callback multiple times on inlined functions stacktrace_frame resolve_frame(HANDLE proc, frame_ptr addr) { + // The get_frame_object_info() ends up being inexpensive, at on my machine + // debug release + // uncached trace resolution (29 frames) 1.9-2.1 ms 1.4-1.8 ms + // cached trace resolution (29 frames) 1.1-1.2 ms 0.2-0.4 ms + // get_frame_object_info() 0.001-0.002 ms 0.0003-0.0006 ms + // At some point it might make sense to make an option to control this. + auto object_frame = get_frame_object_info(addr); const std::lock_guard lock(dbghelp_lock); // all dbghelp functions are not thread safe alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; SYMBOL_INFO* symbol = (SYMBOL_INFO*)buffer; @@ -345,7 +352,7 @@ namespace dbghelp { std::fprintf(stderr, "Stack trace: Internal error while calling SymSetContext\n"); return { addr, - 0, + object_frame.object_address, { static_cast(line.LineNumber) }, nullable::null(), line.FileName, @@ -378,7 +385,7 @@ namespace dbghelp { signature = std::regex_replace(signature, comma_re, ", "); return { addr, - 0, + object_frame.object_address, { static_cast(line.LineNumber) }, nullable::null(), line.FileName, @@ -388,7 +395,7 @@ namespace dbghelp { } else { return { addr, - 0, + object_frame.object_address, nullable::null(), nullable::null(), "", @@ -397,7 +404,15 @@ namespace dbghelp { }; } } else { - return { addr, 0, nullable::null(), nullable::null(), "", "", false }; + return { + addr, + object_frame.object_address, + nullable::null(), + nullable::null(), + "", + "", + false + }; } } diff --git a/src/symbols/symbols_with_libbacktrace.cpp b/src/symbols/symbols_with_libbacktrace.cpp index 02f6ded..baeb79c 100644 --- a/src/symbols/symbols_with_libbacktrace.cpp +++ b/src/symbols/symbols_with_libbacktrace.cpp @@ -38,7 +38,7 @@ namespace libbacktrace { } void error_callback(void*, const char* msg, int errnum) { - throw internal_error(stringf("Libbacktrace error: %s, code %d\n", msg, errnum)); + throw internal_error(microfmt::format("Libbacktrace error: {}, code {}\n", msg, errnum)); } backtrace_state* get_backtrace_state() { diff --git a/src/utils/common.hpp b/src/utils/common.hpp index c353e86..0c640d5 100644 --- a/src/utils/common.hpp +++ b/src/utils/common.hpp @@ -60,18 +60,6 @@ namespace cpptrace { namespace detail { - // Placed here instead of utils because it's used by error.hpp and utils.hpp - template std::string stringf(T... args) { - int length = std::snprintf(nullptr, 0, args...); - if(length < 0) { - throw std::logic_error("invalid arguments to stringf"); - } - std::string str(length, 0); - // .data is const char* in c++11, but &str[0] should be legal - std::snprintf(&str[0], length + 1, args...); - return str; - } - static const stacktrace_frame null_frame { 0, 0, diff --git a/src/utils/dbghelp_syminit_manager.hpp b/src/utils/dbghelp_syminit_manager.hpp index b7053fb..a906703 100644 --- a/src/utils/dbghelp_syminit_manager.hpp +++ b/src/utils/dbghelp_syminit_manager.hpp @@ -17,7 +17,7 @@ namespace detail { ~dbghelp_syminit_manager() { for(auto handle : set) { if(!SymCleanup(handle)) { - ASSERT(false, stringf("Cpptrace SymCleanup failed with code %llu\n", to_ull(GetLastError()))); + ASSERT(false, microfmt::format("Cpptrace SymCleanup failed with code {}\n", GetLastError())); } } } @@ -25,7 +25,7 @@ namespace detail { void init(HANDLE proc) { if(set.count(proc) == 0) { if(!SymInitialize(proc, NULL, TRUE)) { - throw internal_error(stringf("SymInitialize failed %llu", to_ull(GetLastError()))); + throw internal_error(microfmt::format("SymInitialize failed {}", GetLastError())); } set.insert(proc); } diff --git a/src/utils/dwarf.hpp b/src/utils/dwarf.hpp index 8a82cbe..d6b6a20 100644 --- a/src/utils/dwarf.hpp +++ b/src/utils/dwarf.hpp @@ -28,7 +28,7 @@ namespace libdwarf { char* msg = dwarf_errmsg(error); (void)dbg; // dwarf_dealloc_error(dbg, error); - throw internal_error(stringf("Cpptrace dwarf error %u %s\n", ev, msg)); + throw internal_error(microfmt::format("Cpptrace dwarf error {} {}\n", ev, msg)); } struct die_object { @@ -235,7 +235,7 @@ namespace libdwarf { return die_object(dbg, target); } default: - PANIC(stringf("unknown form for attribute %d %d\n", attr_num, form)); + PANIC(microfmt::format("unknown form for attribute {} {}\n", attr_num, form)); } } @@ -343,7 +343,7 @@ namespace libdwarf { return; } Dwarf_Addr baseaddr = 0; - if(lowpc != std::numeric_limits::max()) { + if(lowpc != (std::numeric_limits::max)()) { baseaddr = lowpc; } Dwarf_Ranges* ranges = nullptr; @@ -381,7 +381,7 @@ namespace libdwarf { template // callback should return true to keep going void dwarf_ranges(int version, optional pc, F callback) const { - Dwarf_Addr lowpc = std::numeric_limits::max(); + Dwarf_Addr lowpc = (std::numeric_limits::max)(); if(wrap(dwarf_lowpc, die, &lowpc) == DW_DLV_OK) { if(pc.has_value() && pc.unwrap() == lowpc) { callback(lowpc, lowpc + 1); diff --git a/src/utils/error.hpp b/src/utils/error.hpp index a8fd46b..2cb6345 100644 --- a/src/utils/error.hpp +++ b/src/utils/error.hpp @@ -7,6 +7,7 @@ #include #include "common.hpp" +#include "microfmt.hpp" #if IS_MSVC #define CPPTRACE_PFUNC __FUNCSIG__ @@ -62,8 +63,8 @@ namespace detail { const char* name = assert_names[static_cast::type>(type)]; if(message == "") { throw internal_error( - stringf( - "Cpptrace %s failed at %s:%d: %s\n" + microfmt::format( + "Cpptrace {} failed at {}:{}: {}\n" " %s(%s);\n", action, location.file, location.line, signature, name, expression @@ -71,8 +72,8 @@ namespace detail { ); } else { throw internal_error( - stringf( - "Cpptrace %s failed at %s:%d: %s: %s\n" + microfmt::format( + "Cpptrace {} failed at {}:{}: {}: {}\n" " %s(%s);\n", action, location.file, location.line, signature, message.c_str(), name, expression @@ -88,15 +89,15 @@ namespace detail { ) { if(message == "") { throw internal_error( - stringf( - "Cpptrace panic %s:%d: %s\n", + microfmt::format( + "Cpptrace panic {}:{}: {}\n", location.file, location.line, signature ) ); } else { throw internal_error( - stringf( - "Cpptrace panic %s:%d: %s: %s\n", + microfmt::format( + "Cpptrace panic {}:{}: {}: {}\n", location.file, location.line, signature, message.c_str() ) ); diff --git a/src/utils/microfmt.hpp b/src/utils/microfmt.hpp new file mode 100644 index 0000000..b9d7f4c --- /dev/null +++ b/src/utils/microfmt.hpp @@ -0,0 +1,325 @@ +#ifndef MICROFMT_HPP +#define MICROFMT_HPP + +// Copyright (c) 2024 Jeremy Rifkin; MIT License + +#include +#include +#include +#include +#include +#include +#if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L +#include +#endif +#ifdef _MSC_VER +#include +#endif + +// {[[align][width]:[fill][base]]} +// width: number or {} + +namespace microfmt { + namespace detail { + #define STR2(x) #x + #define STR(x) STR2(x) + #define MICROFMT_ASSERT(expr) if(!(expr)) { \ + throw std::runtime_error("Microfmt check failed" __FILE__ ":" STR(__LINE__) ": " #expr); \ + } + + #ifdef _MSC_VER + inline std::uint64_t clz(std::uint64_t value) { + unsigned long out = 0; + #ifdef _WIN64 + _BitScanForward64(&out, value); + #else + if(_BitScanForward(&out, std::uint32_t(value >> 32))) { + return 63 ^ int(out + 32); + } + _BitScanForward(&out, std::uint32_t(value)); + #endif + return out; + } + #else + inline std::uint64_t clz(std::uint64_t value) { + return __builtin_clzll(value); + } + #endif + + enum class alignment { left, right }; + + struct format_options { + alignment align = alignment::left; + char fill = ' '; + size_t width = 0; + char base = 'd'; + }; + + template void do_write(std::string& out, It begin, It end, const format_options& options) { + auto size = end - begin; + MICROFMT_ASSERT(size >= 0); + if(static_cast(size) >= options.width) { + out.append(begin, end); + } else { + auto out_size = out.size(); + out.resize(out_size + options.width); + if(options.align == alignment::left) { + std::copy(begin, end, out.begin() + out_size); + std::fill(out.begin() + out_size + size, out.end(), options.fill); + } else { + std::fill(out.begin() + out_size, out.begin() + out_size + (options.width - size), options.fill); + std::copy(begin, end, out.begin() + out_size + (options.width - size)); + } + } + } + + template + std::string to_string(std::uint64_t value, const char* digits = "0123456789abcdef") { + if(value == 0) { + return "0"; + } else { + // digits = floor(1 + log_base(x)) + // log_base(x) = log_2(x) / log_2(base) + // log_2(x) == 63 - clz(x) + // 1 + (63 - clz(value)) / (63 - clz(1 << shift)) + // 63 - clz(1 << shift) is the same as shift + auto n_digits = 1 + (63 - clz(value)) / shift; + std::string number; + number.resize(n_digits); + std::size_t i = n_digits - 1; + while(value > 0) { + number[i--] = digits[value & mask]; + value >>= shift; + } + return number; + } + } + + inline std::string to_string(std::uint64_t value, const format_options& options) { + switch(options.base) { + case 'd': return std::to_string(value); + case 'H': return to_string<4, 0xf>(value, "0123456789ABCDEF"); + case 'h': return to_string<4, 0xf>(value); + case 'o': return to_string<3, 0x7>(value); + case 'b': return to_string<1, 0x1>(value); + default: + MICROFMT_ASSERT(false); + } + } + + class format_value { + enum class value_type { + char_value, + int64_value, + uint64_value, + string_value, + #if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L + string_view_value, + #endif + c_string_value, + }; + union { + char char_value; + std::int64_t int64_value; + std::uint64_t uint64_value; + const std::string* string_value; + #if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L + std::string_view string_view_value; + #endif + const char* c_string_value; + }; + value_type value; + + public: + format_value(char c) : char_value(c), value(value_type::char_value) {} + format_value(short int_val) : int64_value(int_val), value(value_type::int64_value) {} + format_value(int int_val) : int64_value(int_val), value(value_type::int64_value) {} + format_value(long int_val) : int64_value(int_val), value(value_type::int64_value) {} + format_value(long long int_val) : int64_value(int_val), value(value_type::int64_value) {} + format_value(unsigned char int_val) : uint64_value(int_val), value(value_type::uint64_value) {} + format_value(unsigned short int_val) : uint64_value(int_val), value(value_type::uint64_value) {} + format_value(unsigned int int_val) : uint64_value(int_val), value(value_type::uint64_value) {} + format_value(unsigned long int_val) : uint64_value(int_val), value(value_type::uint64_value) {} + format_value(unsigned long long int_val) : uint64_value(int_val), value(value_type::uint64_value) {} + format_value(const std::string& string) : string_value(&string), value(value_type::string_value) {} + #if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L + format_value(std::string_view sv) : string_view_value(sv), value(value_type::string_view_value) {} + #endif + format_value(const char* c_string) : c_string_value(c_string), value(value_type::c_string_value) {} + + int unwrap_int() const { + switch(value) { + case value_type::int64_value: return static_cast(int64_value); + case value_type::uint64_value: return static_cast(uint64_value); + default: MICROFMT_ASSERT(false); + } + } + + public: + void write(std::string& out, const format_options& options) const { + switch(value) { + case value_type::char_value: + do_write(out, &char_value, &char_value + 1, options); + break; + case value_type::int64_value: + { + std::string str; + std::int64_t val = int64_value; + if(val < 0) { + str += '-'; + val *= -1; + } + str += to_string(static_cast(val), options); + do_write(out, str.begin(), str.end(), options); + } + break; + case value_type::uint64_value: + { + std::string str = to_string(uint64_value, options); + do_write(out, str.begin(), str.end(), options); + } + break; + case value_type::string_value: + do_write(out, string_value->begin(), string_value->end(), options); + break; + #if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L + case value_type::string_view_value: + do_write(out, string_view_value.begin(), string_view_value.end(), options); + break; + #endif + case value_type::c_string_value: + do_write(out, c_string_value, c_string_value + std::strlen(c_string_value), options); + break; + default: + MICROFMT_ASSERT(false); + } + } + }; + + inline int parse_int(const char* begin, const char* end) { + int x = 0; + for(auto it = begin; it != end; it++) { + MICROFMT_ASSERT(isdigit(*it)); + x *= 10; + x += *it - '0'; + } + return x; + } + + template + std::string format(const char* fmt_begin, const char* fmt_end, std::array args) { + std::string str; + std::size_t arg_i = 0; + auto it = fmt_begin; + auto peek = [&] (std::size_t dist = 1) -> char { // 0 on failure + if(it != fmt_end) { + return *(it + dist); + } else { + return 0; + } + }; + auto read_number = [&] () -> int { // -1 on failure + auto scan = it; + while(scan != fmt_end && isdigit(*scan)) { + scan++; + } + if(scan != it) { + int val = parse_int(it, scan); + it = scan; + return val; + } else { + return -1; + } + }; + while(it != fmt_end) { + if(*it == '{') { + if(peek() == '{') { + // try to handle escape + str += '{'; + it++; + } else { + // parse format string + it++; + MICROFMT_ASSERT(it != fmt_end); + format_options options; + // try to parse alignment + if(*it == '<' || *it == '>') { + options.align = *it == '<' ? alignment::left : alignment::right; + it++; + } + // try to parse width + auto width = read_number(); + if(width != -1) { + options.width = width; + } else if(*it == '{') { // try to parse variable width + MICROFMT_ASSERT(peek() == '}'); + it += 2; + MICROFMT_ASSERT(arg_i < N); + options.width = args[arg_i++].unwrap_int(); + } + // try to parse fill/base + if(*it == ':') { + it++; + // try to parse fill + if(*it != '}' && peek(1) != '}') { + // two chars before the }, treat as fill+base + options.fill = *it++; + options.base = *it++; + } else if(*it != '}') { + // one char before the }, treat as base if possible + if(*it == 'd' || *it == 'h' || *it == 'H' || *it == 'o' || *it == 'b') { + options.base = *it++; + } else { + options.fill = *it++; + } + } else { + MICROFMT_ASSERT(false); + } + } + MICROFMT_ASSERT(*it == '}'); + MICROFMT_ASSERT(arg_i < N); + args[arg_i++].write(str, options); + } + } else if(*it == '}') { + // parse }} escape + if(peek() == '}') { + str += '}'; + it++; + } else { + MICROFMT_ASSERT(false); + } + } else { + str += *it; + } + it++; + } + return str; + } + } + + template + #if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L + std::string format(std::string_view fmt, Args&&... args) { + #else + std::string format(const std::string& fmt, Args&&... args) { + #endif + return detail::format(fmt.begin(), fmt.end(), {detail::format_value(args)...}); + } + + template + std::string format(const char* fmt, Args&&... args) { + return detail::format(fmt, fmt + std::strlen(fmt), {detail::format_value(args)...}); + } + + template + void print(const S& fmt, Args&&... args) { + std::cout< + void print(std::ostream& ostream, const S& fmt, Args&&... args) { + ostream< @@ -607,7 +608,9 @@ namespace detail { } inline void file_deleter(std::FILE* ptr) { - fclose(ptr); + if(ptr) { + fclose(ptr); + } } using file_wrapper = raii_wrapper;