From 1b58531249a1759fe273f2fcdcd9327c20f42950 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sun, 14 Sep 2025 19:18:07 +0300 Subject: [PATCH 1/2] ci(emscripten): use emsdk action --- .github/workflows/ci.yml | 23 +++ CMakeLists.txt | 16 ++ .../logit_cpp/logit/detail/TaskExecutor.hpp | 39 ++++- .../logit_cpp/logit/loggers/ConsoleLogger.hpp | 156 +++++++++--------- .../logit_cpp/logit/loggers/FileLogger.hpp | 8 +- .../logit/loggers/UniqueFileLogger.hpp | 8 +- tests/CMakeLists.txt | 34 ++-- tests/ems/async_flush.cpp | 10 ++ tests/ems/console_smoke.cpp | 12 ++ 9 files changed, 201 insertions(+), 105 deletions(-) create mode 100644 tests/ems/async_flush.cpp create mode 100644 tests/ems/console_smoke.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a60ae1..42ce957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,3 +96,26 @@ jobs: run: cmake -B build -S tests/install_consumer -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake - name: Build consumer project run: cmake --build build + + emscripten: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - run: git submodule update --init --recursive + - uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + - name: Configure (Node target) + run: | + emcmake cmake -S . -B build-ems \ + -DCMAKE_BUILD_TYPE=Release \ + -DLOG_IT_CPP_BUILD_TESTS=ON \ + -DLOGIT_EMSCRIPTEN=ON -DLOGIT_FORCE_ASYNC_OFF=ON + - name: Build + run: cmake --build build-ems --target ems_console ems_async_flush -j + - name: Run smoke tests + run: | + node --no-experimental-fetch build-ems/tests/ems_console.js + node --no-experimental-fetch build-ems/tests/ems_async_flush.js diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d2ce19..a75556f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,11 @@ option(LOGIT_USE_SUBMODULES "Allow bundled third_party fallback" OFF) option(LOGIT_WITH_SYSLOG "Enable POSIX syslog backend" ON) option(LOGIT_WITH_WIN_EVENT_LOG "Enable Windows Event Log backend" ON) +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + option(LOGIT_EMSCRIPTEN "Build for Emscripten" ON) +endif() +option(LOGIT_FORCE_ASYNC_OFF "Force disable async logging" OFF) + if(NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 11) endif() @@ -35,6 +40,17 @@ target_include_directories(log-it-cpp INTERFACE target_link_libraries(log-it-cpp INTERFACE time_shield::time_shield) +if(LOGIT_EMSCRIPTEN) + set(LOGIT_WITH_SYSLOG OFF CACHE BOOL "" FORCE) + set(LOGIT_WITH_WIN_EVENT_LOG OFF CACHE BOOL "" FORCE) + target_compile_definitions(log-it-cpp INTERFACE LOGIT_EMSCRIPTEN=1) + target_compile_definitions(log-it-cpp INTERFACE LOGIT_DEFAULT_ASYNC_OFF=1) +endif() + +if(LOGIT_FORCE_ASYNC_OFF) + target_compile_definitions(log-it-cpp INTERFACE LOGIT_DEFAULT_ASYNC_OFF=1) +endif() + if(LOGIT_WITH_SYSLOG AND (UNIX OR APPLE) AND NOT EMSCRIPTEN) target_compile_definitions(log-it-cpp INTERFACE LOGIT_HAS_SYSLOG=1) endif() diff --git a/include/logit_cpp/logit/detail/TaskExecutor.hpp b/include/logit_cpp/logit/detail/TaskExecutor.hpp index b5d6e3f..fc4e396 100644 --- a/include/logit_cpp/logit/detail/TaskExecutor.hpp +++ b/include/logit_cpp/logit/detail/TaskExecutor.hpp @@ -5,23 +5,27 @@ /// \file TaskExecutor.hpp /// \brief Defines the TaskExecutor class, which manages task execution in a separate thread. +#include +#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__) +#include +#include +#else #include #include #include -#include #include -#include #include +#endif namespace logit { namespace detail { /// \brief Queue overflow handling policy. enum class QueuePolicy { Drop, Block }; -#if defined(__EMSCRIPTEN__) +#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__) /// \class TaskExecutor - /// \brief Simplified task executor for single-threaded environments. + /// \brief Simplified task executor for single-threaded Emscripten builds. class TaskExecutor { public: static TaskExecutor& get_instance() { @@ -30,11 +34,16 @@ namespace logit { namespace detail { } void add_task(std::function task) { - if (task) task(); + if (!task) return; + const bool schedule = m_tasks.empty(); + m_tasks.push_back(std::move(task)); + if (schedule) { + emscripten_async_call(&TaskExecutor::drain_thunk, this, 0); + } } - void wait() {} - void shutdown() {} + void wait() { drain(); } + void shutdown() { drain(); } void set_max_queue_size(std::size_t) {} void set_queue_policy(QueuePolicy) {} @@ -46,6 +55,20 @@ namespace logit { namespace detail { TaskExecutor& operator=(const TaskExecutor&) = delete; TaskExecutor(TaskExecutor&&) = delete; TaskExecutor& operator=(TaskExecutor&&) = delete; + + std::deque> m_tasks; + + static void drain_thunk(void* arg) { + static_cast(arg)->drain(); + } + + void drain() { + while (!m_tasks.empty()) { + auto task = std::move(m_tasks.front()); + m_tasks.pop_front(); + task(); + } + } }; #else @@ -166,7 +189,7 @@ namespace logit { namespace detail { TaskExecutor& operator=(TaskExecutor&&) = delete; }; -#endif // defined(__EMSCRIPTEN__) +#endif // defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__) }} // namespace logit::detail diff --git a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp index 2cf040a..e0c4a85 100644 --- a/include/logit_cpp/logit/loggers/ConsoleLogger.hpp +++ b/include/logit_cpp/logit/loggers/ConsoleLogger.hpp @@ -12,6 +12,34 @@ #endif #ifdef __EMSCRIPTEN__ #include +#if defined(LOGIT_EM_BROWSER_COLORS) +EM_JS(void, log_ansi_js, (int lvl, const char* cmsg, const char* cdefcolor), { + const msg = UTF8ToString(cmsg); + const def = UTF8ToString(cdefcolor); + const isNode = (typeof process !== 'undefined' && process.versions && process.versions.node); + const fn = lvl >= 5 ? console.error : (lvl == 4 ? console.warn : console.log); + if (isNode) { fn(msg); return; } + const map = {30:"black",31:"darkred",32:"darkgreen",33:"olive",34:"darkblue",35:"purple",36:"teal",37:"lightgray", + 90:"gray",91:"red",92:"green",93:"yellow",94:"blue",95:"magenta",96:"cyan",97:"white"}; + const re = /\x1b\[(\d+)m/g; + let last = 0, m, style = 'color:' + def; + const fmt = []; + const styles = []; + while ((m = re.exec(msg)) !== null) { + if (m.index > last) { fmt.push('%c' + msg.slice(last, m.index)); styles.push(style); } + style = 'color:' + (map[m[1]] || def); + last = re.lastIndex; + } + if (last < msg.length) { fmt.push('%c' + msg.slice(last)); styles.push(style); } + fn(fmt.join(''), ...styles); +}); +#else +EM_JS(void, log_level, (int lvl, const char* msg), { + const s = UTF8ToString(msg); + const fn = lvl >= 5 ? console.error : (lvl == 4 ? console.warn : console.log); + fn(s); +}); +#endif #endif #include #include @@ -89,8 +117,27 @@ namespace logit { void log(const LogRecord& record, const std::string& message) override { m_last_log_ts = record.timestamp_ms; #ifdef __EMSCRIPTEN__ - std::lock_guard lock(m_mutex); - handle_ansi_colors_emscripten(message); + std::unique_lock lock(m_mutex); + const int lvl = static_cast(record.log_level); + if (!m_config.async) { +# if defined(LOGIT_EM_BROWSER_COLORS) + log_ansi_js(lvl, message.c_str(), text_color_to_css(m_config.default_color)); +# else + log_level(lvl, message.c_str()); +# endif + return; + } + auto msg_copy = std::string(message); + const auto def_color = m_config.default_color; + lock.unlock(); + detail::TaskExecutor::get_instance().add_task([this, lvl, msg_copy, def_color]() { + std::lock_guard inner_lock(m_mutex); +# if defined(LOGIT_EM_BROWSER_COLORS) + log_ansi_js(lvl, msg_copy.c_str(), text_color_to_css(def_color)); +# else + log_level(lvl, msg_copy.c_str()); +# endif + }); return; #else std::unique_lock lock(m_mutex); @@ -174,15 +221,10 @@ namespace logit { /// \brief Waits for all asynchronous tasks to complete. /// If asynchronous logging is enabled, waits for all pending log messages to be written. void wait() override { -#ifdef __EMSCRIPTEN__ - // Nothing to wait for in single-threaded mode - return; -#else std::unique_lock lock(m_mutex); if (!m_config.async) return; lock.unlock(); detail::TaskExecutor::get_instance().wait(); -#endif } private: @@ -191,6 +233,31 @@ namespace logit { std::atomic m_last_log_ts = ATOMIC_VAR_INIT(0); std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); +# ifdef __EMSCRIPTEN__ + /// \brief Convert TextColor to a CSS color name. + const char* text_color_to_css(TextColor color) const { + switch (color) { + case TextColor::Black: return "black"; + case TextColor::DarkRed: return "darkred"; + case TextColor::DarkGreen: return "darkgreen"; + case TextColor::DarkYellow: return "olive"; + case TextColor::DarkBlue: return "darkblue"; + case TextColor::DarkMagenta: return "purple"; + case TextColor::DarkCyan: return "teal"; + case TextColor::LightGray: return "lightgray"; + case TextColor::DarkGray: return "gray"; + case TextColor::Red: return "red"; + case TextColor::Green: return "green"; + case TextColor::Yellow: return "yellow"; + case TextColor::Blue: return "blue"; + case TextColor::Magenta: return "magenta"; + case TextColor::Cyan: return "cyan"; + case TextColor::White: return "white"; + default: return "inherit"; + } + } +# endif + # if defined(_WIN32) // Windows console colors @@ -308,81 +375,6 @@ namespace logit { } # endif -# ifdef __EMSCRIPTEN__ - /// \brief Convert TextColor to a CSS color name for Emscripten console output. - const char* text_color_to_css(TextColor color) const { - switch (color) { - case TextColor::Black: return "black"; - case TextColor::DarkRed: return "darkred"; - case TextColor::DarkGreen: return "darkgreen"; - case TextColor::DarkYellow: return "olive"; - case TextColor::DarkBlue: return "darkblue"; - case TextColor::DarkMagenta: return "purple"; - case TextColor::DarkCyan: return "teal"; - case TextColor::LightGray: return "lightgray"; - case TextColor::DarkGray: return "gray"; - case TextColor::Red: return "red"; - case TextColor::Green: return "green"; - case TextColor::Yellow: return "yellow"; - case TextColor::Blue: return "blue"; - case TextColor::Magenta: return "magenta"; - case TextColor::Cyan: return "cyan"; - case TextColor::White: return "white"; - default: return "inherit"; - } - } - - /// \brief Map ANSI code to a CSS color name. - std::string css_color_from_ansi(const std::string& code) const { - int value = std::stoi(code); - switch (value) { - case 30: return "black"; - case 31: return "darkred"; - case 32: return "darkgreen"; - case 33: return "olive"; - case 34: return "darkblue"; - case 35: return "purple"; - case 36: return "teal"; - case 37: return "lightgray"; - case 90: return "gray"; - case 91: return "red"; - case 92: return "green"; - case 93: return "yellow"; - case 94: return "blue"; - case 95: return "magenta"; - case 96: return "cyan"; - case 97: return "white"; - default: return text_color_to_css(m_config.default_color); - } - } - - /// \brief Handle ANSI color codes when compiling with Emscripten. - void handle_ansi_colors_emscripten(const std::string& message) const { - std::string current_color = text_color_to_css(m_config.default_color); - std::string::size_type start = 0; - std::string::size_type pos = 0; - - while ((pos = message.find("\033[", start)) != std::string::npos) { - if (pos > start) { - std::string part = message.substr(start, pos - start); - EM_ASM_({ console.log('%c' + UTF8ToString($0), 'color: ' + UTF8ToString($1)); }, part.c_str(), current_color.c_str()); - } - std::string::size_type end_pos = message.find('m', pos); - if (end_pos != std::string::npos) { - std::string ansi_code = message.substr(pos + 2, end_pos - pos - 2); - current_color = css_color_from_ansi(ansi_code); - start = end_pos + 1; - } else { - break; - } - } - - if (start < message.size()) { - std::string part = message.substr(start); - EM_ASM_({ console.log('%c' + UTF8ToString($0), 'color: ' + UTF8ToString($1)); }, part.c_str(), current_color.c_str()); - } - } -# endif // __EMSCRIPTEN__ /// \brief Resets the console text color to the default. void reset_color() { diff --git a/include/logit_cpp/logit/loggers/FileLogger.hpp b/include/logit_cpp/logit/loggers/FileLogger.hpp index 9ac9260..4ef0ef9 100644 --- a/include/logit_cpp/logit/loggers/FileLogger.hpp +++ b/include/logit_cpp/logit/loggers/FileLogger.hpp @@ -60,7 +60,13 @@ namespace logit { void wait() override {} private: - void warn() const { std::cerr << "FileLogger is not supported under Emscripten" << std::endl; } + void warn() const { + static bool warned = false; + if (!warned) { + warned = true; + std::cerr << "FileLogger is not supported under Emscripten" << std::endl; + } + } std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); }; diff --git a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp index 4025218..7baf689 100644 --- a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp +++ b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp @@ -46,7 +46,13 @@ namespace logit { void wait() override {} private: - void warn() const { std::cerr << "UniqueFileLogger is not supported under Emscripten" << std::endl; } + void warn() const { + static bool warned = false; + if (!warned) { + warned = true; + std::cerr << "UniqueFileLogger is not supported under Emscripten" << std::endl; + } + } std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index adbc584..034bce0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,14 +1,22 @@ -file(GLOB TEST_SOURCES *.cpp) -if(NOT LOGIT_WITH_GZIP) - list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_gzip_compression_test.cpp) - list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_external_cmd_compression_test.cpp) +if(LOGIT_EMSCRIPTEN) + add_executable(ems_console ems/console_smoke.cpp) + target_link_libraries(ems_console PRIVATE log-it-cpp) + + add_executable(ems_async_flush ems/async_flush.cpp) + target_link_libraries(ems_async_flush PRIVATE log-it-cpp) +else() + file(GLOB TEST_SOURCES *.cpp) + if(NOT LOGIT_WITH_GZIP) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_gzip_compression_test.cpp) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_external_cmd_compression_test.cpp) + endif() + if(NOT LOGIT_WITH_ZSTD) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_zstd_compression_test.cpp) + endif() + foreach(test_src ${TEST_SOURCES}) + get_filename_component(test_name ${test_src} NAME_WE) + add_executable(${test_name} ${test_src}) + target_link_libraries(${test_name} PRIVATE log-it-cpp) + add_test(NAME ${test_name} COMMAND ${test_name}) + endforeach() endif() -if(NOT LOGIT_WITH_ZSTD) - list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_zstd_compression_test.cpp) -endif() -foreach(test_src ${TEST_SOURCES}) - get_filename_component(test_name ${test_src} NAME_WE) - add_executable(${test_name} ${test_src}) - target_link_libraries(${test_name} PRIVATE log-it-cpp) - add_test(NAME ${test_name} COMMAND ${test_name}) -endforeach() diff --git a/tests/ems/async_flush.cpp b/tests/ems/async_flush.cpp new file mode 100644 index 0000000..40ba5a1 --- /dev/null +++ b/tests/ems/async_flush.cpp @@ -0,0 +1,10 @@ +#include + +int main() { + LOGIT_ADD_CONSOLE(LOGIT_CONSOLE_PATTERN, true); + for (int i = 0; i < 1000; ++i) { + LOGIT_INFO("message"); + } + LOGIT_WAIT(); + return 0; +} diff --git a/tests/ems/console_smoke.cpp b/tests/ems/console_smoke.cpp new file mode 100644 index 0000000..008fc34 --- /dev/null +++ b/tests/ems/console_smoke.cpp @@ -0,0 +1,12 @@ +#include + +int main() { + LOGIT_ADD_CONSOLE_DEFAULT(); + LOGIT_TRACE("trace message"); + LOGIT_DEBUG("debug message"); + LOGIT_INFO("info message"); + LOGIT_WARN("warn message"); + LOGIT_ERROR("error message"); + LOGIT_FATAL("fatal message"); + return 0; +} From e56fdf666947e808c61311d49a5fa960a609a2b1 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sun, 14 Sep 2025 19:38:47 +0300 Subject: [PATCH 2/2] fix(emscripten): export UTF8ToString runtime method Ensure UTF8ToString is kept in the generated JS runtime to avoid ReferenceError at startup. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a75556f..aa1207c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ if(LOGIT_EMSCRIPTEN) set(LOGIT_WITH_WIN_EVENT_LOG OFF CACHE BOOL "" FORCE) target_compile_definitions(log-it-cpp INTERFACE LOGIT_EMSCRIPTEN=1) target_compile_definitions(log-it-cpp INTERFACE LOGIT_DEFAULT_ASYNC_OFF=1) + target_link_options(log-it-cpp INTERFACE "-sEXPORTED_RUNTIME_METHODS=['UTF8ToString']") endif() if(LOGIT_FORCE_ASYNC_OFF)