Skip to content

Commit 8453236

Browse files
committed
Add cross-DSO logger instance sharing and flush functionality; bump version to v1.0.8
1 parent 6126ddf commit 8453236

9 files changed

Lines changed: 553 additions & 8 deletions

File tree

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# v1.0.8 - 04-02-2026
2+
- Add cross-DSO logger instance sharing via `Logger::set_instance()` and `Logger::clear_instance_override()`
3+
- Enables plugin/shared-library architectures where dynamically-loaded modules should route `LOG_*` calls to the host application's logger
4+
- `instance()` now uses an `std::atomic<Logger*>` initialized to the library-local singleton, swappable at runtime
5+
- Thread-safe: atomic acquire-release ordering ensures full visibility of the target logger's initialization
6+
- Add `Logger::flush()`: blocks until all queued log entries are written, then returns with the logger still running
7+
- Use before `FreeLibrary`/`dlclose` to drain entries whose format-string pointers live in the plugin's code segment
8+
19
# v1.0.7 - 03-30-2026
210
- Add to_log_level function
311

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.7
4+
VERSION 1.0.8
55
LANGUAGES CXX)
66

77
set(CMAKE_CXX_STANDARD 20)

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,56 @@ int main() {
365365
}
366366
```
367367

368+
### Sharing the Logger Across Shared Libraries (Plugin / Strategy Pattern)
369+
370+
Because slick-logger is header-only, each binary (EXE or `.dll`/`.so`) that includes `logger.hpp` gets its own `Logger` instance. This means `LOG_*` calls inside a dynamically-loaded plugin will not appear in the host application's log file by default.
371+
372+
Use `Logger::set_instance()` to redirect a plugin's `LOG_*` calls to the host's logger:
373+
374+
**Host application** — pass its logger to the plugin after loading:
375+
376+
```cpp
377+
// host/main.cpp
378+
#include <slick/logger.hpp>
379+
380+
// Platform-specific shared library loading omitted for brevity (LoadLibrary on Windows, dlopen on Linux)
381+
using StrategyInitFn = void(*)(slick::logger::Logger&);
382+
383+
void load_strategy(void* handle) {
384+
auto* init_fn = reinterpret_cast<StrategyInitFn>(get_symbol(handle, "strategy_init"));
385+
if (init_fn) {
386+
// Redirect the plugin's LOG_* macros to this process's logger
387+
init_fn(slick::logger::Logger::instance());
388+
}
389+
}
390+
```
391+
392+
**Plugin / strategy shared library** — expose a C-linkage init function:
393+
394+
```cpp
395+
// strategy/strategy.cpp
396+
#include <slick/logger.hpp>
397+
398+
extern "C" void strategy_init(slick::logger::Logger& framework_logger) {
399+
// All LOG_* calls in this shared library now route to the host's logger
400+
slick::logger::Logger::set_instance(&framework_logger);
401+
LOG_INFO("Strategy loaded — logger connected to framework");
402+
}
403+
404+
extern "C" void strategy_shutdown() {
405+
// Optional: restore this library's own local logger on unload
406+
slick::logger::Logger::clear_instance_override();
407+
}
408+
```
409+
410+
> **Why `extern "C"`?** C linkage prevents name-mangling differences between compilers, ensuring `get_symbol`/`GetProcAddress`/`dlsym` can reliably locate the function.
411+
412+
> **Thread-safety:** `set_instance()` uses `std::atomic` with acquire-release ordering. Call it once during plugin initialization, before any logging threads in the plugin start.
413+
414+
> **Unload ordering — important:** `LOG_*` with string literals stores a raw pointer to the format string, which lives in the plugin's code segment. The host must call `Logger::instance().flush()` **before** calling `FreeLibrary`/`dlclose`. `flush()` blocks until the writer thread has consumed all queued entries, then returns with the logger still running so host logging can continue normally. Using `shutdown()` instead would stop the logger entirely.
415+
416+
> **Multiple plugins:** Each shared library (`.dll`/`.so`) holds its own copy of `override_instance_`. All plugins can independently call `set_instance()` with the same host logger — the host logger's lock-free queue is designed for concurrent producers.
417+
368418
## Sink Types
369419

370420
### ConsoleSink

include/slick/logger.hpp

Lines changed: 66 additions & 4 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 7
59-
#define SLICK_LOGGER_VERSION "1.0.7"
58+
#define SLICK_LOGGER_VERSION_PATCH 8
59+
#define SLICK_LOGGER_VERSION "1.0.8"
6060

6161
#ifndef SLICK_LOGGER_MAX_ARGS
6262
#define SLICK_LOGGER_MAX_ARGS 20
@@ -620,12 +620,43 @@ class Logger {
620620
*/
621621
void shutdown(bool clear_sinks = true);
622622

623+
/**
624+
* @brief Block until all log entries queued before this call have been
625+
* written to all sinks. The logger keeps running — unlike shutdown(),
626+
* logging can continue normally after flush() returns.
627+
*
628+
* Primary use case: call flush() before unloading a shared library whose
629+
* string literals are referenced by queued log entries. Because LOG_* with
630+
* string literals stores a raw pointer into the caller's code segment,
631+
* unloading the library while entries are still queued would leave dangling
632+
* pointers that the writer thread would later dereference.
633+
*
634+
* Does nothing if the logger has not been initialized.
635+
*/
636+
void flush();
637+
623638
/**
624639
* @brief Reset the logger to uninitialized state
625640
* Note: This is primarily for testing purposes. Use with caution.
626641
*/
627642
void reset();
628643

644+
/**
645+
* @brief Redirect Logger::instance() to an external Logger.
646+
*
647+
* Because slick-logger is header-only, every shared library (.dll/.so) and
648+
* the host executable each compile in their own copy of Logger::instance().
649+
* Call this from a shared library's init function to make all LOG_* macros
650+
* in that library route to the host's logger instead of the library-local one.
651+
* Pass nullptr to restore the library-local singleton.
652+
*
653+
* Thread-safety: uses atomic store/load with acquire-release ordering, so
654+
* all initialization of the target logger is visible before any LOG_* call
655+
* after set_instance() returns.
656+
*/
657+
static void set_instance(Logger* logger) noexcept;
658+
static void clear_instance_override() noexcept;
659+
629660
private:
630661
Logger() = default;
631662
~Logger();
@@ -658,6 +689,14 @@ class Logger {
658689
uint64_t read_index_{0};
659690
std::atomic<LogLevel> log_level_{LogLevel::L_TRACE};
660691
std::unordered_map<std::string_view, int> sinkname_index_map_;
692+
693+
// The logger instance local to this shared library / executable.
694+
// override_instance_ points here by default.
695+
static Logger instance_;
696+
// Points to the active Logger for this shared library / executable.
697+
// Defaults to &instance_. Call set_instance() to redirect to an external
698+
// logger (e.g. the host application's logger when loaded as a plugin).
699+
static std::atomic<Logger*> override_instance_;
661700
};
662701

663702

@@ -1253,8 +1292,18 @@ inline std::string DailyFileSink::get_date_string() const {
12531292
}
12541293

12551294
inline Logger& Logger::instance() {
1256-
static Logger instance;
1257-
return instance;
1295+
return *override_instance_.load(std::memory_order_acquire);
1296+
}
1297+
1298+
inline Logger Logger::instance_;
1299+
inline std::atomic<Logger*> Logger::override_instance_{&Logger::instance_};
1300+
1301+
inline void Logger::set_instance(Logger* logger) noexcept {
1302+
override_instance_.store(logger, std::memory_order_release);
1303+
}
1304+
1305+
inline void Logger::clear_instance_override() noexcept {
1306+
override_instance_.store(&instance_, std::memory_order_release);
12581307
}
12591308

12601309
inline void Logger::init(const std::filesystem::path& log_file, size_t log_queue_size, size_t string_buffer_size) {
@@ -1680,6 +1729,19 @@ inline Logger::~Logger() {
16801729
shutdown();
16811730
}
16821731

1732+
inline void Logger::flush() {
1733+
if (!running_.load(std::memory_order_relaxed) || !log_queue_) {
1734+
return;
1735+
}
1736+
// Snapshot the write cursor: any entry reserved before this point has
1737+
// already been assigned an index below drain_target.
1738+
const uint64_t drain_target = log_queue_->initial_reading_index();
1739+
// Wait until the writer thread's read cursor reaches drain_target.
1740+
while (running_.load(std::memory_order_relaxed) && read_index_ < drain_target) {
1741+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
1742+
}
1743+
}
1744+
16831745
inline void Logger::reset() {
16841746
shutdown();
16851747
// Reset all state for fresh initialization

src/logger.hpp

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,12 +618,43 @@ class Logger {
618618
*/
619619
void shutdown(bool clear_sinks = true);
620620

621+
/**
622+
* @brief Block until all log entries queued before this call have been
623+
* written to all sinks. The logger keeps running — unlike shutdown(),
624+
* logging can continue normally after flush() returns.
625+
*
626+
* Primary use case: call flush() before unloading a shared library whose
627+
* string literals are referenced by queued log entries. Because LOG_* with
628+
* string literals stores a raw pointer into the caller's code segment,
629+
* unloading the library while entries are still queued would leave dangling
630+
* pointers that the writer thread would later dereference.
631+
*
632+
* Does nothing if the logger has not been initialized.
633+
*/
634+
void flush();
635+
621636
/**
622637
* @brief Reset the logger to uninitialized state
623638
* Note: This is primarily for testing purposes. Use with caution.
624639
*/
625640
void reset();
626641

642+
/**
643+
* @brief Redirect Logger::instance() to an external Logger.
644+
*
645+
* Because slick-logger is header-only, every shared library (.dll/.so) and
646+
* the host executable each compile in their own copy of Logger::instance().
647+
* Call this from a shared library's init function to make all LOG_* macros
648+
* in that library route to the host's logger instead of the library-local one.
649+
* Pass nullptr to restore the library-local singleton.
650+
*
651+
* Thread-safety: uses atomic store/load with acquire-release ordering, so
652+
* all initialization of the target logger is visible before any LOG_* call
653+
* after set_instance() returns.
654+
*/
655+
static void set_instance(Logger* logger) noexcept;
656+
static void clear_instance_override() noexcept;
657+
627658
private:
628659
Logger() = default;
629660
~Logger();
@@ -656,6 +687,14 @@ class Logger {
656687
uint64_t read_index_{0};
657688
std::atomic<LogLevel> log_level_{LogLevel::L_TRACE};
658689
std::unordered_map<std::string_view, int> sinkname_index_map_;
690+
691+
// The logger instance local to this shared library / executable.
692+
// override_instance_ points here by default.
693+
static Logger instance_;
694+
// Points to the active Logger for this shared library / executable.
695+
// Defaults to &instance_. Call set_instance() to redirect to an external
696+
// logger (e.g. the host application's logger when loaded as a plugin).
697+
static std::atomic<Logger*> override_instance_;
659698
};
660699

661700

@@ -1251,8 +1290,18 @@ inline std::string DailyFileSink::get_date_string() const {
12511290
}
12521291

12531292
inline Logger& Logger::instance() {
1254-
static Logger instance;
1255-
return instance;
1293+
return *override_instance_.load(std::memory_order_acquire);
1294+
}
1295+
1296+
inline Logger Logger::instance_;
1297+
inline std::atomic<Logger*> Logger::override_instance_{&Logger::instance_};
1298+
1299+
inline void Logger::set_instance(Logger* logger) noexcept {
1300+
override_instance_.store(logger, std::memory_order_release);
1301+
}
1302+
1303+
inline void Logger::clear_instance_override() noexcept {
1304+
override_instance_.store(&instance_, std::memory_order_release);
12561305
}
12571306

12581307
inline void Logger::init(const std::filesystem::path& log_file, size_t log_queue_size, size_t string_buffer_size) {
@@ -1678,6 +1727,19 @@ inline Logger::~Logger() {
16781727
shutdown();
16791728
}
16801729

1730+
inline void Logger::flush() {
1731+
if (!running_.load(std::memory_order_relaxed) || !log_queue_) {
1732+
return;
1733+
}
1734+
// Snapshot the write cursor: any entry reserved before this point has
1735+
// already been assigned an index below drain_target.
1736+
const uint64_t drain_target = log_queue_->initial_reading_index();
1737+
// Wait until the writer thread's read cursor reaches drain_target.
1738+
while (running_.load(std::memory_order_relaxed) && read_index_ < drain_target) {
1739+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
1740+
}
1741+
}
1742+
16811743
inline void Logger::reset() {
16821744
shutdown();
16831745
// Reset all state for fresh initialization

tests/CMakeLists.txt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,28 @@ if(BUILD_SLICK_LOGGER_TESTING)
3030
target_include_directories(slick_logger_timestamp_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
3131
target_link_libraries(slick_logger_timestamp_tests slick::logger GTest::gtest_main)
3232
add_test(NAME slick_logger_timestamp_tests COMMAND slick_logger_timestamp_tests)
33-
33+
34+
# --- Shared-library integration test ---
35+
# Build a minimal plugin as a real shared library (.dll/.so), then load it
36+
# at runtime to verify LOG_* calls inside the plugin are routed to the host
37+
# logger via Logger::set_instance().
38+
add_library(slick_logger_test_plugin SHARED plugin/plugin.cpp)
39+
target_link_libraries(slick_logger_test_plugin PRIVATE slick::logger)
40+
# Hide all symbols by default on non-Windows so only PLUGIN_EXPORT ones are visible.
41+
set_target_properties(slick_logger_test_plugin PROPERTIES
42+
CXX_VISIBILITY_PRESET hidden
43+
VISIBILITY_INLINES_HIDDEN ON
44+
)
45+
46+
# Resolve the plugin path at configure time so the test executable knows
47+
# where to find the .dll/.so without relying on PATH/LD_LIBRARY_PATH.
48+
add_executable(slick_logger_shared_lib_tests test_shared_library.cpp)
49+
target_link_libraries(slick_logger_shared_lib_tests PRIVATE slick::logger GTest::gtest_main)
50+
target_compile_definitions(slick_logger_shared_lib_tests PRIVATE
51+
PLUGIN_LIB_PATH="$<TARGET_FILE:slick_logger_test_plugin>"
52+
)
53+
add_test(NAME slick_logger_shared_lib_tests COMMAND slick_logger_shared_lib_tests)
54+
3455
else()
3556
message(STATUS "Testing disabled - skipping slick-logger tests")
3657
endif()

tests/plugin/plugin.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <slick/logger.hpp>
2+
3+
// Exported symbols use C linkage to avoid name-mangling differences across
4+
// compilers and platforms (required for GetProcAddress / dlsym to work).
5+
6+
extern "C" {
7+
8+
#if defined(_WIN32)
9+
# define PLUGIN_EXPORT __declspec(dllexport)
10+
#else
11+
# define PLUGIN_EXPORT __attribute__((visibility("default")))
12+
#endif
13+
14+
/**
15+
* Called by the host after loading this shared library.
16+
* Redirects this library's Logger::instance() to the host's logger so that
17+
* all LOG_* calls from this library appear in the host's log file.
18+
*
19+
* IMPORTANT — unload ordering:
20+
* LOG_* with string literals stores a raw pointer to the format string, which
21+
* lives in this library's code segment. The host must call
22+
* Logger::instance().flush() BEFORE calling FreeLibrary/dlclose to ensure the
23+
* writer thread has consumed all queued entries. Unlike shutdown(), flush()
24+
* leaves the logger running so the host can continue logging after the call.
25+
*/
26+
PLUGIN_EXPORT void plugin_init(slick::logger::Logger* host_logger) {
27+
slick::logger::Logger::set_instance(host_logger);
28+
LOG_INFO("plugin_init: logger connected to host");
29+
}
30+
31+
/**
32+
* Exercise several log levels so the test can verify each one is routed
33+
* to the host logger.
34+
*/
35+
PLUGIN_EXPORT void plugin_log_messages() {
36+
LOG_DEBUG("plugin debug message");
37+
LOG_INFO("plugin info message");
38+
LOG_WARN("plugin warn message");
39+
LOG_ERROR("plugin error message");
40+
}
41+
42+
/**
43+
* Called by the host before unloading this shared library.
44+
* Restores this library's own local logger so that any logging after
45+
* unload does not dereference the (potentially destroyed) host logger.
46+
*/
47+
PLUGIN_EXPORT void plugin_shutdown() {
48+
slick::logger::Logger::clear_instance_override();
49+
}
50+
51+
} // extern "C"

0 commit comments

Comments
 (0)