├── .github └── workflows │ ├── cmake.yml │ └── format.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── data ├── PresentationScreenshot.png └── SlideTitle.png ├── examples ├── CMakeLists.txt ├── custom_queue_example │ ├── CMakeLists.txt │ └── customqueuemain.cpp └── everlog │ ├── CMakeLists.txt │ └── everlogmain.cpp ├── include └── rtlog │ └── rtlog.h └── test ├── CMakeLists.txt └── test_rtlog.cpp /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 11 | BUILD_TYPE: Release 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | use_fmtlib: [ON, OFF] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set up CMake 25 | if: runner.os == 'Windows' 26 | run: | 27 | choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' 28 | cmake --version 29 | - name: Set up CMake 30 | if: runner.os != 'Windows' 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y cmake 34 | cmake --version 35 | 36 | - name: Configure CMake 37 | run: cmake -B build -DRTLOG_USE_FMTLIB=${{ matrix.use_fmtlib }} -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DRTLOG_FULL_WARNINGS=ON -DRTLOG_BUILD_TESTS=ON -DRTLOG_BUILD_EXAMPLES=ON 38 | 39 | - name: Build 40 | run: cmake --build build --config ${{ env.BUILD_TYPE }} -j 2 41 | 42 | - name: Test 43 | working-directory: build 44 | run: ctest -C ${{ env.BUILD_TYPE }} --timeout 10 45 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | # Run clang-format 2 | name: Clang-format 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | checkout-and-check-formatting: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run clang-format 17 | uses: HorstBaerbel/action-clang-format@1.5 18 | with: 19 | extensions: 'c,h,cpp' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### C++ 2 | # Prerequisites 3 | *.d 4 | 5 | # Compiled Object files 6 | *.slo 7 | *.lo 8 | *.o 9 | *.obj 10 | 11 | # Precompiled Headers 12 | *.gch 13 | *.pch 14 | 15 | # Compiled Dynamic libraries 16 | *.so 17 | *.dylib 18 | *.dll 19 | 20 | # Fortran module files 21 | *.mod 22 | *.smod 23 | 24 | # Compiled Static libraries 25 | *.lai 26 | *.la 27 | *.a 28 | *.lib 29 | 30 | # Executables 31 | *.exe 32 | *.out 33 | *.app 34 | 35 | ### CMake 36 | CMakeLists.txt.user 37 | CMakeCache.txt 38 | CMakeFiles 39 | CMakeScripts 40 | Testing 41 | Makefile 42 | cmake_install.cmake 43 | install_manifest.txt 44 | compile_commands.json 45 | CTestTestfile.cmake 46 | _deps 47 | 48 | ### CLion 49 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 50 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 51 | 52 | # User-specific stuff 53 | .idea/**/workspace.xml 54 | .idea/**/tasks.xml 55 | .idea/**/usage.statistics.xml 56 | .idea/**/dictionaries 57 | .idea/**/shelf 58 | 59 | # AWS User-specific 60 | .idea/**/aws.xml 61 | 62 | # Generated files 63 | .idea/**/contentModel.xml 64 | 65 | # Sensitive or high-churn files 66 | .idea/**/dataSources/ 67 | .idea/**/dataSources.ids 68 | .idea/**/dataSources.local.xml 69 | .idea/**/sqlDataSources.xml 70 | .idea/**/dynamic.xml 71 | .idea/**/uiDesigner.xml 72 | .idea/**/dbnavigator.xml 73 | 74 | # Gradle 75 | .idea/**/gradle.xml 76 | .idea/**/libraries 77 | 78 | # Gradle and Maven with auto-import 79 | # When using Gradle or Maven with auto-import, you should exclude module files, 80 | # since they will be recreated, and may cause churn. Uncomment if using 81 | # auto-import. 82 | # .idea/artifacts 83 | # .idea/compiler.xml 84 | # .idea/jarRepositories.xml 85 | # .idea/modules.xml 86 | # .idea/*.iml 87 | # .idea/modules 88 | # *.iml 89 | # *.ipr 90 | 91 | # CMake 92 | cmake-build-*/ 93 | 94 | # Mongo Explorer plugin 95 | .idea/**/mongoSettings.xml 96 | 97 | # File-based project format 98 | *.iws 99 | 100 | # IntelliJ 101 | out/ 102 | 103 | # mpeltonen/sbt-idea plugin 104 | .idea_modules/ 105 | 106 | # JIRA plugin 107 | atlassian-ide-plugin.xml 108 | 109 | # Cursive Clojure plugin 110 | .idea/replstate.xml 111 | 112 | # SonarLint plugin 113 | .idea/sonarlint/ 114 | 115 | # Crashlytics plugin (for Android Studio and IntelliJ) 116 | com_crashlytics_export_strings.xml 117 | crashlytics.properties 118 | crashlytics-build.properties 119 | fabric.properties 120 | 121 | # Editor-based Rest Client 122 | .idea/httpRequests 123 | 124 | # Android studio 3.1+ serialized cache file 125 | .idea/caches/build_file_checksums.ser 126 | 127 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(rtlog VERSION 1.0.0) 3 | 4 | option(RTLOG_USE_FMTLIB "Use fmtlib for formatting" OFF) 5 | option(RTLOG_FULL_WARNINGS "Enable full warnings" OFF) 6 | option(RTLOG_BUILD_TESTS "Build tests" OFF) 7 | option(RTLOG_BUILD_EXAMPLES "Build examples" OFF) 8 | 9 | 10 | set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY") 11 | include(CheckCXXCompilerFlag) 12 | check_cxx_compiler_flag("-fsanitize=realtime" COMPILER_SUPPORTS_RTSAN) 13 | unset(CMAKE_TRY_COMPILE_TARGET_TYPE) 14 | if(COMPILER_SUPPORTS_RTSAN) 15 | message(STATUS "Compiler supports -fsanitize=realtime, allowing RTLOG_USE_RTSAN option.") 16 | option(RTLOG_USE_RTSAN "Use -fsanitize=realtime" OFF) 17 | endif() 18 | 19 | # Add library header files 20 | set(HEADERS 21 | include/rtlog/rtlog.h 22 | ) 23 | 24 | # Create library target 25 | add_library(rtlog INTERFACE) 26 | add_library(rtlog::rtlog ALIAS rtlog) 27 | 28 | if (CMAKE_CXX_STANDARD LESS 17) 29 | message(WARNING "C++17 or higher is required for rtlog. Setting C++17 for the target..") 30 | endif() 31 | 32 | target_compile_features(rtlog INTERFACE cxx_std_17) 33 | 34 | # Set include directories for library 35 | target_include_directories(rtlog INTERFACE 36 | $ 37 | $ 38 | ) 39 | 40 | # Set project include directories 41 | target_include_directories(${PROJECT_NAME} INTERFACE 42 | $ 43 | $ 44 | ) 45 | 46 | if (NOT TARGET farbot) 47 | include(FetchContent) 48 | 49 | FetchContent_Declare(farbot 50 | GIT_REPOSITORY https://github.com/hogliux/farbot 51 | GIT_TAG 0416705394720c12f0d02e55c144e4f69bb06912 52 | ) 53 | # Note we do not "MakeAvailable" here, because farbot does not fully work via FetchContent 54 | if(NOT farbot_POPULATED) 55 | FetchContent_Populate(farbot) 56 | endif() 57 | add_library(farbot INTERFACE) 58 | add_library(farbot::farbot ALIAS farbot) 59 | 60 | target_include_directories(farbot INTERFACE 61 | $ 62 | $ 63 | ) 64 | endif() 65 | 66 | if(NOT RTSAN_USE_FMTLIB AND NOT TARGET stb::stb) 67 | # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: 68 | if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 69 | cmake_policy(SET CMP0135 NEW) 70 | endif() 71 | 72 | include(FetchContent) 73 | FetchContent_Declare(stb 74 | GIT_REPOSITORY https://github.com/nothings/stb 75 | ) 76 | # Note we do not "MakeAvailable" here, because stb is not cmake, just populate 77 | if(NOT stb_POPULATED) 78 | FetchContent_Populate(stb) 79 | endif() 80 | 81 | add_library(stb INTERFACE) 82 | add_library(stb::stb ALIAS stb) 83 | 84 | target_include_directories(stb INTERFACE 85 | $ 86 | $ 87 | ) 88 | endif() 89 | 90 | if (RTLOG_USE_FMTLIB AND NOT TARGET fmt::fmt) 91 | include(FetchContent) 92 | 93 | FetchContent_Declare(fmtlib 94 | GIT_REPOSITORY https://github.com/fmtlib/fmt 95 | GIT_TAG 11.0.2 96 | ) 97 | FetchContent_MakeAvailable(fmtlib) 98 | endif() 99 | 100 | target_link_libraries(rtlog 101 | INTERFACE 102 | farbot::farbot 103 | stb::stb 104 | $<$:fmt::fmt> 105 | ) 106 | 107 | target_compile_definitions(rtlog 108 | INTERFACE 109 | STB_SPRINTF_IMPLEMENTATION 110 | $<$:RTLOG_USE_FMTLIB> 111 | $<$>:RTLOG_USE_STB> 112 | $<$:DEBUG> 113 | $<$:NDEBUG> 114 | ) 115 | 116 | set(RTLOG_ALL_WARNINGS) 117 | 118 | if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 119 | set(RTLOG_ALL_WARNINGS "-Wall;-Werror;-Wformat;-Wextra;-Wformat-security;-Wno-unused-function") 120 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 121 | set(RTLOG_ALL_WARNINGS "/W4;/WX;/wd4505") 122 | endif() 123 | 124 | target_compile_options(rtlog 125 | INTERFACE 126 | $<$:${RTLOG_ALL_WARNINGS}> 127 | $<$:-fsanitize=realtime> 128 | ) 129 | 130 | target_link_options(rtlog 131 | INTERFACE 132 | $<$:-fsanitize=realtime> 133 | ) 134 | 135 | if(RTLOG_BUILD_TESTS) 136 | include(CTest) 137 | set_property(GLOBAL PROPERTY CTEST_TARGETS_ADDED 1) 138 | enable_testing() 139 | add_subdirectory(test) 140 | endif() 141 | 142 | if(RTLOG_BUILD_EXAMPLES) 143 | add_subdirectory(examples) 144 | endif() 145 | 146 | # TODO: figure out installing 147 | # Install library 148 | #install(TARGETS rtlog 149 | # EXPORT rtlogTargets 150 | # INCLUDES DESTINATION include 151 | #) 152 | # 153 | ## Install library header files 154 | #install(DIRECTORY include/rtlog 155 | # DESTINATION include 156 | #) 157 | # 158 | ## Install CMake config files 159 | #install(EXPORT rtlogTargets 160 | # FILE rtlogConfig.cmake 161 | # NAMESPACE rtlog:: 162 | # DESTINATION lib/cmake/rtlog 163 | #) 164 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # My License: 2 | This license applies to all code in this repo, except for third party libraries. If you use this code in your project, please send me an email and tell me! I'd love to hear about it being used :) 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2023 Chris Apple 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | 27 | # ReaderWriterQueue license: 28 | This license applies to all the code in this repository except that written by third parties, namely the files in benchmarks/ext, which have their own licenses, and Jeff Preshing's semaphore implementation (used in the blocking queues) which has a zlib license (embedded in atomicops.h). 29 | 30 | Simplified BSD License: 31 | 32 | Copyright (c) 2013-2021, Cameron Desrochers 33 | All rights reserved. 34 | 35 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 36 | 37 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 38 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 39 | 40 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 41 | 42 | 43 | # Doctest license: 44 | The MIT License (MIT) 45 | 46 | Copyright (c) 2016-2023 Viktor Kirilov 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy 49 | of this software and associated documentation files (the "Software"), to deal 50 | in the Software without restriction, including without limitation the rights 51 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 52 | copies of the Software, and to permit persons to whom the Software is 53 | furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in all 56 | copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 64 | SOFTWARE. 65 | 66 | # Libfmt license: 67 | Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors 68 | 69 | Permission is hereby granted, free of charge, to any person obtaining 70 | a copy of this software and associated documentation files (the 71 | "Software"), to deal in the Software without restriction, including 72 | without limitation the rights to use, copy, modify, merge, publish, 73 | distribute, sublicense, and/or sell copies of the Software, and to 74 | permit persons to whom the Software is furnished to do so, subject to 75 | the following conditions: 76 | 77 | The above copyright notice and this permission notice shall be 78 | included in all copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 81 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 82 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 83 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 84 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 85 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 86 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 87 | 88 | --- Optional exception to the license --- 89 | 90 | As an exception, if, as a result of your compiling your source code, portions 91 | of this Software are embedded into a machine-executable object form of such 92 | source code, you may redistribute such embedded portions in such object form 93 | without including the above copyright and permission notices. 94 | 95 | STB: 96 | This software is available under 2 licenses -- choose whichever you prefer. 97 | ------------------------------------------------------------------------------ 98 | ALTERNATIVE A - MIT License 99 | Copyright (c) 2017 Sean Barrett 100 | Permission is hereby granted, free of charge, to any person obtaining a copy of 101 | this software and associated documentation files (the "Software"), to deal in 102 | the Software without restriction, including without limitation the rights to 103 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 104 | of the Software, and to permit persons to whom the Software is furnished to do 105 | so, subject to the following conditions: 106 | The above copyright notice and this permission notice shall be included in all 107 | copies or substantial portions of the Software. 108 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 109 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 110 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 111 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 112 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 113 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 114 | SOFTWARE. 115 | ------------------------------------------------------------------------------ 116 | ALTERNATIVE B - Public Domain (www.unlicense.org) 117 | This is free and unencumbered software released into the public domain. 118 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 119 | software, either in source code form or as a compiled binary, for any purpose, 120 | commercial or non-commercial, and by any means. 121 | In jurisdictions that recognize copyright laws, the author or authors of this 122 | software dedicate any and all copyright interest in the software to the public 123 | domain. We make this dedication for the benefit of the public at large and to 124 | the detriment of our heirs and successors. We intend this dedication to be an 125 | overt act of relinquishment in perpetuity of all present and future rights to 126 | this software under copyright law. 127 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 128 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 129 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 130 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 131 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 132 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 133 | 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtlog-cpp 🔊 2 | 3 | `rtlog-cpp` is a logging library designed specifically for logging messages from the real-time thread. This is particularly useful in the audio and embedded industries, where hard real-time requirements must be met, and logging in traditional ways from your real-time threads is unacceptable. 4 | 5 | If you're looking for a general use logger, this probably isn't the library for you! 6 | 7 | The design behind this logger was presented at ADCx 2023. 8 | 9 | [![ADCx rtlog presentation on youtube](data/PresentationScreenshot.png)](https://www.youtube.com/watch?v=4KFFMGTQIFM) 10 | 11 | Slides: 12 | ![Slide Title Page](data/SlideTitle.png) 13 | 14 | ## Features 15 | 16 | - Ability to log messages of any type and size from the real-time thread 17 | - Statically allocated memory at compile time, no allocations in the real-time thread 18 | - Support for printf-style format specifiers (using [a version of the printf family](https://github.com/nothings/stb/blob/master/stb_sprintf.h) that doesn't hit the `localeconv` lock) OR support for modern libfmt formatting. 19 | - Efficient thread-safe logging using a [lock free queue](https://github.com/hogliux/farbot). 20 | 21 | ## Requirements 22 | 23 | - A C++17 compatible compiler 24 | - The C++17 standard library 25 | - farbot::fifo (will be downloaded via cmake if not provided) 26 | - stb's vsnprintf (will be downloaded via cmake if not provided) OR libfmt if cmake is run with the `RTSAN_USE_FMTLIB` option 27 | 28 | ## Installation via CMake 29 | 30 | In CMakeLists.txt 31 | ```cmake 32 | include(FetchContent) 33 | FetchContent_Declare(rtlog-cpp 34 | GIT_REPOSITORY https://github.com/cjappl/rtlog-cpp 35 | ) 36 | FetchContent_MakeAvailable(rtlog-cpp) 37 | 38 | add_executable(audioapp ${SOURCES}) 39 | target_link_libraries(audioapp 40 | PRIVATE 41 | rtlog::rtlog 42 | ) 43 | ``` 44 | 45 | To use formatlib, set the variable, either on the command line or in cmake: 46 | ```bash 47 | cmake .. -DRTLOG_USE_FMTLIB=ON 48 | ``` 49 | 50 | ## Usage 51 | 52 | For more fleshed out fully running examples check out `examples/` and `test/` 53 | 54 | After including via cmake: 55 | 56 | 1. Include the `rtlog/rtlog.h` header file in your source code 57 | 2. Create a `rtlog::Logger` object with the desired template parameters: 58 | 3. Process the log messages on your own thread, or via the provided `rtlog::LogProcessingThread` 59 | 60 | ```c++ 61 | #include 62 | 63 | struct ExampleLogData 64 | { 65 | ExampleLogLevel level; 66 | ExampleLogRegion region; 67 | }; 68 | 69 | constexpr auto MAX_LOG_MESSAGE_LENGTH = 256; 70 | constexpr auto MAX_NUM_LOG_MESSAGES = 100; 71 | 72 | std::atomic gSequenceNumber{0}; 73 | 74 | using RealtimeLogger = rtlog::Logger; 75 | 76 | ... 77 | 78 | RealtimeLogger logger; 79 | 80 | void SomeRealtimeCallback() 81 | { 82 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Audio}, "Hello, world! %i", 42); 83 | 84 | // using RTSAN_USE_LIBFMT 85 | logger.Log({ExampleLogData::Debug, ExampleLogRegion::Audio, FMT_STRING("Hello, world! {}", 42); 86 | } 87 | 88 | ... 89 | 90 | ``` 91 | 92 | To process the logs in another thread, call `PrintAndClearLogQueue` with a function to call on the output data. 93 | 94 | ```c++ 95 | 96 | static auto PrintMessage = [](const ExampleLogData& data, size_t sequenceNumber, const char* fstring, ...) __attribute__ ((format (printf, 4, 5))) 97 | { 98 | std::array buffer; 99 | 100 | va_list args; 101 | va_start(args, fstring); 102 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 103 | va_end(args); 104 | 105 | printf("{%lu} [%s] (%s): %s\n", 106 | sequenceNumber, 107 | rtlog::test::to_string(data.level), 108 | rtlog::test::to_string(data.region), 109 | buffer.data()); 110 | }; 111 | 112 | ... 113 | 114 | void LogProcessorThreadMain() 115 | { 116 | while (running) 117 | { 118 | if (logger.PrintAndClearLogQueue(PrintMessage) == 0) 119 | std::this_thread::sleep_for(std::chrono::milliseconds(10); 120 | } 121 | } 122 | 123 | ``` 124 | 125 | Or alternatively spin up a `rtlog::LogProcessingThread` 126 | 127 | ```c++ 128 | rtlog::LogProcessingThread thread(logger, PrintMessage, std::chrono::milliseconds(10)); 129 | ``` 130 | 131 | ## Customizing the queue type 132 | 133 | rtlog provides two queue type variants: `rtlog::SingleRealtimeWriterQueueType` (SPSC - default) and `rtlog::MultiRealtimeWriterQueueType` (MPSC). It is always assummed that you have one log printing thread. These may be used by specifying them: 134 | 135 | ```cpp 136 | using SingleWriterRtLoggerType = rtlog::Logger; 137 | 138 | SingleWriterRtLoggerType logger; 139 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Audio}, "Hello, world! %i", 42); 140 | 141 | ... 142 | 143 | using MultiWriterRtLoggerType = rtlog::Logger; 144 | 145 | MultiWriterRtLoggerType logger; 146 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Audio}, "Hello, world! %i", 42); 147 | ``` 148 | 149 | If you don't want to use either of these defaults, you may provide your own queue. 150 | 151 | ** IT IS UP TO YOU TO ENSURE THE QUEUE YOU PROVIDE IS LOCK-FREE AND REAL-TIME SAFE ** 152 | 153 | The queue must have the following: 154 | ```c++ 155 | template 156 | class MyQueue 157 | { 158 | public: 159 | using value_type = T; 160 | 161 | MyQueue(int capacity); 162 | bool try_dequeue(T& item); // MUST return false if the queue is empty 163 | 164 | bool try_enqueue(T&& item); 165 | // OR 166 | bool try_enqueue(const T& item); 167 | }; 168 | ``` 169 | 170 | Then, when creating the logger, provide the queue type as a template parameter: 171 | 172 | ```c++ 173 | using RealtimeLogger = rtlog::Logger; 174 | ``` 175 | 176 | You can see an example of wrapping a known rt-safe queue in `examples/custom_queue_example`. 177 | -------------------------------------------------------------------------------- /data/PresentationScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjappl/rtlog-cpp/36381f9b2d64f7852590dd0c2564dbfdee90e116/data/PresentationScreenshot.png -------------------------------------------------------------------------------- /data/SlideTitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjappl/rtlog-cpp/36381f9b2d64f7852590dd0c2564dbfdee90e116/data/SlideTitle.png -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(everlog) 2 | add_subdirectory(custom_queue_example) 3 | -------------------------------------------------------------------------------- /examples/custom_queue_example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(ReaderWriterQueue QUIET) 2 | if(NOT TARGET readerwriterqueue) 3 | include(FetchContent) 4 | FetchContent_Declare(ReaderWriterQueue 5 | GIT_REPOSITORY https://github.com/cameron314/readerwriterqueue 6 | ) 7 | FetchContent_MakeAvailable(ReaderWriterQueue) 8 | endif() 9 | 10 | add_executable(custom_queue_example 11 | customqueuemain.cpp 12 | ) 13 | 14 | target_link_libraries(custom_queue_example 15 | PRIVATE 16 | rtlog::rtlog 17 | readerwriterqueue 18 | ) 19 | -------------------------------------------------------------------------------- /examples/custom_queue_example/customqueuemain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | template class CustomQueue { 6 | 7 | // technically we could use readerwriterqueue "unwrapped" but showing this off 8 | // in the CustomQueue wrapper for documentation purposes 9 | moodycamel::ReaderWriterQueue mQueue; 10 | 11 | public: 12 | using value_type = T; 13 | 14 | CustomQueue(int capacity) : mQueue(capacity) {} 15 | 16 | bool try_enqueue(T &&item) { return mQueue.try_enqueue(std::move(item)); } 17 | bool try_dequeue(T &item) { return mQueue.try_dequeue(item); } 18 | }; 19 | 20 | struct LogData {}; 21 | 22 | std::atomic gSequenceNumber{0}; 23 | 24 | int main() { 25 | rtlog::Logger logger; 26 | logger.Log({}, "Hello, World!"); 27 | 28 | logger.PrintAndClearLogQueue( 29 | [](const LogData &data, size_t sequenceNumber, const char *fstring, ...) { 30 | (void)data; 31 | std::array buffer; 32 | 33 | va_list args; 34 | va_start(args, fstring); 35 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 36 | va_end(args); 37 | 38 | printf("{%zu} %s\n", sequenceNumber, buffer.data()); 39 | }); 40 | 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /examples/everlog/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(everlog 2 | everlogmain.cpp 3 | ) 4 | 5 | target_link_libraries(everlog 6 | PRIVATE 7 | rtlog::rtlog 8 | ) 9 | 10 | target_compile_definitions(everlog 11 | PRIVATE 12 | $<$:RTLOG_HAS_PTHREADS> 13 | ) 14 | -------------------------------------------------------------------------------- /examples/everlog/everlogmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace evr { 6 | constexpr auto MAX_LOG_MESSAGE_LENGTH = 256; 7 | constexpr auto MAX_NUM_LOG_MESSAGES = 128; 8 | 9 | enum class LogLevel { Debug, Info, Warning, Critical }; 10 | 11 | const char *to_string(LogLevel level) { 12 | switch (level) { 13 | case LogLevel::Debug: 14 | return "DEBG"; 15 | case LogLevel::Info: 16 | return "INFO"; 17 | case LogLevel::Warning: 18 | return "WARN"; 19 | case LogLevel::Critical: 20 | return "CRIT"; 21 | default: 22 | return "Unknown"; 23 | } 24 | } 25 | 26 | enum class LogRegion { Engine, Game, Network, Audio }; 27 | 28 | const char *to_string(LogRegion region) { 29 | switch (region) { 30 | case LogRegion::Engine: 31 | return "ENGIN"; 32 | case LogRegion::Game: 33 | return "GAME "; 34 | case LogRegion::Network: 35 | return "NETWK"; 36 | case LogRegion::Audio: 37 | return "AUDIO"; 38 | default: 39 | return "UNKWN"; 40 | } 41 | } 42 | 43 | struct LogData { 44 | LogLevel level; 45 | LogRegion region; 46 | }; 47 | 48 | class PrintMessageFunctor { 49 | public: 50 | explicit PrintMessageFunctor(const std::string &filename) : mFile(filename) { 51 | mFile.open(filename); 52 | mFile.clear(); 53 | } 54 | 55 | ~PrintMessageFunctor() { 56 | if (mFile.is_open()) { 57 | mFile.close(); 58 | } 59 | } 60 | 61 | PrintMessageFunctor(const PrintMessageFunctor &) = delete; 62 | PrintMessageFunctor(PrintMessageFunctor &&) = delete; 63 | PrintMessageFunctor &operator=(const PrintMessageFunctor &) = delete; 64 | PrintMessageFunctor &operator=(PrintMessageFunctor &&) = delete; 65 | 66 | void operator()(const LogData &data, size_t sequenceNumber, 67 | const char *fstring, ...) { 68 | std::array buffer; 69 | 70 | va_list args; 71 | va_start(args, fstring); 72 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 73 | va_end(args); 74 | 75 | printf("{%zu} [%s] (%s): %s\n", sequenceNumber, to_string(data.level), 76 | to_string(data.region), buffer.data()); 77 | 78 | mFile << "{" << sequenceNumber << "} [" << to_string(data.level) << "] (" 79 | << to_string(data.region) << "): " << buffer.data() << std::endl; 80 | } 81 | std::ofstream mFile; 82 | }; 83 | 84 | static PrintMessageFunctor PrintMessage("everlog.txt"); 85 | 86 | template 87 | void RealtimeBusyWait(int milliseconds, LoggerType &logger) { 88 | auto start = std::chrono::high_resolution_clock::now(); 89 | while (true) { 90 | auto now = std::chrono::high_resolution_clock::now(); 91 | auto elapsed = 92 | std::chrono::duration_cast(now - start); 93 | if (elapsed.count() >= milliseconds) { 94 | logger.Log({LogLevel::Debug, LogRegion::Engine}, "Done!!"); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | } // namespace evr 101 | 102 | using namespace evr; 103 | 104 | std::atomic gRunning{true}; 105 | 106 | std::atomic gSequenceNumber{0}; 107 | static rtlog::Logger 109 | gRealtimeLogger; 110 | 111 | #define EVR_LOG_DEBUG(Region, fstring, ...) \ 112 | PrintMessage({LogLevel::Debug, Region}, ++gSequenceNumber, fstring, \ 113 | ##__VA_ARGS__) 114 | #define EVR_LOG_INFO(Region, fstring, ...) \ 115 | PrintMessage({LogLevel::Info, Region}, ++gSequenceNumber, fstring, \ 116 | ##__VA_ARGS__) 117 | #define EVR_LOG_WARNING(Region, fstring, ...) \ 118 | PrintMessage({LogLevel::Warning, Region}, ++gSequenceNumber, fstring, \ 119 | ##__VA_ARGS__) 120 | #define EVR_LOG_CRITICAL(Region, ...) \ 121 | PrintMessage({LogLevel::Critical, Region}, ++gSequenceNumber, fstring, \ 122 | ##__VA_ARGS__) 123 | 124 | #ifdef RTLOG_USE_STB 125 | #define EVR_RTLOG_DEBUG(Region, fstring, ...) \ 126 | gRealtimeLogger.Log({LogLevel::Debug, Region}, fstring, ##__VA_ARGS__) 127 | #define EVR_RTLOG_INFO(Region, fstring, ...) \ 128 | gRealtimeLogger.Log({LogLevel::Info, Region}, fstring, ##__VA_ARGS__) 129 | #define EVR_RTLOG_WARNING(Region, fstring, ...) \ 130 | gRealtimeLogger.Log({LogLevel::Warning, Region}, fstring, ##__VA_ARGS__) 131 | #define EVR_RTLOG_CRITICAL(Region, fstring, ...) \ 132 | gRealtimeLogger.Log({LogLevel::Critical, Region}, fstring, ##__VA_ARGS__) 133 | #else 134 | #define EVR_RTLOG_DEBUG(Region, fstring, ...) (void)0 135 | #define EVR_RTLOG_INFO(Region, fstring, ...) (void)0 136 | #define EVR_RTLOG_WARNING(Region, fstring, ...) (void)0 137 | #define EVR_RTLOG_CRITICAL(Region, fstring, ...) (void)0 138 | #endif // RTLOG_USE_STB 139 | 140 | #ifdef RTLOG_USE_FMTLIB 141 | 142 | #define EVR_RTLOG_FMT_DEBUG(Region, fstring, ...) \ 143 | gRealtimeLogger.Log({LogLevel::Debug, Region}, FMT_STRING(fstring), \ 144 | ##__VA_ARGS__) 145 | #define EVR_RTLOG_FMT_INFO(Region, fstring, ...) \ 146 | gRealtimeLogger.Log({LogLevel::Info, Region}, FMT_STRING(fstring), \ 147 | ##__VA_ARGS__) 148 | #define EVR_RTLOG_FMT_WARNING(Region, fstring, ...) \ 149 | gRealtimeLogger.Log({LogLevel::Warning, Region}, FMT_STRING(fstring), \ 150 | ##__VA_ARGS__) 151 | #define EVR_RTLOG_FMT_CRITICAL(Region, fstring, ...) \ 152 | gRealtimeLogger.Log({LogLevel::Critical, Region}, FMT_STRING(fstring), \ 153 | ##__VA_ARGS__) 154 | 155 | #else 156 | 157 | // define the above macros as no-ops 158 | #define EVR_RTLOG_FMT_DEBUG(Region, fstring, ...) (void)0 159 | #define EVR_RTLOG_FMT_INFO(Region, fstring, ...) (void)0 160 | #define EVR_RTLOG_FMT_WARNING(Region, fstring, ...) (void)0 161 | #define EVR_RTLOG_FMT_CRITICAL(Region, fstring, ...) (void)0 162 | 163 | #endif // RTLOG_USE_FMTLIB 164 | 165 | int main() { 166 | EVR_LOG_INFO(LogRegion::Network, "Hello from main thread!"); 167 | 168 | rtlog::LogProcessingThread thread(gRealtimeLogger, PrintMessage, 169 | std::chrono::milliseconds(10)); 170 | 171 | std::thread realtimeThread{[&]() { 172 | while (gRunning) { 173 | for (int i = 99; i >= 0; i--) { 174 | EVR_RTLOG_DEBUG(LogRegion::Audio, "Hello %d from rt-thread", i); 175 | EVR_RTLOG_FMT_WARNING(LogRegion::Audio, 176 | "Hello {} from rt-thread - logging with {}", i, 177 | "libfmt"); 178 | RealtimeBusyWait(10, gRealtimeLogger); 179 | } 180 | } 181 | }}; 182 | 183 | std::thread nonRealtimeThread{[]() { 184 | while (gRunning) { 185 | for (int i = 0; i < 100; i++) { 186 | EVR_LOG_WARNING(LogRegion::Network, "Hello %d from non-rt-thread", i); 187 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); 188 | } 189 | } 190 | }}; 191 | 192 | realtimeThread.join(); 193 | nonRealtimeThread.join(); 194 | return 0; 195 | } 196 | 197 | void signalHandler() { gRunning = false; } 198 | -------------------------------------------------------------------------------- /include/rtlog/rtlog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #if !defined(RTLOG_USE_FMTLIB) && !defined(RTLOG_USE_STB) 12 | // The default behavior to match legacy behavior is to use STB 13 | #define RTLOG_USE_STB 14 | #endif 15 | 16 | #ifdef RTLOG_USE_FMTLIB 17 | #include 18 | #endif // RTLOG_USE_FMTLIB 19 | 20 | #include 21 | 22 | #ifdef RTLOG_USE_STB 23 | #ifndef STB_SPRINTF_IMPLEMENTATION 24 | #define STB_SPRINTF_IMPLEMENTATION 25 | #endif 26 | 27 | #ifndef STB_SPRINTF_STATIC 28 | #define STB_SPRINTF_STATIC 29 | #endif 30 | 31 | #include 32 | #endif // RTLOG_USE_STB 33 | 34 | #if defined(__has_feature) 35 | #if __has_feature(realtime_sanitizer) 36 | #define RTLOG_NONBLOCKING [[clang::nonblocking]] 37 | #endif 38 | #endif 39 | 40 | #ifndef RTLOG_NONBLOCKING 41 | #define RTLOG_NONBLOCKING 42 | #endif 43 | 44 | #if defined(__GNUC__) || defined(__clang__) 45 | #define RTLOG_ATTRIBUTE_FORMAT __attribute__((format(printf, 3, 4))) 46 | #else 47 | #define RTLOG_ATTRIBUTE_FORMAT 48 | #endif 49 | 50 | namespace rtlog { 51 | 52 | enum class Status { 53 | Success = 0, 54 | 55 | Error_QueueFull = 1, 56 | Error_MessageTruncated = 2, 57 | }; 58 | 59 | namespace detail { 60 | 61 | template struct BasicLogData { 62 | LogData mLogData{}; 63 | size_t mSequenceNumber{}; 64 | std::array mMessage{}; 65 | }; 66 | 67 | template 68 | struct has_try_enqueue_by_move : std::false_type {}; 69 | 70 | template 71 | struct has_try_enqueue_by_move< 72 | T, std::void_t().try_enqueue( 73 | std::declval()))>> : std::true_type {}; 74 | 75 | template 76 | inline constexpr bool has_try_enqueue_by_move_v = 77 | has_try_enqueue_by_move::value; 78 | 79 | template 80 | struct has_try_enqueue_by_value : std::false_type {}; 81 | 82 | template 83 | struct has_try_enqueue_by_value< 84 | T, std::void_t().try_enqueue( 85 | std::declval()))>> : std::true_type { 86 | }; 87 | 88 | template 89 | inline constexpr bool has_try_enqueue_by_value_v = 90 | has_try_enqueue_by_value::value; 91 | 92 | template 93 | inline constexpr bool has_try_enqueue_v = 94 | has_try_enqueue_by_move_v || has_try_enqueue_by_value_v; 95 | 96 | template 97 | struct has_try_dequeue : std::false_type {}; 98 | 99 | template 100 | struct has_try_dequeue().try_dequeue( 101 | std::declval()))>> 102 | : std::true_type {}; 103 | 104 | template 105 | inline constexpr bool has_try_dequeue_v = has_try_dequeue::value; 106 | 107 | template 108 | struct has_value_type : std::false_type {}; 109 | 110 | template 111 | struct has_value_type> : std::true_type { 112 | }; 113 | 114 | template 115 | inline constexpr bool has_value_type_v = has_value_type::value; 116 | 117 | template 118 | struct has_int_constructor : std::false_type {}; 119 | 120 | template 121 | struct has_int_constructor()))>> 122 | : std::true_type {}; 123 | 124 | template 125 | inline constexpr bool has_int_constructor_v = has_int_constructor::value; 126 | } // namespace detail 127 | 128 | template 130 | class FarbotFifoType { 131 | farbot::fifo // producer_failure_mode 137 | 138 | mQueue; 139 | 140 | public: 141 | using value_type = T; 142 | 143 | FarbotFifoType(int capacity) : mQueue(capacity) {} 144 | 145 | bool try_enqueue(T &&item) { return mQueue.push(std::move(item)); } 146 | bool try_dequeue(T &item) { return mQueue.pop(item); } 147 | }; 148 | 149 | template 150 | using SingleRealtimeWriterQueueType = 151 | FarbotFifoType; 154 | 155 | // NOTE: This version overwrites on full, which is a requirement to make writing 156 | // real-time safe. 157 | // This means it will never report Error_QueueFull. 158 | template 159 | using MultiRealtimeWriterQueueType = FarbotFifoType< 160 | T, farbot::fifo_options::concurrency::multiple, 161 | farbot::fifo_options::full_empty_failure_mode::overwrite_or_return_default>; 162 | 163 | /** 164 | * @brief A logger class for logging messages. 165 | * This class allows you to log messages of type LogData. 166 | * This type is user defined, and is often the additional data outside the 167 | * format string you want to log. For instance: The log level, the log region, 168 | * the file name, the line number, etc. See examples or tests for some ideas. 169 | * 170 | * @tparam LogData The type of the data to be logged. 171 | * @tparam MaxNumMessages The maximum number of messages that can be enqueud at 172 | * once. If this number is exceeded, the logger will return an error. 173 | * @tparam MaxMessageLength The maximum length of each message. Messages longer 174 | * than this will be truncated and still enqueued 175 | * @tparam SequenceNumber This number is incremented when the message is 176 | * enqueued. It is assumed that your non-realtime logger increments and logs it 177 | * on Log. 178 | * @tparam QType is the configurable underlying queue. By default it is a SPSC 179 | * queue from moodycamel. WARNING! It is up to the user to ensure this queue 180 | * type is real-time safe!! 181 | * 182 | * Requirements on QType: 183 | * 1. Is real-time safe 184 | * 2. Accepts one type template paramter for the type to be queued 185 | * 3. Has a constructor that takes an integer which will be the queue's 186 | * capacity 187 | * 4. Has methods `bool try_enqueue(T &&item)` and/or `bool 188 | * try_enqueue(const T &item)` and `bool try_dequeue(T &item)` 189 | */ 190 | template &SequenceNumber, 192 | template class QType = SingleRealtimeWriterQueueType> 193 | class Logger { 194 | public: 195 | using InternalLogData = detail::BasicLogData; 196 | using InternalQType = QType; 197 | 198 | static_assert(MaxNumMessages > 0); 199 | static_assert((MaxNumMessages & (MaxNumMessages - 1)) == 0, 200 | "MaxNumMessages must be a power of 2"); 201 | static_assert( 202 | detail::has_int_constructor_v, 203 | "QType must have a constructor that takes an int - `QType(int)`"); 204 | static_assert(detail::has_value_type_v, 205 | "QType must have a value_type - `using value_type = T;`"); 206 | static_assert(detail::has_try_enqueue_v, 207 | "QType must have a try_enqueue method - `bool try_enqueue(T " 208 | "&&item)` and/or `bool try_enqueue(const T &item)`"); 209 | static_assert( 210 | detail::has_try_dequeue_v, 211 | "QType must have a try_dequeue method - `bool try_dequeue(T &item)`"); 212 | 213 | /* 214 | * @brief Logs a message with the given format and input data. 215 | * 216 | * REALTIME SAFE; you are supposed to allocate va_list in realtime safe 217 | * manner, or expect that the system does not allocate va_args. 218 | * 219 | * This function logs a message with the given format and input data. The 220 | * format is specified using printf-style format specifiers. It's highly 221 | * recommended you use and respect -Wformat to ensure your format specifiers 222 | * are correct. 223 | * 224 | * To actually process the log messages (print, write to file, etc) you must 225 | * call PrintAndClearLogQueue. 226 | * 227 | * @param inputData The data to be logged. 228 | * @param format The printf-style format specifiers for the message. 229 | * @param args The variable arguments to the printf-style format specifiers. 230 | * @return Status A Status value indicating whether the logging operation was 231 | * successful. 232 | * 233 | * This function attempts to enqueue the log message regardless of whether the 234 | * message was truncated due to being too long for the buffer. If the message 235 | * queue is full, the function returns `Status::Error_QueueFull`. If the 236 | * message was truncated, the function returns 237 | * `Status::Error_MessageTruncated`. Otherwise, it returns `Status::Success`. 238 | */ 239 | #ifdef RTLOG_USE_STB 240 | Status Logv(LogData &&inputData, const char *format, 241 | va_list args) noexcept RTLOG_NONBLOCKING { 242 | auto retVal = Status::Success; 243 | 244 | InternalLogData dataToQueue; 245 | dataToQueue.mLogData = std::forward(inputData); 246 | dataToQueue.mSequenceNumber = 247 | SequenceNumber.fetch_add(1, std::memory_order_relaxed); 248 | 249 | const auto charsPrinted = stbsp_vsnprintf( 250 | dataToQueue.mMessage.data(), 251 | static_cast(dataToQueue.mMessage.size()), format, args); 252 | 253 | if (charsPrinted < 0 || 254 | static_cast(charsPrinted) >= dataToQueue.mMessage.size()) 255 | retVal = Status::Error_MessageTruncated; 256 | 257 | // Even if the message was truncated, we still try to enqueue it to minimize 258 | // data loss 259 | const bool dataWasEnqueued = mQueue.try_enqueue(std::move(dataToQueue)); 260 | 261 | if (!dataWasEnqueued) 262 | retVal = Status::Error_QueueFull; 263 | 264 | return retVal; 265 | } 266 | 267 | /* 268 | * @brief Logs a message with the given format and input data. 269 | * 270 | * REALTIME SAFE - except on systems where va_args allocates 271 | * 272 | * This function logs a message with the given format and input data. The 273 | * format is specified using printf-style format specifiers. It's highly 274 | * recommended you use and respect -Wformat to ensure your format specifiers 275 | * are correct. 276 | * 277 | * To actually process the log messages (print, write to file, etc) you must 278 | * call PrintAndClearLogQueue. 279 | * 280 | * @param inputData The data to be logged. 281 | * @param format The printf-style format specifiers for the message. 282 | * @param ... The variable arguments to the printf-style format specifiers. 283 | * @return Status A Status value indicating whether the logging operation was 284 | * successful. 285 | * 286 | * This function attempts to enqueue the log message regardless of whether the 287 | * message was truncated due to being too long for the buffer. If the message 288 | * queue is full, the function returns `Status::Error_QueueFull`. If the 289 | * message was truncated, the function returns 290 | * `Status::Error_MessageTruncated`. Otherwise, it returns `Status::Success`. 291 | */ 292 | Status Log(LogData &&inputData, const char *format, 293 | ...) noexcept RTLOG_NONBLOCKING RTLOG_ATTRIBUTE_FORMAT { 294 | va_list args; 295 | va_start(args, format); 296 | auto retVal = Logv(std::move(inputData), format, args); 297 | va_end(args); 298 | return retVal; 299 | } 300 | #endif // RTLOG_USE_STB 301 | 302 | #ifdef RTLOG_USE_FMTLIB 303 | 304 | /** 305 | * @brief Logs a message with the given format string and input data. 306 | * 307 | * REALTIME SAFE ON ALL SYSTEMS! 308 | * 309 | * This function logs a message using a format string and input data, similar 310 | * to the `Log` function. However, instead of printf-style format specifiers, 311 | * this function uses the format specifiers of the {fmt} library. Because the 312 | * variadic template is resolved at compile time, this is guaranteed to be 313 | * realtime safe on all systems. 314 | * 315 | * To actually process the log messages (print, write to file, etc), you must 316 | * call PrintAndClearLogQueue. 317 | * 318 | * @tparam T The types of the arguments to the format specifiers. 319 | * @param inputData The data to be logged. 320 | * @param fmtString The {fmt}-style format string for the message. 321 | * @param args The arguments to the format specifiers. 322 | * @return Status A Status value indicating whether the logging operation was 323 | * successful. 324 | * 325 | * This function attempts to enqueue the log message regardless of whether the 326 | * message was truncated due to being too long for the buffer. If the message 327 | * queue is full, the function returns `Status::Error_QueueFull`. If the 328 | * message was truncated, the function returns 329 | * `Status::Error_MessageTruncated`. Otherwise, it returns `Status::Success`. 330 | */ 331 | template 332 | Status Log(LogData &&inputData, fmt::format_string fmtString, 333 | T &&...args) noexcept RTLOG_NONBLOCKING { 334 | auto retVal = Status::Success; 335 | 336 | InternalLogData dataToQueue; 337 | dataToQueue.mLogData = std::forward(inputData); 338 | dataToQueue.mSequenceNumber = 339 | SequenceNumber.fetch_add(1, std::memory_order_relaxed); 340 | 341 | const auto maxMessageLength = 342 | dataToQueue.mMessage.size() - 1; // Account for null terminator 343 | 344 | const auto result = 345 | fmt::format_to_n(dataToQueue.mMessage.data(), maxMessageLength, 346 | fmtString, std::forward(args)...); 347 | 348 | if (result.size >= dataToQueue.mMessage.size()) { 349 | dataToQueue.mMessage[dataToQueue.mMessage.size() - 1] = '\0'; 350 | retVal = Status::Error_MessageTruncated; 351 | } else 352 | dataToQueue.mMessage[result.size] = '\0'; 353 | 354 | // Even if the message was truncated, we still try to enqueue it to minimize 355 | // data loss 356 | const bool dataWasEnqueued = mQueue.try_enqueue(std::move(dataToQueue)); 357 | 358 | if (!dataWasEnqueued) 359 | retVal = Status::Error_QueueFull; 360 | 361 | return retVal; 362 | }; 363 | 364 | #endif // RTLOG_USE_FMTLIB 365 | 366 | /** 367 | * @brief Processes and prints all queued log data. 368 | * 369 | * ONLY REALTIME SAFE IF printLogFn IS REALTIME SAFE! - not generally the case 370 | * 371 | * This function processes and prints all queued log data. It takes a 372 | * PrintLogFn object as input, which is used to print the log data. 373 | * 374 | * See tests and examples for some ideas on how to use this function. Using 375 | * ctad you often don't need to specify the template parameter. 376 | * 377 | * @tparam PrintLogFn The type of the print log function object. 378 | * @param printLogFn The print log function object to be used to print the log 379 | * data. 380 | * @return int The number of log messages that were processed and printed. 381 | */ 382 | template 383 | int PrintAndClearLogQueue(PrintLogFn &&printLogFn) { 384 | int numProcessed = 0; 385 | 386 | InternalLogData value; 387 | while (mQueue.try_dequeue(value)) { 388 | printLogFn(value.mLogData, value.mSequenceNumber, "%s", 389 | value.mMessage.data()); 390 | numProcessed++; 391 | } 392 | 393 | return numProcessed; 394 | } 395 | 396 | private: 397 | InternalQType mQueue{MaxNumMessages}; 398 | }; 399 | 400 | /** 401 | * @brief A class representing a log processing thread. 402 | * 403 | * This class represents a log processing thread that continuously dequeues log 404 | * data from a LoggerType object and calls a PrintLogFn object to print the log 405 | * data. The wait time between each log processing iteration can be specified in 406 | * milliseconds. 407 | * 408 | * @tparam LoggerType The type of the logger object to be used for log 409 | * processing. 410 | * @tparam PrintLogFn The type of the print log function object. 411 | */ 412 | template class LogProcessingThread { 413 | public: 414 | /** 415 | * @brief Constructs a new LogProcessingThread object. 416 | * 417 | * This constructor creates a new LogProcessingThread object. It takes a 418 | * reference to a LoggerType object, generally assumed to be some 419 | * specialization of rtlog::Logger, a reference to a PrintLogFn object, and a 420 | * wait time in ms 421 | * 422 | * On construction, the LogProcessingThread will start a thread that will 423 | * continually dequeue the messages from the logger and call printFn on them. 424 | * 425 | * You must call Stop() to stop the thread and join it before your logger goes 426 | * out of scope! Otherwise it's a use-after-free 427 | * 428 | * See tests and examples for some ideas on how to use this class. Using ctad 429 | * you often don't need to specify the template parameters. 430 | * 431 | * @param logger The logger object to be used for log processing. 432 | * @param printFn The print log function object to be used to print the log 433 | * data. 434 | * @param waitTime The time to wait between each log processing iteration. 435 | */ 436 | LogProcessingThread(LoggerType &logger, PrintLogFn &printFn, 437 | std::chrono::milliseconds waitTime) 438 | : mPrintFn(printFn), mLogger(logger), mWaitTime(waitTime) { 439 | mThread = std::thread(&LogProcessingThread::ThreadMain, this); 440 | } 441 | 442 | ~LogProcessingThread() { 443 | if (mThread.joinable()) { 444 | Stop(); 445 | mThread.join(); 446 | } 447 | } 448 | 449 | void Stop() { mShouldRun.store(false); } 450 | 451 | LogProcessingThread(const LogProcessingThread &) = delete; 452 | LogProcessingThread &operator=(const LogProcessingThread &) = delete; 453 | LogProcessingThread(LogProcessingThread &&) = delete; 454 | LogProcessingThread &operator=(LogProcessingThread &&) = delete; 455 | 456 | private: 457 | void ThreadMain() { 458 | while (mShouldRun.load()) { 459 | 460 | if (mLogger.PrintAndClearLogQueue(mPrintFn) == 0) 461 | std::this_thread::sleep_for(mWaitTime); 462 | 463 | std::this_thread::sleep_for(mWaitTime); 464 | } 465 | 466 | mLogger.PrintAndClearLogQueue(mPrintFn); 467 | } 468 | 469 | PrintLogFn &mPrintFn{}; 470 | LoggerType &mLogger{}; 471 | std::thread mThread{}; 472 | std::atomic mShouldRun{true}; 473 | std::chrono::milliseconds mWaitTime{}; 474 | }; 475 | 476 | template 477 | LogProcessingThread(LoggerType &, PrintLogFn) 478 | -> LogProcessingThread; 479 | 480 | } // namespace rtlog 481 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if (NOT TARGET gtest_main) 2 | include(FetchContent) 3 | FetchContent_Declare( 4 | googletest 5 | GIT_REPOSITORY https://github.com/google/googletest.git 6 | GIT_TAG main 7 | ) 8 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 9 | FetchContent_MakeAvailable(googletest) 10 | endif() 11 | 12 | add_executable(rtlog_tests test_rtlog.cpp) 13 | 14 | target_link_libraries(rtlog_tests 15 | PRIVATE 16 | gtest_main 17 | rtlog::rtlog 18 | ) 19 | 20 | set_property(GLOBAL PROPERTY CTEST_TARGETS_ADDED ON) 21 | 22 | include(GoogleTest) 23 | gtest_discover_tests(rtlog_tests) 24 | -------------------------------------------------------------------------------- /test/test_rtlog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace rtlog::test { 6 | 7 | static std::atomic gSequenceNumber{0}; 8 | 9 | constexpr auto MAX_LOG_MESSAGE_LENGTH = 256; 10 | constexpr auto MAX_NUM_LOG_MESSAGES = 128; 11 | 12 | enum class ExampleLogLevel { Debug, Info, Warning, Critical }; 13 | 14 | static const char *to_string(ExampleLogLevel level) { 15 | switch (level) { 16 | case ExampleLogLevel::Debug: 17 | return "DEBG"; 18 | case ExampleLogLevel::Info: 19 | return "INFO"; 20 | case ExampleLogLevel::Warning: 21 | return "WARN"; 22 | case ExampleLogLevel::Critical: 23 | return "CRIT"; 24 | default: 25 | return "Unknown"; 26 | } 27 | } 28 | 29 | enum class ExampleLogRegion { Engine, Game, Network, Audio }; 30 | 31 | static const char *to_string(ExampleLogRegion region) { 32 | switch (region) { 33 | case ExampleLogRegion::Engine: 34 | return "ENGIN"; 35 | case ExampleLogRegion::Game: 36 | return "GAME "; 37 | case ExampleLogRegion::Network: 38 | return "NETWK"; 39 | case ExampleLogRegion::Audio: 40 | return "AUDIO"; 41 | default: 42 | return "UNKWN"; 43 | } 44 | } 45 | 46 | struct ExampleLogData { 47 | ExampleLogLevel level; 48 | ExampleLogRegion region; 49 | }; 50 | 51 | static auto PrintMessage = [](const ExampleLogData &data, size_t sequenceNumber, 52 | const char *fstring, ...) { 53 | std::array buffer; 54 | // print fstring and the varargs into a std::string 55 | va_list args; 56 | va_start(args, fstring); 57 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 58 | va_end(args); 59 | 60 | printf("{%zu} [%s] (%s): %s\n", sequenceNumber, 61 | rtlog::test::to_string(data.level), 62 | rtlog::test::to_string(data.region), buffer.data()); 63 | }; 64 | 65 | } // namespace rtlog::test 66 | 67 | using namespace rtlog::test; 68 | 69 | using SingleWriterRtLoggerType = 70 | rtlog::Logger; 72 | using MultiWriterRtLoggerType = 73 | rtlog::Logger; 75 | 76 | template class RtLogTest : public ::testing::Test { 77 | protected: 78 | LoggerType logger_; 79 | }; 80 | 81 | typedef ::testing::Types 82 | LoggerTypes; 83 | TYPED_TEST_SUITE(RtLogTest, LoggerTypes); 84 | 85 | using TruncatedSingleWriterRtLoggerType = 86 | rtlog::Logger; 88 | using TruncatedMultiWriterRtLoggerType = 89 | rtlog::Logger; 91 | 92 | template 93 | class TruncatedRtLogTest : public ::testing::Test { 94 | protected: 95 | LoggerType logger_; 96 | inline static const size_t maxMessageLength_ = 10; 97 | }; 98 | 99 | typedef ::testing::Types 101 | TruncatedLoggerTypes; 102 | TYPED_TEST_SUITE(TruncatedRtLogTest, TruncatedLoggerTypes); 103 | 104 | #ifdef RTLOG_USE_STB 105 | 106 | TYPED_TEST(RtLogTest, BasicConstruction) { 107 | auto &logger = this->logger_; 108 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 109 | "Hello, world!"); 110 | logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, world!"); 111 | logger.Log({ExampleLogLevel::Warning, ExampleLogRegion::Network}, 112 | "Hello, world!"); 113 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, 114 | "Hello, world!"); 115 | 116 | EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 4); 117 | } 118 | 119 | TYPED_TEST(RtLogTest, VaArgsWorksAsIntended) { 120 | auto &logger = this->logger_; 121 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %lu!", 122 | 123ul); 123 | logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, %f!", 124 | 123.0); 125 | logger.Log({ExampleLogLevel::Warning, ExampleLogRegion::Network}, 126 | "Hello, %lf!", 123.0); 127 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, "Hello, %p!", 128 | (void *)123); 129 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %d!", 130 | 123); 131 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, "Hello, %s!", 132 | "world"); 133 | 134 | EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); 135 | } 136 | 137 | template 138 | void vaArgsTest(LoggerType &&logger, ExampleLogData &&data, const char *format, 139 | ...) { 140 | va_list args; 141 | va_start(args, format); 142 | logger.Logv(std::move(data), format, args); 143 | va_end(args); 144 | } 145 | 146 | TYPED_TEST(RtLogTest, LogvVersionWorks) { 147 | auto &logger = this->logger_; 148 | vaArgsTest(logger, {ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 149 | "Hello, %lu!", 123ul); 150 | vaArgsTest(logger, {ExampleLogLevel::Info, ExampleLogRegion::Game}, 151 | "Hello, %f!", 123.0); 152 | vaArgsTest(logger, {ExampleLogLevel::Warning, ExampleLogRegion::Network}, 153 | "Hello, %lf!", 123.0); 154 | vaArgsTest(logger, {ExampleLogLevel::Critical, ExampleLogRegion::Audio}, 155 | "Hello, %p!", (void *)123); 156 | vaArgsTest(logger, {ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 157 | "Hello, %d!", 123); 158 | vaArgsTest(logger, {ExampleLogLevel::Critical, ExampleLogRegion::Audio}, 159 | "Hello, %s!", "world"); 160 | 161 | EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); 162 | } 163 | 164 | TYPED_TEST(RtLogTest, LoggerThreadDoesItsJob) { 165 | auto &logger = this->logger_; 166 | rtlog::LogProcessingThread thread(logger, PrintMessage, 167 | std::chrono::milliseconds(10)); 168 | 169 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %lu!", 170 | 123ul); 171 | logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, %f!", 172 | 123.0); 173 | logger.Log({ExampleLogLevel::Warning, ExampleLogRegion::Network}, 174 | "Hello, %lf!", 123.0); 175 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, "Hello, %p!", 176 | (void *)123); 177 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %d!", 178 | 123); 179 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, "Hello, %s!", 180 | "world"); 181 | 182 | thread.Stop(); 183 | } 184 | 185 | TYPED_TEST(TruncatedRtLogTest, ErrorsReturnedFromLog) { 186 | auto &logger = this->logger_; 187 | auto maxMessageLength = this->maxMessageLength_; 188 | EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 189 | "Hello, %lu", 12ul), 190 | rtlog::Status::Success); 191 | 192 | EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 193 | "Hello, %lu! xxxxxxxxxxx", 123ul), 194 | rtlog::Status::Error_MessageTruncated); 195 | 196 | // Inspect truncated message 197 | auto InspectLogMessage = [=](const ExampleLogData &data, 198 | size_t sequenceNumber, const char *fstring, 199 | ...) { 200 | (void)sequenceNumber; 201 | EXPECT_EQ(data.level, ExampleLogLevel::Debug); 202 | EXPECT_EQ(data.region, ExampleLogRegion::Engine); 203 | 204 | std::array buffer{}; 205 | va_list args; 206 | va_start(args, fstring); 207 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 208 | va_end(args); 209 | 210 | EXPECT_STREQ(buffer.data(), "Hello, 12"); 211 | EXPECT_EQ(strlen(buffer.data()), maxMessageLength - 1); 212 | }; 213 | EXPECT_EQ(logger.PrintAndClearLogQueue(InspectLogMessage), 2); 214 | } 215 | #endif // RTLOG_USE_STB 216 | 217 | #ifdef RTLOG_USE_FMTLIB 218 | 219 | TYPED_TEST(RtLogTest, FormatLibVersionWorksAsIntended) { 220 | auto &logger = this->logger_; 221 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 222 | FMT_STRING("Hello, {}!"), 123l); 223 | logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, 224 | FMT_STRING("Hello, {}!"), 123.0f); 225 | logger.Log({ExampleLogLevel::Warning, ExampleLogRegion::Network}, 226 | FMT_STRING("Hello, {}!"), 123.0); 227 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, 228 | FMT_STRING("Hello, {}!"), (void *)123); 229 | logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 230 | FMT_STRING("Hello, {}!"), 123); 231 | logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, 232 | FMT_STRING("Hello, {}!"), "world"); 233 | 234 | EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); 235 | } 236 | 237 | TYPED_TEST(RtLogTest, LogReturnsSuccessOnNormalEnqueue) { 238 | auto &logger = this->logger_; 239 | EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 240 | FMT_STRING("Hello, {}!"), 123l), 241 | rtlog::Status::Success); 242 | } 243 | 244 | TYPED_TEST(TruncatedRtLogTest, LogHandlesLongMessageTruncation) { 245 | auto &logger = this->logger_; 246 | auto maxMessageLength = this->maxMessageLength_; 247 | EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 248 | FMT_STRING("Hello, {}! xxxxxxxxxxx"), 123l), 249 | rtlog::Status::Error_MessageTruncated); 250 | 251 | auto InspectLogMessage = [=](const ExampleLogData &data, 252 | size_t sequenceNumber, const char *fstring, 253 | ...) { 254 | (void)sequenceNumber; 255 | 256 | EXPECT_EQ(data.level, ExampleLogLevel::Debug); 257 | EXPECT_EQ(data.region, ExampleLogRegion::Engine); 258 | 259 | std::array buffer{}; 260 | va_list args; 261 | va_start(args, fstring); 262 | vsnprintf(buffer.data(), buffer.size(), fstring, args); 263 | va_end(args); 264 | 265 | EXPECT_STREQ(buffer.data(), "Hello, 12"); 266 | EXPECT_EQ(strlen(buffer.data()), maxMessageLength - 1); 267 | }; 268 | 269 | EXPECT_EQ(logger.PrintAndClearLogQueue(InspectLogMessage), 1); 270 | } 271 | 272 | TEST(LoggerTest, SingleWriterLogHandlesQueueFullError) { 273 | const auto maxNumMessages = 16; 274 | rtlog::Logger 276 | logger; 277 | 278 | auto status = rtlog::Status::Success; 279 | 280 | while (status == rtlog::Status::Success) { 281 | status = logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 282 | FMT_STRING("Hello, {}!"), "world"); 283 | } 284 | 285 | EXPECT_EQ(status, rtlog::Status::Error_QueueFull); 286 | } 287 | 288 | TEST(LoggerTest, MultipleWriterLogHandlesNeverReturnsFull) { 289 | const auto maxNumMessages = 16; 290 | rtlog::Logger 292 | logger; 293 | 294 | auto status = rtlog::Status::Success; 295 | 296 | int messageCount = 0; 297 | 298 | while (status == rtlog::Status::Success && 299 | messageCount < maxNumMessages + 10) { 300 | status = logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, 301 | FMT_STRING("Hello, {} {}!"), "world", messageCount); 302 | messageCount++; 303 | } 304 | 305 | // We can never report full on a multi-writer queue, it is not realtime safe 306 | // We will just happily spin forever in this loop unless we break 307 | EXPECT_EQ(status, rtlog::Status::Success); 308 | } 309 | 310 | #endif // RTLOG_USE_FMTLIB 311 | --------------------------------------------------------------------------------