|
5 | 5 | #include <string_view> |
6 | 6 | #include <mutex> |
7 | 7 | #include <memory> |
| 8 | +#include <array> |
| 9 | +#include <cstddef> |
8 | 10 | #include <format> |
9 | 11 | #include <atomic> |
10 | 12 |
|
@@ -55,6 +57,14 @@ namespace DetourModKit |
55 | 57 | inline constexpr const char *DEFAULT_LOG_FILE_NAME = "DetourModKit_Log.txt"; |
56 | 58 | inline constexpr const char *DEFAULT_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"; |
57 | 59 |
|
| 60 | + // Upper bound, in bytes, on a log line that the formatted log() / try_log() fast path renders without |
| 61 | + // a heap allocation. Those templates format into a stack buffer of this size and forward a view when |
| 62 | + // the line fits, so a line up to this length never materializes a heap std::string. It mirrors the |
| 63 | + // async sink's inline message buffer (LogMessage::MAX_INLINE_SIZE), which likewise stores a line of |
| 64 | + // this size without touching the StringPool; longer lines take the documented overflow path on both |
| 65 | + // sides. |
| 66 | + inline constexpr std::size_t LOG_INLINE_MESSAGE_SIZE = 512; |
| 67 | + |
58 | 68 | // Forward declarations |
59 | 69 | struct AsyncLoggerConfig; |
60 | 70 | class AsyncLogger; |
@@ -202,7 +212,13 @@ namespace DetourModKit |
202 | 212 | { |
203 | 213 | if (level >= m_current_log_level.load(std::memory_order_acquire)) |
204 | 214 | { |
205 | | - log(level, std::format(fmt, std::forward<Args>(args)...)); |
| 215 | + // Format into a stack buffer instead of through a std::format temporary so a line that fits |
| 216 | + // the inline buffer never heap-allocates. The view handed to the sink is consumed |
| 217 | + // synchronously (the async path copies it into LogMessage, the sync path writes it) before |
| 218 | + // this call returns, so it never outlives the stack buffer. |
| 219 | + static_cast<void>(format_dispatch([this, level](std::string_view formatted) |
| 220 | + { return this->log(level, formatted); }, fmt, |
| 221 | + std::forward<Args>(args)...)); |
206 | 222 | } |
207 | 223 | } |
208 | 224 |
|
@@ -255,7 +271,11 @@ namespace DetourModKit |
255 | 271 | } |
256 | 272 | try |
257 | 273 | { |
258 | | - return log_noexcept(level, std::format(fmt, std::forward<Args>(args)...)); |
| 274 | + // Same allocation-free formatting as log(); the try/catch preserves the no-throw contract |
| 275 | + // because format_to_n and the std::format overflow fallback can both throw. |
| 276 | + return format_dispatch([this, level](std::string_view formatted) noexcept |
| 277 | + { return this->log_noexcept(level, formatted); }, fmt, |
| 278 | + std::forward<Args>(args)...); |
259 | 279 | } |
260 | 280 | catch (...) |
261 | 281 | { |
@@ -296,6 +316,33 @@ namespace DetourModKit |
296 | 316 | Logger(Logger &&) = delete; |
297 | 317 | Logger &operator=(Logger &&) = delete; |
298 | 318 |
|
| 319 | + /** |
| 320 | + * @brief Formats one log line into a stack buffer and hands it to @p sink. |
| 321 | + * @details Renders into a buffer the size of the async sink's inline message buffer |
| 322 | + * (LOG_INLINE_MESSAGE_SIZE). std::format_to_n reports the untruncated length, so a line |
| 323 | + * that fits is passed to @p sink as a view into the stack buffer with no heap allocation; |
| 324 | + * a std::format temporary would instead allocate for any result past the small-string |
| 325 | + * limit, which the async LogMessage then copies into its own inline buffer anyway. A line |
| 326 | + * longer than the inline buffer is the documented overflow case (the async sink stores it |
| 327 | + * via StringPool / heap up to MAX_MESSAGE_SIZE), so it is re-formatted once through |
| 328 | + * std::format to give @p sink the full line. The formatter only reads its arguments (it |
| 329 | + * never moves them), so forwarding the same pack to both format_to_n and the std::format |
| 330 | + * fallback is safe. |
| 331 | + * @return Whatever @p sink returns for the line. |
| 332 | + */ |
| 333 | + template <typename Sink, typename... Args> |
| 334 | + static auto format_dispatch(Sink &&sink, std::format_string<Args...> fmt, Args &&...args) |
| 335 | + { |
| 336 | + std::array<char, LOG_INLINE_MESSAGE_SIZE> buffer; |
| 337 | + const auto result = std::format_to_n(buffer.data(), buffer.size(), fmt, std::forward<Args>(args)...); |
| 338 | + const auto formatted = static_cast<std::size_t>(result.size); |
| 339 | + if (formatted <= buffer.size()) |
| 340 | + { |
| 341 | + return sink(std::string_view(buffer.data(), formatted)); |
| 342 | + } |
| 343 | + return sink(std::string_view(std::format(fmt, std::forward<Args>(args)...))); |
| 344 | + } |
| 345 | + |
299 | 346 | /** |
300 | 347 | * @brief Shared shutdown logic used by both ~Logger() and shutdown(). |
301 | 348 | */ |
|
0 commit comments