Commit fa8c5950 authored by Clemens Backes's avatar Clemens Backes Committed by V8 LUCI CQ

[base] Introduce FormattedString

This introduces a class which can be used for formatting dynamic values
into a constant-size, stack-allocated array. You get ostream-style code
but printf-style performance, and in particular no dynamic allocation.
This makes this class also suitable to be used in OOM or other fatal
situations where we cannot rely on dynamic memory allocation to still
work.

Using FormattedString will automatically compute the format string
depending on the types. It also computes the maximum size of the output.
Last but not least, it makes the code a lot more readable than
traditional printf style printing.

R=mlippautz@chromium.org

Bug: chromium:1323177
Change-Id: I47228b3603c694c1fa23516dd3f1c57e39c0ca35
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3644622
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Reviewed-by: 's avatarMichael Lippautz <mlippautz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80529}
parent 119443fc
......@@ -659,6 +659,7 @@ filegroup(
"src/base/safe_conversions_arm_impl.h",
"src/base/safe_conversions_impl.h",
"src/base/small-vector.h",
"src/base/string-format.h",
"src/base/strings.cc",
"src/base/strings.h",
"src/base/sys-info.cc",
......
......@@ -5258,6 +5258,7 @@ v8_component("v8_libbase") {
"src/base/sanitizer/msan.h",
"src/base/sanitizer/tsan.h",
"src/base/small-vector.h",
"src/base/string-format.h",
"src/base/strings.cc",
"src/base/strings.h",
"src/base/sys-info.cc",
......
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_BASE_STRING_FORMAT_H_
#define V8_BASE_STRING_FORMAT_H_
#include <array>
#include <limits>
#include <string_view>
#include <tuple>
#include "src/base/logging.h"
#include "src/base/platform/platform.h"
namespace v8::base {
// Implementation detail, do not use outside this header. The public interface
// is below.
namespace impl {
template <const std::string_view&... strs>
struct JoinedStringViews {
static constexpr auto JoinIntoNullTerminatedArray() noexcept {
constexpr size_t kArraySize = (1 + ... + strs.size());
std::array<char, kArraySize> arr{};
char* ptr = arr.data();
for (auto str : std::initializer_list<std::string_view>{strs...}) {
for (auto c : str) *ptr++ = c;
}
*ptr++ = '\0';
DCHECK_EQ(arr.data() + arr.size(), ptr);
return arr;
}
// Store in an array with static linkage, so we can reference it from the
// {std::string_view} below.
static constexpr auto array = JoinIntoNullTerminatedArray();
// Create a string view to the null-terminated array. The null byte is not
// included.
static constexpr std::string_view string_view = {array.data(),
array.size() - 1};
};
template <typename T>
struct FormattedStringPart {
static_assert(sizeof(T) < 0,
"unimplemented type, add specialization below if needed");
};
template <>
struct FormattedStringPart<int> {
// Integer range: [-2147483647, 2147483647]. Representable in 11 characters.
static constexpr int kMaxLen = 11;
static constexpr std::string_view kFormatPart = "%d";
int value;
};
template <>
struct FormattedStringPart<size_t> {
// size_t range: [0, 4294967295] on 32-bit, [0, +18446744073709551615] on
// 64-bit. Needs 10 or 20 characters.
static constexpr int kMaxLen = sizeof(size_t) == sizeof(uint32_t) ? 10 : 20;
static constexpr std::string_view kFormatPart = "%zu";
size_t value;
};
template <size_t N>
struct FormattedStringPart<char[N]> {
static_assert(N >= 1, "Do not print (static) empty strings");
static_assert(N <= 128, "Do not include huge strings");
static constexpr int kMaxLen = N - 1;
static constexpr std::string_view kFormatPart = "%s";
const char* value;
};
template <const std::string_view& kFormat, int kMaxLen, typename... Parts>
std::array<char, kMaxLen> PrintFormattedStringToArray(Parts... parts) {
std::array<char, kMaxLen> message;
static_assert(kMaxLen > 0);
static_assert(
kMaxLen < 128,
"Don't generate overly large strings; this limit can be increased, but "
"consider that the array lives on the stack of the caller.");
// This special case is needed because clang does not consider the empty
// string_view a valid format string (but "" is fine).
constexpr const char* kFormatString =
kFormat.size() == 0 ? "" : kFormat.data();
int characters = base::OS::SNPrintF(message.data(), kMaxLen, kFormatString,
parts.value...);
CHECK(characters >= 0 && characters < kMaxLen);
DCHECK_EQ('\0', message[characters]);
return message;
}
} // namespace impl
// `FormattedString` allows to format strings with statically known number and
// type of constituents.
// The class stores all values that should be printed, and generates the final
// string via `SNPrintF` into a `std::array`, without any dynamic memory
// allocation. The format string is computed statically.
// This makes this class not only very performant, but also suitable for
// situations where we do not want to perform any memory allocation (like for
// reporting OOM or fatal errors).
//
// Use like this:
// auto message = FormattedString{} << "Cannot allocate " << size << " bytes";
// V8::FatalProcessOutOfMemory(nullptr, message.PrintToArray().data());
//
// This code is compiled into the equivalent of
// std::array<char, 34> message_arr;
// int chars = SNPrintF(message_arr.data(), 34, "%s%d%s", "Cannot allocate ",
// size, " bytes");
// CHECK(chars >= 0 && chars < 34);
// V8::FatalProcessOutOfMemory(nullptr, message_arr.data());
template <typename... Ts>
class FormattedString {
template <typename T>
using Part = impl::FormattedStringPart<T>;
static_assert(std::conjunction_v<std::is_trivial<Part<Ts>>...>,
"All parts needs to be trivial to guarantee optimal code");
public:
static constexpr int kMaxLen = (1 + ... + Part<Ts>::kMaxLen);
static constexpr std::string_view kFormat =
impl::JoinedStringViews<Part<Ts>::kFormatPart...>::string_view;
FormattedString() {
static_assert(sizeof...(Ts) == 0,
"Only explicitly construct empty FormattedString, use "
"operator<< to appending");
}
// Add one more part to the FormattedString. Only allowed on r-value ref (i.e.
// temporary object) to avoid misuse like `FormattedString<> str; str << 3;`
// instead of `auto str = FormattedString{} << 3;`.
template <typename T>
V8_WARN_UNUSED_RESULT auto operator<<(T&& t) const&& {
using PlainT = std::remove_cv_t<std::remove_reference_t<T>>;
return FormattedString<Ts..., PlainT>{
std::tuple_cat(parts_, std::make_tuple(Part<PlainT>{t}))};
}
// Print this FormattedString into an array. Does not allocate any dynamic
// memory. The result lives on the stack of the caller.
V8_INLINE V8_WARN_UNUSED_RESULT std::array<char, kMaxLen> PrintToArray()
const {
return std::apply(
impl::PrintFormattedStringToArray<kFormat, kMaxLen, Part<Ts>...>,
parts_);
}
private:
template <typename... Us>
friend class FormattedString;
explicit FormattedString(std::tuple<Part<Ts>...> parts) : parts_(parts) {}
std::tuple<Part<Ts>...> parts_;
};
} // namespace v8::base
#endif // V8_BASE_STRING_FORMAT_H_
......@@ -238,6 +238,7 @@ v8_source_set("unittests_sources") {
"base/platform/semaphore-unittest.cc",
"base/platform/time-unittest.cc",
"base/region-allocator-unittest.cc",
"base/string-format-unittest.cc",
"base/sys-info-unittest.cc",
"base/template-utils-unittest.cc",
"base/threaded-list-unittest.cc",
......
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/base/string-format.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest-support.h"
namespace v8::base {
// Some hard-coded assumptions.
constexpr int kMaxPrintedIntLen = 11;
constexpr int kMaxPrintedSizetLen = sizeof(size_t) == 4 ? 10 : 20;
TEST(FormattedStringTest, Empty) {
auto empty = FormattedString{};
EXPECT_EQ("", decltype(empty)::kFormat);
EXPECT_EQ(1, decltype(empty)::kMaxLen);
EXPECT_EQ('\0', empty.PrintToArray()[0]);
}
TEST(FormattedStringTest, SingleString) {
auto message = FormattedString{} << "foo";
EXPECT_EQ("%s", decltype(message)::kFormat);
constexpr std::array<char, 4> kExpectedOutput{'f', 'o', 'o', '\0'};
EXPECT_EQ(kExpectedOutput, message.PrintToArray());
}
TEST(FormattedStringTest, Int) {
auto message = FormattedString{} << 42;
EXPECT_EQ("%d", decltype(message)::kFormat);
// +1 for null-termination.
EXPECT_EQ(kMaxPrintedIntLen + 1, decltype(message)::kMaxLen);
EXPECT_THAT(message.PrintToArray().begin(), ::testing::StrEq("42"));
}
TEST(FormattedStringTest, MaxInt) {
auto message = FormattedString{} << std::numeric_limits<int>::max();
auto result_arr = message.PrintToArray();
// We *nearly* used the full reserved array size (the minimum integer is still
// one character longer)..
EXPECT_EQ(size_t{decltype(message)::kMaxLen}, result_arr.size());
EXPECT_THAT(result_arr.begin(), ::testing::StrEq("2147483647"));
}
TEST(FormattedStringTest, MinInt) {
auto message = FormattedString{} << std::numeric_limits<int>::min();
auto result_arr = message.PrintToArray();
// We used the full reserved array size.
EXPECT_EQ(size_t{decltype(message)::kMaxLen}, result_arr.size());
EXPECT_THAT(result_arr.begin(), ::testing::StrEq("-2147483648"));
}
TEST(FormattedStringTest, SizeT) {
auto message = FormattedString{} << size_t{42};
EXPECT_EQ("%zu", decltype(message)::kFormat);
// +1 for null-termination.
EXPECT_EQ(kMaxPrintedSizetLen + 1, decltype(message)::kMaxLen);
EXPECT_THAT(message.PrintToArray().begin(), ::testing::StrEq("42"));
}
TEST(FormattedStringTest, MaxSizeT) {
auto message = FormattedString{} << std::numeric_limits<size_t>::max();
auto result_arr = message.PrintToArray();
// We used the full reserved array size.
EXPECT_EQ(size_t{decltype(message)::kMaxLen}, result_arr.size());
constexpr const char* kMaxSizeTStr =
sizeof(size_t) == 4 ? "4294967295" : "18446744073709551615";
EXPECT_THAT(result_arr.begin(), ::testing::StrEq(kMaxSizeTStr));
}
TEST(FormattedStringTest, Combination) {
auto message = FormattedString{} << "Expected " << 11 << " got " << size_t{42}
<< "!";
EXPECT_EQ("%s%d%s%zu%s", decltype(message)::kFormat);
size_t expected_array_len =
strlen("Expected got !") + kMaxPrintedIntLen + kMaxPrintedSizetLen + 1;
EXPECT_EQ(expected_array_len, size_t{decltype(message)::kMaxLen});
EXPECT_THAT(message.PrintToArray().begin(),
::testing::StrEq("Expected 11 got 42!"));
}
} // namespace v8::base
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment