diff --git a/CHANGELOG.md b/CHANGELOG.md index b08a3ad7..78250dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,16 @@ or disable this feature through the backend options by modifying the `check_printable_char` callback in `BackendOptions`. +- Added `StringRef`, a utility for passing string arguments by reference without copying. Suitable for string literals + or immutable strings with a guaranteed persistent lifetime. For example + + ```c++ + #include "quill/StringRef.h" + + static constexpr std::string_view sv {"string_view"}; + LOG_INFO(logger, "{} {}", quill::utility::StringRef{sv}, quill::utility::StringRef{"string_literal"}); + ``` + ## v4.4.1 - Fixed multiple definitions of `quill::detail::get_error_message` ([#469](https://github.com/odygrd/quill/issues/469)) diff --git a/examples/advanced/user_quill_codec.h b/examples/advanced/user_quill_codec.h index 2be43292..72dcfeb5 100644 --- a/examples/advanced/user_quill_codec.h +++ b/examples/advanced/user_quill_codec.h @@ -1,6 +1,7 @@ #pragma once // Always required +#include "quill/bundled/fmt/format.h" #include "quill/core/Codec.h" #include "quill/core/DynamicFormatArgStore.h" @@ -15,14 +16,11 @@ template <> struct fmtquill::formatter { - template - constexpr auto parse(FormatContext& ctx) - { + constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - template - auto format(::User const& user, FormatContext& ctx) const + auto format(::User const& user, format_context& ctx) const { return fmtquill::format_to(ctx.out(), "Name: {}, Surname: {}, Age: {}, Favorite Colors: {}", user.name, user.surname, user.age, user.favorite_colors); diff --git a/quill/CMakeLists.txt b/quill/CMakeLists.txt index 9581406c..a6931f56 100644 --- a/quill/CMakeLists.txt +++ b/quill/CMakeLists.txt @@ -85,6 +85,7 @@ set(HEADER_FILES include/quill/Frontend.h include/quill/Logger.h include/quill/LogMacros.h + include/quill/StringRef.h include/quill/UserClockSource.h include/quill/Utility.h ) diff --git a/quill/include/quill/StringRef.h b/quill/include/quill/StringRef.h new file mode 100644 index 00000000..dcb7969f --- /dev/null +++ b/quill/include/quill/StringRef.h @@ -0,0 +1,88 @@ +/** + * Copyright(c) 2020-present, Odysseas Georgoudis & quill contributors. + * Distributed under the MIT License (http://opensource.org/licenses/MIT) + */ + +#pragma once + +#include +#include +#include +#include + +#include "quill/core/Attributes.h" +#include "quill/core/Codec.h" +#include "quill/core/DynamicFormatArgStore.h" + +namespace quill::utility +{ +/** + * StringRef is used to specify that a string argument should be passed by reference instead of by + * value, ensuring that no copy of the string is made. + * + * Note that by default, all strings, including C strings and std::string_view, are copied before + * being passed to the backend. To pass strings by reference, they must be wrapped in a StringRef. + * + * Use this with caution, as the backend will parse the string asynchronously. The wrapped string + * must have a valid lifetime and should not be modified. + */ +class StringRef +{ +public: + explicit StringRef(std::string const& str) : _str_view(str){}; + explicit StringRef(std::string_view str) : _str_view(str){}; + explicit StringRef(char const* str) : _str_view(str, strlen(str)){}; + StringRef(char const* str, size_t size) : _str_view(str, size){}; + + QUILL_NODISCARD std::string_view const& get_string_view() const noexcept { return _str_view; } + +private: + std::string_view _str_view; +}; +} // namespace quill::utility + +/***/ +template <> +struct quill::detail::ArgSizeCalculator +{ + static size_t calculate(std::vector& conditional_arg_size_cache, + quill::utility::StringRef const& no_copy) noexcept + { + return sizeof(size_t) + sizeof(uintptr_t); + } +}; + +/***/ +template <> +struct quill::detail::Encoder +{ + static void encode(std::byte*& buffer, std::vector const& conditional_arg_size_cache, + uint32_t& conditional_arg_size_cache_index, quill::utility::StringRef const& no_copy) noexcept + { + char const* data = no_copy.get_string_view().data(); + std::memcpy(buffer, &data, sizeof(uintptr_t)); + buffer += sizeof(uintptr_t); + + size_t const size = no_copy.get_string_view().size(); + std::memcpy(buffer, &size, sizeof(size_t)); + buffer += sizeof(size_t); + } +}; + +/***/ +template <> +struct quill::detail::Decoder +{ + static void decode(std::byte*& buffer, DynamicFormatArgStore* args_store) + { + char const* data; + std::memcpy(&data, buffer, sizeof(uintptr_t)); + buffer += sizeof(uintptr_t); + + size_t size; + std::memcpy(&size, buffer, sizeof(size_t)); + buffer += sizeof(size_t); + + args_store->push_back(std::string_view{data, size}); + } +}; \ No newline at end of file diff --git a/quill/test/integration_tests/CMakeLists.txt b/quill/test/integration_tests/CMakeLists.txt index 6e83eb49..294b5e35 100644 --- a/quill/test/integration_tests/CMakeLists.txt +++ b/quill/test/integration_tests/CMakeLists.txt @@ -96,6 +96,7 @@ quill_add_test(TEST_StringLogging StringLoggingTest.cpp) quill_add_test(TEST_StringRandomLargeLogging StringRandomLargeLoggingTest.cpp) quill_add_test(TEST_StringRandomLogging StringRandomLoggingTest.cpp) quill_add_test(TEST_StringRandomSmallLogging StringRandomSmallLoggingTest.cpp) +quill_add_test(TEST_StringRefTest StringRefTest.cpp) quill_add_test(TEST_TagsLogging TagsLoggingTest.cpp) quill_add_test(TEST_UserClockSource UserClockSourceTest.cpp) quill_add_test(TEST_UserDefinedTypeLogging UserDefinedTypeLoggingTest.cpp) diff --git a/quill/test/integration_tests/StringRefTest.cpp b/quill/test/integration_tests/StringRefTest.cpp new file mode 100644 index 00000000..0dae0aec --- /dev/null +++ b/quill/test/integration_tests/StringRefTest.cpp @@ -0,0 +1,89 @@ +#include "doctest/doctest.h" + +#include "misc/TestUtilities.h" +#include "quill/Backend.h" +#include "quill/Frontend.h" +#include "quill/LogMacros.h" +#include "quill/StringRef.h" +#include "quill/sinks/FileSink.h" + +#include +#include +#include +#include + +using namespace quill; + +/***/ +TEST_CASE("string_no_copy_logging") +{ + static constexpr char const* filename = "string_no_copy_logging.log"; + static std::string const logger_name = "logger"; + + // Start the logging backend thread + Backend::start(); + + // Set writing logging to a file + auto file_sink = Frontend::create_or_get_sink( + filename, + []() + { + FileSinkConfig cfg; + cfg.set_open_mode('w'); + return cfg; + }(), + FileEventNotifier{}); + + Logger* logger = Frontend::create_or_get_logger(logger_name, std::move(file_sink)); + + static std::string s = "adipiscing"; + static std::string_view sv = "string_view"; + static char const* c_style_string_empty = ""; + static const char* c_style_string = "Lorem ipsum"; + static const char* npcs = "Example\u0003String\u0004"; + + std::string s1 = "adipiscing_1"; + char const* s2 = "adipiscing_2"; + + LOG_INFO(logger, "static string [{}]", quill::utility::StringRef{s}); + LOG_INFO(logger, "static string_view [{}]", quill::utility::StringRef{sv}); + LOG_INFO(logger, "static c_style_string_empty [{}]", quill::utility::StringRef{c_style_string_empty}); + LOG_INFO(logger, "static c_style_string [{}]", quill::utility::StringRef{c_style_string}); + LOG_INFO(logger, "static npcs [{}]", quill::utility::StringRef{npcs}); + LOG_INFO(logger, "string literal [{}]", quill::utility::StringRef{"test string literal"}); + LOG_INFO(logger, "mix strings [{}] [{}] [{}] [{}]", + quill::utility::StringRef{"test string literal"}, s1, quill::utility::StringRef{s}, s2); + + logger->flush_log(); + Frontend::remove_logger(logger); + + // Wait until the backend thread stops for test stability + Backend::stop(); + + // Read file and check + std::vector const file_contents = quill::testing::file_contents(filename); + REQUIRE_EQ(file_contents.size(), 7); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " static string [adipiscing]"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " static string_view [string_view]"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " static c_style_string_empty []"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " static c_style_string [Lorem ipsum]"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " static npcs [Example\\x03String\\x04]"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " string literal [test string literal]"})); + + REQUIRE(quill::testing::file_contains( + file_contents, std::string{"LOG_INFO " + logger_name + " mix strings [test string literal] [adipiscing_1] [adipiscing] [adipiscing_2]"})); + + testing::remove_file(filename); +} \ No newline at end of file