Merge branch 'dev' into jr/more-thorough-error-handling
This commit is contained in:
commit
89a7afaded
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -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
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@ -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:
|
||||
|
||||
25
CHANGELOG.md
25
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:
|
||||
|
||||
@ -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 "<CMAKE_RANLIB> -no_warning_for_no_symbols -c <TARGET>")
|
||||
SET(CMAKE_CXX_ARCHIVE_FINISH "<CMAKE_RANLIB> -no_warning_for_no_symbols -c <TARGET>")
|
||||
|
||||
36
README.md
36
README.md
@ -6,7 +6,7 @@
|
||||
<br/>
|
||||
[-Community%20Discord-blue?labelColor=2C3239&color=7289DA&style=flat&logo=discord&logoColor=959DA5)](https://discord.gg/frjaAZvqUZ)
|
||||
<br/>
|
||||
[](https://godbolt.org/z/5sEszzEPE)
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
## 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 # <HASH or TAG>
|
||||
GIT_TAG v0.5.1 # <HASH or TAG>
|
||||
)
|
||||
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 # <HASH or TAG>
|
||||
GIT_TAG v0.5.1 # <HASH or TAG>
|
||||
)
|
||||
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
|
||||
|
||||
@ -118,7 +118,7 @@ namespace cpptrace {
|
||||
return *this;
|
||||
}
|
||||
bool has_value() const noexcept {
|
||||
return raw_value != std::numeric_limits<T>::max();
|
||||
return raw_value != (std::numeric_limits<T>::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<T>::max();
|
||||
raw_value = (std::numeric_limits<T>::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<T>::max() };
|
||||
return { (std::numeric_limits<T>::max)() };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
BIN
res/snippets.png
Normal file
BIN
res/snippets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@ -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;
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include <iostream>
|
||||
|
||||
#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<std::size_t>(size));
|
||||
if(!file.read(&contents[0], size)) {
|
||||
// error ...
|
||||
}
|
||||
|
||||
@ -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<std::recursive_mutex> 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<std::uint32_t>(line.LineNumber) },
|
||||
nullable<std::uint32_t>::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<std::uint32_t>(line.LineNumber) },
|
||||
nullable<std::uint32_t>::null(),
|
||||
line.FileName,
|
||||
@ -388,7 +395,7 @@ namespace dbghelp {
|
||||
} else {
|
||||
return {
|
||||
addr,
|
||||
0,
|
||||
object_frame.object_address,
|
||||
nullable<std::uint32_t>::null(),
|
||||
nullable<std::uint32_t>::null(),
|
||||
"",
|
||||
@ -397,7 +404,15 @@ namespace dbghelp {
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return { addr, 0, nullable<std::uint32_t>::null(), nullable<std::uint32_t>::null(), "", "", false };
|
||||
return {
|
||||
addr,
|
||||
object_frame.object_address,
|
||||
nullable<std::uint32_t>::null(),
|
||||
nullable<std::uint32_t>::null(),
|
||||
"",
|
||||
"",
|
||||
false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -60,18 +60,6 @@
|
||||
|
||||
namespace cpptrace {
|
||||
namespace detail {
|
||||
// Placed here instead of utils because it's used by error.hpp and utils.hpp
|
||||
template<typename... T> 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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<Dwarf_Addr>::max()) {
|
||||
if(lowpc != (std::numeric_limits<Dwarf_Addr>::max)()) {
|
||||
baseaddr = lowpc;
|
||||
}
|
||||
Dwarf_Ranges* ranges = nullptr;
|
||||
@ -381,7 +381,7 @@ namespace libdwarf {
|
||||
template<typename F>
|
||||
// callback should return true to keep going
|
||||
void dwarf_ranges(int version, optional<Dwarf_Addr> pc, F callback) const {
|
||||
Dwarf_Addr lowpc = std::numeric_limits<Dwarf_Addr>::max();
|
||||
Dwarf_Addr lowpc = (std::numeric_limits<Dwarf_Addr>::max)();
|
||||
if(wrap(dwarf_lowpc, die, &lowpc) == DW_DLV_OK) {
|
||||
if(pc.has_value() && pc.unwrap() == lowpc) {
|
||||
callback(lowpc, lowpc + 1);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <utility>
|
||||
|
||||
#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<std::underlying_type<assert_type>::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()
|
||||
)
|
||||
);
|
||||
|
||||
325
src/utils/microfmt.hpp
Normal file
325
src/utils/microfmt.hpp
Normal file
@ -0,0 +1,325 @@
|
||||
#ifndef MICROFMT_HPP
|
||||
#define MICROFMT_HPP
|
||||
|
||||
// Copyright (c) 2024 Jeremy Rifkin; MIT License
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201606L
|
||||
#include <string_view>
|
||||
#endif
|
||||
#ifdef _MSC_VER
|
||||
#include <intrin.h>
|
||||
#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<typename It> 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<std::size_t>(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<int shift, int mask>
|
||||
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<int>(int64_value);
|
||||
case value_type::uint64_value: return static_cast<int>(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<std::uint64_t>(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::size_t N>
|
||||
std::string format(const char* fmt_begin, const char* fmt_end, std::array<format_value, N> 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<typename... Args>
|
||||
#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<sizeof...(args)>(fmt.begin(), fmt.end(), {detail::format_value(args)...});
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
std::string format(const char* fmt, Args&&... args) {
|
||||
return detail::format<sizeof...(args)>(fmt, fmt + std::strlen(fmt), {detail::format_value(args)...});
|
||||
}
|
||||
|
||||
template<typename S, typename... Args>
|
||||
void print(const S& fmt, Args&&... args) {
|
||||
std::cout<<format(fmt, args...);
|
||||
}
|
||||
|
||||
template<typename S, typename... Args>
|
||||
void print(std::ostream& ostream, const S& fmt, Args&&... args) {
|
||||
ostream<<format(fmt, args...);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
#include "common.hpp"
|
||||
#include "error.hpp"
|
||||
#include "microfmt.hpp"
|
||||
|
||||
#if IS_WINDOWS
|
||||
#include <windows.h>
|
||||
@ -607,7 +608,9 @@ namespace detail {
|
||||
}
|
||||
|
||||
inline void file_deleter(std::FILE* ptr) {
|
||||
fclose(ptr);
|
||||
if(ptr) {
|
||||
fclose(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
using file_wrapper = raii_wrapper<std::FILE*, void(*)(std::FILE*)>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user