Skip to content

Commit 647a95c

Browse files
committed
Allow dynamic format string with arguments; bump version to v1.0.6
1 parent 8a9b7a4 commit 647a95c

5 files changed

Lines changed: 82 additions & 45 deletions

File tree

CHANGELOG

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# v1.0.6 - 02-13-2026
2+
- Fix logging through downstream `const char*` format bridge with `std::format_args`
3+
- Dynamic (non-literal) format strings are now copied into the internal string queue so they remain valid across the async writer boundary
4+
- Dynamic format strings now support arguments instead of requiring zero args
5+
- Fix string queue copy path to null-terminate using explicit payload length
6+
- Add regression test for `const char* + std::format_args` bridge logging
7+
18
# v1.0.5 - 02-10-2026
29
- Add `std::format_args` support: pass a pre-built `std::format_args` object directly to any log call
310
- Arguments are unpacked and copied into the log entry on the calling thread (safe across thread boundary)

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.20)
22

33
project(slick-logger
4-
VERSION 1.0.5
4+
VERSION 1.0.6
55
LANGUAGES CXX)
66

77
set(CMAKE_CXX_STANDARD 20)

include/slick/logger.hpp

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555

5656
#define SLICK_LOGGER_VERSION_MAJOR 1
5757
#define SLICK_LOGGER_VERSION_MINOR 0
58-
#define SLICK_LOGGER_VERSION_PATCH 5
59-
#define SLICK_LOGGER_VERSION "1.0.5"
58+
#define SLICK_LOGGER_VERSION_PATCH 6
59+
#define SLICK_LOGGER_VERSION "1.0.6"
6060

6161
#ifndef SLICK_LOGGER_MAX_ARGS
6262
#define SLICK_LOGGER_MAX_ARGS 20
@@ -1441,26 +1441,27 @@ inline void Logger::log_to_sink(int sink_index, LogLevel level, FormatT&& format
14411441
entry.sink_index = sink_index;
14421442
if constexpr (IS_STRING_LITERAL(format)) {
14431443
entry.format_ptr = format; // String literal - safe to store pointer
1444-
1445-
if constexpr (is_single_format_args_v<Args...>) {
1446-
// Pre-built std::format_args: unpack values into the entry on the
1447-
// calling thread (format_args holds non-owning references).
1448-
enqueue_format_args(entry,
1449-
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1450-
} else {
1451-
// Normal path: push individual arguments
1452-
entry.arg_count = sizeof...(args);
1453-
size_t arg_idx = 0;
1454-
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1455-
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1456-
}
14571444
}
14581445
else {
1459-
// Store dynamic string in string queue
1460-
static_assert(sizeof...(args) == 0, "Dynamic format strings are only supported when there are no arguments, to avoid dangling pointers.");
1461-
entry.format_ptr = "{}";
1462-
entry.arg_count = 1;
1463-
enqueue_argument(entry.args[0], format);
1446+
// Non-literal format strings are copied into the string queue so their
1447+
// lifetime extends until the writer thread consumes the entry.
1448+
static_assert(std::is_convertible_v<FormatT, std::string_view>,
1449+
"Format string type must be a string literal or convertible to std::string_view.");
1450+
entry.format_ptr = store_string_in_queue(
1451+
std::string_view{std::forward<FormatT>(format)}).ptr;
1452+
}
1453+
1454+
if constexpr (is_single_format_args_v<Args...>) {
1455+
// Pre-built std::format_args: unpack values into the entry on the
1456+
// calling thread (format_args holds non-owning references).
1457+
enqueue_format_args(entry,
1458+
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1459+
} else {
1460+
// Normal path: push individual arguments
1461+
entry.arg_count = sizeof...(args);
1462+
size_t arg_idx = 0;
1463+
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1464+
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
14641465
}
14651466

14661467

@@ -1602,8 +1603,9 @@ inline StringRef Logger::store_string_in_queue(std::string_view str) {
16021603

16031604
char* dest = (*string_queue_)[start_index];
16041605
if (length) {
1605-
// Copy string data
1606-
std::memcpy(dest, str.data(), len);
1606+
// Copy only the string payload, then terminate explicitly.
1607+
std::memcpy(dest, str.data(), length);
1608+
dest[length] = '\0';
16071609
}
16081610
else {
16091611
*dest = '\0';

src/logger.hpp

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,26 +1439,27 @@ inline void Logger::log_to_sink(int sink_index, LogLevel level, FormatT&& format
14391439
entry.sink_index = sink_index;
14401440
if constexpr (IS_STRING_LITERAL(format)) {
14411441
entry.format_ptr = format; // String literal - safe to store pointer
1442-
1443-
if constexpr (is_single_format_args_v<Args...>) {
1444-
// Pre-built std::format_args: unpack values into the entry on the
1445-
// calling thread (format_args holds non-owning references).
1446-
enqueue_format_args(entry,
1447-
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1448-
} else {
1449-
// Normal path: push individual arguments
1450-
entry.arg_count = sizeof...(args);
1451-
size_t arg_idx = 0;
1452-
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1453-
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1454-
}
14551442
}
14561443
else {
1457-
// Store dynamic string in string queue
1458-
static_assert(sizeof...(args) == 0, "Dynamic format strings are only supported when there are no arguments, to avoid dangling pointers.");
1459-
entry.format_ptr = "{}";
1460-
entry.arg_count = 1;
1461-
enqueue_argument(entry.args[0], format);
1444+
// Non-literal format strings are copied into the string queue so their
1445+
// lifetime extends until the writer thread consumes the entry.
1446+
static_assert(std::is_convertible_v<FormatT, std::string_view>,
1447+
"Format string type must be a string literal or convertible to std::string_view.");
1448+
entry.format_ptr = store_string_in_queue(
1449+
std::string_view{std::forward<FormatT>(format)}).ptr;
1450+
}
1451+
1452+
if constexpr (is_single_format_args_v<Args...>) {
1453+
// Pre-built std::format_args: unpack values into the entry on the
1454+
// calling thread (format_args holds non-owning references).
1455+
enqueue_format_args(entry,
1456+
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1457+
} else {
1458+
// Normal path: push individual arguments
1459+
entry.arg_count = sizeof...(args);
1460+
size_t arg_idx = 0;
1461+
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1462+
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
14621463
}
14631464

14641465

@@ -1600,8 +1601,9 @@ inline StringRef Logger::store_string_in_queue(std::string_view str) {
16001601

16011602
char* dest = (*string_queue_)[start_index];
16021603
if (length) {
1603-
// Copy string data
1604-
std::memcpy(dest, str.data(), len);
1604+
// Copy only the string payload, then terminate explicitly.
1605+
std::memcpy(dest, str.data(), length);
1606+
dest[length] = '\0';
16051607
}
16061608
else {
16071609
*dest = '\0';

tests/test_logger.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class SlickLoggerTest : public ::testing::Test {
1919
std::filesystem::remove("test_single_string.log");
2020
std::filesystem::remove("test_empty_string.log");
2121
std::filesystem::remove("test_format_args.log");
22+
std::filesystem::remove("test_format_args_const_char.log");
2223
}
2324
};
2425

@@ -398,9 +399,34 @@ TEST_F(SlickLoggerTest, FormatArgsLogging) {
398399
}
399400

400401
EXPECT_NE(file_contents.find("int=42 double=3.14 str=hello bool=true"), std::string::npos);
402+
}
401403

402-
log_file.close();
403-
std::filesystem::remove("test_format_args.log");
404+
TEST_F(SlickLoggerTest, FormatArgsConstCharBridgeLogging) {
405+
std::filesystem::remove("test_format_args_const_char.log");
406+
407+
slick::logger::Logger::instance().init("test_format_args_const_char.log", 1024);
408+
409+
auto log_bridge = [](slick::logger::LogLevel level, const char* format_text, std::format_args args) {
410+
slick::logger::Logger::instance().log(level, format_text, args);
411+
};
412+
413+
int number = 7;
414+
std::string_view text = "bridge";
415+
log_bridge(slick::logger::LogLevel::L_INFO, "bridge value={} text={}", std::make_format_args(number, text));
416+
417+
slick::logger::Logger::instance().shutdown();
418+
419+
ASSERT_TRUE(std::filesystem::exists("test_format_args_const_char.log"));
420+
421+
std::ifstream log_file("test_format_args_const_char.log");
422+
std::string file_contents;
423+
std::string line;
424+
std::getline(log_file, line); // first line is the logger's version
425+
while (std::getline(log_file, line)) {
426+
file_contents += line + "\n";
427+
}
428+
429+
EXPECT_NE(file_contents.find("bridge value=7 text=bridge"), std::string::npos);
404430
}
405431

406432
int main(int argc, char **argv) {

0 commit comments

Comments
 (0)