diff --git a/include/fmt/async.h b/include/fmt/async.h new file mode 100644 index 00000000..f0fa5686 --- /dev/null +++ b/include/fmt/async.h @@ -0,0 +1,323 @@ +#ifndef FMT_ASYNC_H_ +#define FMT_ASYNC_H_ + +#include "format.h" +#include + +namespace fmt { + +template struct basic_async_entry { +protected: + using char_type = typename Context::char_type; + using format_arg = typename basic_format_args::format_arg; + using arg_destructor = void (*)(void *p); + + basic_string_view format_; + unsigned long long desc_; + arg_destructor dtor_; + + FMT_CONSTEXPR basic_async_entry(basic_string_view format) : format_(format), desc_(0), dtor_(0) {} + const format_arg* get_format_args() const; + + template + using enable_out = enable_if_t::value, T>; + + void destruct() { if (dtor_) dtor_(this); } +public: + struct dtor_sentry { + dtor_sentry(basic_async_entry& entry) : entry_(entry) {} + ~dtor_sentry() { entry_.destruct(); } + basic_async_entry& entry_; + }; + + // libfmt public APIs + std::basic_string format() const { return vformat(format_, {desc_, get_format_args()}); } + + template + auto format_to(OutIt out) const -> enable_out { + return vformat_to(out, format_, {desc_, get_format_args()}); + } + + // template + // auto format_to(OutIt out, size_t n) const -> enable_out> { + // return vformat_to(detail::truncating_iterator(out, n), format_, {desc_, get_format_args()}); + // } + + size_t formatted_size() const { + detail::counting_buffer<> buf; + format_to(buf); + return buf.count(); + } + + void print(std::FILE* file = stdout) const { return vprint(file, format_, {desc_, get_format_args()}); } + +}; + +template struct async_entry : basic_async_entry { + using entry = basic_async_entry; + + format_arg_store arg_store_; + + template + FMT_CONSTEXPR async_entry(const S& format_str, const Args&... args) : entry(to_string_view(format_str)), arg_store_(args...) { + entry::desc_ = arg_store_.desc; + } + FMT_CONSTEXPR void set_dtor(typename entry::arg_destructor dtor) { this->dtor_ = dtor; } +}; + +template +inline const typename basic_async_entry::format_arg* basic_async_entry::get_format_args() const { + union obfuscated_args { + const detail::value* values_; + const format_arg* args_; + intptr_t pointer_; // more efficient to add integer with size, as the compiler is able to avoid emitting branch + } args; + auto& entry = static_cast&>(*this); + args.values_ = entry.arg_store_.data_.args_; + if (entry.desc_ & detail::has_named_args_bit) { + args.pointer_ += (desc_ & detail::is_unpacked_bit) ? sizeof(*args.args_) : sizeof(*args.values_); + } + return args.args_; +} + +// +// A stored entry looks like: +// ----------------------------------------------------------------------- +// | basic_async_entry | arg_store | stored_objs... | stored_buffers... | +// ----------------------------------------------------------------------- +// +namespace async { +namespace detail { +namespace detail = fmt::detail; +template using decay_t = typename std::decay::type; +template using add_const_t = typename std::add_const::type; +template struct disjunction : std::false_type {}; +template struct disjunction : B1 {}; +template struct disjunction : conditional_t> {}; + +enum class store_method { + numeric, // stored by libfmt as numeric value, no need for extra storage + object, // stored by libfmt as object, copy/move construct the object manually (requires properly calling destructor) + buffer, // string/hexdump, store the string/binary trivially as buffer. + constexpr_str // compile-time string, can be stored as pointer directly (requires c++20 is_constant_evaluated()) +}; + +template using store_method_constant = std::integral_constant; +using store_as_object = store_method_constant; +using store_as_buffer = store_method_constant; + +template +using stored_as_numeric = std::integral_constant; +template +using stored_as_string = std::integral_constant; +template struct is_basic_string : std::false_type {}; +template struct is_basic_string> : std::true_type {}; +template +using stored_as_string_object = std::integral_constant::value && is_basic_string>::value && std::is_rvalue_reference::value>; + +struct custom_store_method_checker { + template , typename Formatter = typename Context::template formatter_type> + static enable_if_t::value, std::tuple(), std::declval()))>> test(double); + template , typename Formatter = typename Context::template formatter_type> + static enable_if_t::value, std::tuple(), std::declval()))>> test(int); + template + static std::tuple test(...); +}; + +template +struct custom_store_method { + using transformed_type = typename std::tuple_element<1, decltype(custom_store_method_checker::template test(0))>::type; + static constexpr bool custom_store = std::tuple_element<0, decltype(custom_store_method_checker::template test(0))>::type::value && !std::is_same::value; + using store_type = conditional_t::value, store_as_object, store_as_buffer>; +}; + +template , Context>> +struct stored_method_constant : std::integral_constant::value ? store_method::numeric : + stored_as_string::value ? (stored_as_string_object::value ? store_method::object : store_method::buffer) : + // store_method::object> {}; + custom_store_method::store_type::value> {}; + +// Check for integer_sequence +#if defined(__cpp_lib_integer_sequence) || FMT_MSC_VER >= 1900 +template +using integer_sequence = std::integer_sequence; +template using index_sequence = std::index_sequence; +template using make_index_sequence = std::make_index_sequence; +#else +template struct integer_sequence { + using value_type = T; + + static FMT_CONSTEXPR size_t size() { return sizeof...(N); } +}; + +template using index_sequence = integer_sequence; + +template +struct make_integer_sequence : make_integer_sequence {}; +template +struct make_integer_sequence : integer_sequence {}; + +template +using make_index_sequence = make_integer_sequence; +#endif + +template +struct arg_transformer { + using arg_tuple = std::tuple; + template using arg_at = typename std::tuple_element::type; + using type_tuple = std::tuple...>; + template using type_at = typename std::tuple_element::type; + template using size_at = std::integral_constant)>; + template using objsize_at = conditional_t, Context>::value == store_method::object, size_at, std::integral_constant>; + template struct objsizesum_at : std::integral_constant, objsizesum_at>::value + objsize_at::value> {}; + template using objoffset_at = conditional_t, objsizesum_at>; +}; + +template , Context>> +using transformed_arg_type = conditional_t::custom_store, typename custom_store_method::transformed_type, + conditional_t::value && !stored_as_string_object::value, basic_string_view, add_const_t>&> + >; + +template +struct async_entry_constructor { + template + static size_t construct(void* buf, const S& format_str, Args... args) { + return async_entry_constructor(buf, format_str, range(), std::forward(args)...).get_total_size(); + } + +private: + using entry = async_entry>...>; + using trans = arg_transformer; + using char_type = typename Context::char_type; + using range = make_index_sequence; + + template + FMT_CONSTEXPR async_entry_constructor(void* buf, const S& format_str, index_sequence, Args... args) : pentry(reinterpret_cast(buf)), pBuffer(get_buffer_store(buf)) { + auto p = new(buf) entry(format_str, store(std::forward(args))...); + if (disjunction...>::value) p->set_dtor(destructor::dtor); + } + + template using arg_at = typename trans::template arg_at; + template using type_at = typename trans::template type_at; + + template using need_destruct = std::integral_constant, Context>::value == store_method::object && std::is_destructible>::value && !std::is_trivially_destructible>::value>; + template static bool destruct(void *p, std::true_type) { reinterpret_cast*>(p + sizeof(entry) + trans::template objoffset_at::value)->~type_at(); return true; } + template static bool destruct(void *p, std::false_type) { return false; } + template struct destructor { + static void dummy(...) {} + static void dtor(void *p) { dummy(destruct(p, need_destruct())...); } + }; + + template transformed_arg_type, Context> store(arg_at arg) { + using Arg = arg_at; + using select_store_method = custom_store_method; + using mapped_type = detail::mapped_type_constant, Context>; + if constexpr (select_store_method::custom_store == true) { + using Formatter = typename Context::template formatter_type>; + + return Formatter::store(pBuffer, std::forward(arg)); + } + else if constexpr (stored_as_string::value && !stored_as_string_object::value) { + return copy_string(pBuffer, detail::arg_mapper().map(std::forward(arg))); + } + else if constexpr (stored_as_numeric::value) { + return std::forward(arg); + } + else { + char* const pobjs = pentry + sizeof(entry); + char* const pobj = pobjs + trans::template objoffset_at::value; + using Type = type_at; + auto p = new(pobj) Type(std::forward(arg)); + return *p; + } + } + + static basic_string_view copy_string(char*& pBuffer, const char_type* cstr) { + if constexpr (std::is_same::value) { + wchar_t* pStart = reinterpret_cast(pBuffer); + wchar_t* pEnd = wcpcpy(pStart, cstr); + pBuffer = reinterpret_cast(pEnd); + return basic_string_view(pStart, pEnd - pStart); + } + else { + char* pStart = pBuffer; + char* pEnd = stpcpy(pStart, cstr); + pBuffer = pEnd; + return basic_string_view(pStart, pEnd - pStart); + } + } + static basic_string_view copy_string(char*& pBuffer, basic_string_view sv) { + char_type* pStart = reinterpret_cast(pBuffer); + size_t size = sizeof(char_type) * sv.size(); + std::memcpy(pStart, sv.data(), size); + pBuffer += size; + return basic_string_view(pStart, sv.size()); + } + + static FMT_CONSTEXPR char* get_buffer_store(void* buf) { + char* const pentry = reinterpret_cast(buf); // entry will be constructed here + char* const pobjs = pentry + sizeof(entry); // objects will be stored starting here + char* const pbufs = pobjs + get_obj_size(); // buffers will be stored starting here + return pbufs; + } + static constexpr size_t get_obj_size() { return trans::template objsizesum_at::value; } + constexpr size_t get_total_size() const { return pBuffer - pentry; } + char* const pentry; + char* pBuffer; +}; + +} // namespace detail + +template > +inline size_t store(void* buf, const S& format_str, Args&&... args) { + using Context = buffer_context; + using Constructor = detail::async_entry_constructor; + return Constructor::construct(buf, format_str, std::forward(args)...); +} + +template +inline auto format(basic_async_entry& entry) -> decltype(entry.format()) { + typename basic_async_entry::dtor_sentry _(entry); + return entry.format(); +} + +template +inline auto format_to(basic_async_entry& entry, OutIt out) -> decltype(entry.format_to(out)) { + typename basic_async_entry::dtor_sentry _(entry); + return entry.format_to(out); +} + +// template +// inline auto format_to(basic_async_entry& entry, OutIt out, size_t n) -> decltype(entry.format_to_n(out, n)) { +// typename basic_async_entry::dtor_sentry _(entry); +// return entry.format_to(out, n); +// } + +template +inline void print(basic_async_entry& entry, std::FILE* f = stdout) { + typename basic_async_entry::dtor_sentry _(entry); + entry.print(f); +} + +// TODO should we add wrappers like this? +// template +// inline auto format(void* entry) -> decltype(format(std::declval&>())) { +// return format(*reinterpret_cast*>(entry)); +// } +// inline auto wformat(void* entry) -> decltype(format(entry)) { return format(entry); } + +} // namespace async + +template > +inline auto make_async_entry(const S& format_str, Args&&... args) -> async_entry, remove_reference_t...> { + return { format_str, args... }; +} + +template > +inline size_t store_async_entry(void* buf, const S& format_str, Args&&... args) { + return async::store(buf, format_str, std::forward(args)...); +} + +} // namespace fmt +#endif // FMT_ASYNC_H_ diff --git a/include/fmt/core.h b/include/fmt/core.h index 10ac2843..105dd87a 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -1612,6 +1612,7 @@ class format_arg_store using value_type = conditional_t, basic_format_arg>; + public: detail::arg_data data_; @@ -1625,7 +1626,6 @@ class format_arg_store ? static_cast(detail::has_named_args_bit) : 0); - public: FMT_CONSTEXPR FMT_INLINE format_arg_store(const Args&... args) : #if FMT_GCC_VERSION && FMT_GCC_VERSION < 409 @@ -1734,13 +1734,13 @@ template class basic_format_args { return static_cast((desc_ >> shift) & mask); } + public: constexpr FMT_INLINE basic_format_args(unsigned long long desc, const detail::value* values) : desc_(desc), values_(values) {} constexpr basic_format_args(unsigned long long desc, const format_arg* args) : desc_(desc), args_(args) {} - public: constexpr basic_format_args() : desc_(0), args_(nullptr) {} /** diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f1dcf16a..781373ff 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -55,6 +55,7 @@ endfunction() add_fmt_test(args-test) add_fmt_test(assert-test) +add_fmt_test(async-test) add_fmt_test(chrono-test) add_fmt_test(color-test) add_fmt_test(core-test) diff --git a/test/async-test.cc b/test/async-test.cc new file mode 100644 index 00000000..50d8b9ff --- /dev/null +++ b/test/async-test.cc @@ -0,0 +1,126 @@ + +#ifdef WIN32 +# define _CRT_SECURE_NO_WARNINGS +#endif + +#include "fmt/async.h" +#include "gtest-extra.h" + +#define TWENTY_ARGS "{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} " +static const char multiple_brackets[] = TWENTY_ARGS; +static fmt::string_view get_format_string(size_t num_args) { return fmt::string_view(&multiple_brackets[(20 - num_args)*3], num_args * 3); } + +namespace trivial_entry_test { +template +inline void make_async_entry_test(Args&&... args) { + std::string formatted = fmt::format(std::forward(args)...); + // format_arg_store containing named_args are not copy-constructible, auto&& is required. + auto&& entry = fmt::make_async_entry(std::forward(args)...); + EXPECT_EQ(formatted, fmt::async::format(entry)); +} + +template +inline void make_async_entry_test_args(Args&&... args) { + make_async_entry_test(get_format_string(sizeof...(args)), std::forward(args)...); +} + +void make_async_entry_and_alter(const std::string& s) { + std::string str = s; + std::string formatted = fmt::format("{}", str); + auto entry = fmt::make_async_entry("{}", str); + str.front() = formatted.front() = '#'; + str.back() = formatted.back() = '#'; + EXPECT_EQ(formatted, fmt::format("{}", str)); + EXPECT_EQ(formatted, fmt::async::format(entry)); +} +} + +namespace stored_entry_test { +static char buf[1 * 1024 * 1024] = {}; // 1M should be enough for this test +static fmt::basic_async_entry& entry = reinterpret_cast&>(buf); + +template +inline void make_async_entry_test(Args&&... args) { + std::string formatted = fmt::format(std::forward(args)...); + // FIXME: how to test entry_size? + size_t entry_size = fmt::store_async_entry(buf, std::forward(args)...); + EXPECT_EQ(formatted, fmt::async::format(entry)); +} + +template +inline void make_async_entry_test_args(Args&&... args) { + make_async_entry_test(get_format_string(sizeof...(args)), std::forward(args)...); +} + +void make_async_entry_and_alter(const std::string& s) { + std::string str = s; + std::string formatted = fmt::format("{}", str); + size_t entry_size = fmt::store_async_entry(buf, "{}", str); + str.front() = str.back() = '#'; + EXPECT_EQ(formatted, fmt::async::format(entry)); + formatted.front() = formatted.back() = '#'; + EXPECT_EQ(formatted, fmt::format("{}", str)); +} + +void make_async_entry_dtor_test(std::string s) { + static void* constructed; + static void* destructed; + struct my_string : std::string { + my_string(const std::string& s) : std::string(s) { + constructed = this; + } + ~my_string() { destructed = this; } + }; + { + my_string str(s); + size_t entry_size = fmt::store_async_entry(buf, "{}", str); + fmt::async::format(entry); + } + EXPECT_NE(nullptr, constructed); + EXPECT_EQ(constructed, destructed); +}; + + +} + +TEST(AsyncTest, TrivialEntry) { + using namespace trivial_entry_test; + // basic test + make_async_entry_test("The answer is {}", 42); + // index + make_async_entry_test("The answer of {2}*{1} is {0}", 42, 6, 7); + + // named args + using namespace fmt::literals; + make_async_entry_test("The answer of {}*{a} is {product}", 6, "product"_a=42, "a"_a=7); + + // long arg list (>=16, as max_packed_args = 15) + make_async_entry_test_args(short(1), (unsigned short)2, 3, 4U, 5L, 6UL, 7LL, 8ULL, 9.0F, 10.0, 11, 12, 13, 14, 15, 16, 17, 18); + make_async_entry_test(TWENTY_ARGS "{narg}", 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,0/*ignore*/,"narg"_a="bingo"); + + // this API copies only reference. + make_async_entry_and_alter("[change me]"); +} + + +TEST(AsyncTest, StoredEntry) { + using namespace stored_entry_test; + // basic test + make_async_entry_test("The answer is {}", 42); + // index + make_async_entry_test("The answer of {2}*{1} is {0}", 42, 6, 7); + + // named args + using namespace fmt::literals; + // make_async_entry_test("The answer of {}*{a} is {product}", 6, "product"_a=42, "a"_a=7); + + // long arg list (>=16, as max_packed_args = 15) + make_async_entry_test_args(short(1), (unsigned short)2, 3, 4U, 5L, 6UL, 7LL, 8ULL, 9.0F, 10.0, 11, 12, 13, 14, 15, 16, 17, 18); + // make_async_entry_test(TWENTY_ARGS "{narg}", 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,0/*ignore*/,"narg"_a="bingo"); + + // this API copies buffer. + make_async_entry_and_alter("[change me]"); + + // dtor + make_async_entry_dtor_test("custom string copied as object"); +}