diff --git a/kotlin-native/runtime/src/main/cpp/Format.cpp b/kotlin-native/runtime/src/main/cpp/Format.cpp new file mode 100644 index 00000000000..587c1129d12 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/Format.cpp @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "Format.h" + +#include "Porting.h" + +using namespace kotlin; + +std_support::span kotlin::FormatToSpan(std_support::span buffer, const char* format, ...) noexcept { + std::va_list args; + va_start(args, format); + auto result = VFormatToSpan(buffer, format, args); + va_end(args); + return result; +} + +std_support::span kotlin::VFormatToSpan(std_support::span buffer, const char* format, std::va_list args) noexcept { + if (buffer.empty()) return buffer; + if (buffer.size() == 1) { + buffer.front() = '\0'; + return buffer; + } + int written = konan::vsnprintf(buffer.data(), buffer.size(), format, args); + // Consider this a failure, nothing has been written. TODO: Should this be an exception/RuntimeAssert? + if (written < 0) return buffer; + // If `written` is larger than the buffer size, just pretend we filled the entire buffer (ignoring the trailing \0). + size_t writtenSize = std::min(static_cast(written), buffer.size() - 1); + return buffer.subspan(writtenSize); +} diff --git a/kotlin-native/runtime/src/main/cpp/Format.h b/kotlin-native/runtime/src/main/cpp/Format.h new file mode 100644 index 00000000000..db615a25c55 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/Format.h @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_FORMAT_H +#define RUNTIME_FORMAT_H + +#include + +#include "cpp_support/Span.hpp" + +namespace kotlin { + +/* + * `FormatToSpan` is a `snprintf`-like API for formatting text. + * + * It formats text into a `span` buffer (it uses `snprintf` under the hood and so needs a contiguous memory buffer to write into) + * and returns the unused portion of `span`. + * When input `span` is empty it does nothing. + * If `snprintf` fails it returns the buffer unmodified. + * If the formatted text cannot fit into the buffer, it'll be truncated enough to fit. + * If any character was written into the buffer, there will always be a null character appended, and the returned span will point to it. + * + * ``` + * auto output = FormatToSpan(input, "string"); + * ``` + * If `input` was a buffer filled with `'\1'` of size 10, by the end the memory would look something like: + * ``` + * s t r i n g \0 \1 \1 \1 + * ^ ^ + * | | + * input output + * ``` + * and the output will be of size 4. + * If `input` was of size 5 instead: + * ``` + * s t r i \0 + * ^ ^ + * | | + * | output + * input + * ``` + * and the output will be of size 1. + * + * This allows for a composable behaviour: + * ``` + * auto buffer = ...; + * buffer = FormatToSpan(buffer, "str"); + * buffer = FormatToSpan(buffer, "%d", 42); + * buffer = FormatToSpan(buffer, "%s", "again"); + * ``` + * If `buffer` was of sufficient size, the result will be `str42again\0`. + */ + +// Format snprintf-style message into a `buffer` and return the unused portion of a buffer. +std_support::span FormatToSpan(std_support::span buffer, const char* format, ...) noexcept + __attribute__((format(printf, 2, 3))); + +// Format vsnprintf-style message into a `buffer` and return the unused portion of a buffer. +std_support::span VFormatToSpan(std_support::span buffer, const char* format, std::va_list args) noexcept; + +} // namespace kotlin + +#endif // RUNTIME_FORMAT_H diff --git a/kotlin-native/runtime/src/main/cpp/FormatTest.cpp b/kotlin-native/runtime/src/main/cpp/FormatTest.cpp new file mode 100644 index 00000000000..8a6230ee239 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/FormatTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "Format.h" + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "cpp_support/Span.hpp" + +using namespace kotlin; + +TEST(FormatTest, FormatToSpan_String) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(buffer, "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), buffer.size() - 2); +} + +TEST(FormatTest, FormatToSpan_StringFormat) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(buffer, "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), buffer.size() - 2); +} + +TEST(FormatTest, FormatToSpan_IntFormat) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(buffer, "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('4', '2', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), buffer.size() - 2); +} + +TEST(FormatTest, FormatToSpan_String_Size0) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(0), "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('\1', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 0); +} + +TEST(FormatTest, FormatToSpan_StringFormat_Size0) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(0), "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('\1', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 0); +} + +TEST(FormatTest, FormatToSpan_IntFormat_Size0) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(0), "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('\1', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 0); +} + +TEST(FormatTest, FormatToSpan_String_Size1) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(1), "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('\0', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_StringFormat_Size1) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(1), "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('\0', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_IntFormat_Size1) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(1), "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('\0', '\1', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_String_Size2) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(2), "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', '\0', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 1); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_StringFormat_Size2) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(2), "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', '\0', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 1); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_IntFormat_Size2) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(2), "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('4', '\0', '\1', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 1); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_String_Size3) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(3), "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_StringFormat_Size3) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(3), "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_IntFormat_Size3) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(3), "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('4', '2', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_String_Size4) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(4), "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 2); +} + +TEST(FormatTest, FormatToSpan_StringFormat_Size4) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(4), "%s", "ab"); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 2); +} + +TEST(FormatTest, FormatToSpan_IntFormat_Size4) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatToSpan(std_support::span(buffer).first(4), "%d", 42); + EXPECT_THAT(buffer, testing::ElementsAre('4', '2', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 2); +} + +TEST(FormatTest, FormatToSpan_Sequence) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + std_support::span result(buffer); + result = FormatToSpan(result, "a"); + result = FormatToSpan(result, "%s", "b"); + result = FormatToSpan(result, "%d", 4); + EXPECT_THAT(buffer, testing::ElementsAre('a', 'b', '4', '\0', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 3); + EXPECT_THAT(result.size(), 2); +} + +namespace { + +std_support::span FormatNesting(std_support::span buffer) { + buffer = FormatToSpan(buffer, "("); + auto nested = buffer.first(buffer.size() - 1); // Leave a space for `)` + nested = FormatToSpan(nested, "a "); + nested = FormatToSpan(nested, "b "); + buffer = buffer.subspan(nested.data() - buffer.data()); + return FormatToSpan(buffer, ")"); +} + +} // namespace + +TEST(FormatTest, FormatToSpan_Nesting_Size1) { + std::array buffer{'\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('\0')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size2) { + std::array buffer{'\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 1); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size3) { + std::array buffer{'\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size4) { + std::array buffer{'\1', '\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 3); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size5) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 4); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size6) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 5); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size7) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ' ', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 6); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_Nesting_Size8) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNesting(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ' ', ')', '\0', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 6); + EXPECT_THAT(result.size(), 2); +} + +namespace { + +std_support::span FormatNestingWithBacktrack(std_support::span buffer) { + buffer = FormatToSpan(buffer, "("); + auto nested = buffer; + nested = FormatToSpan(nested, "a "); + nested = FormatToSpan(nested, "b "); + if (nested.data() != buffer.data()) { + // If we managed to format something in nested, replace the last formatted character with ')' + buffer = buffer.subspan(nested.data() - buffer.data() - 1); + } + return FormatToSpan(buffer, ")"); +} + +} // namespace + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size1) { + std::array buffer{'\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('\0')); + EXPECT_THAT(result.data(), buffer.data()); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size2) { + std::array buffer{'\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 1); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size3) { + std::array buffer{'\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 2); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size4) { + std::array buffer{'\1', '\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 3); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size5) { + std::array buffer{'\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 4); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size6) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ')', '\0')); + EXPECT_THAT(result.data(), buffer.data() + 5); + EXPECT_THAT(result.size(), 1); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size7) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ')', '\0', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 5); + EXPECT_THAT(result.size(), 2); +} + +TEST(FormatTest, FormatToSpan_NestingWithBacktrack_Size8) { + std::array buffer{'\1', '\1', '\1', '\1', '\1', '\1', '\1', '\1'}; + auto result = FormatNestingWithBacktrack(buffer); + EXPECT_THAT(buffer, testing::ElementsAre('(', 'a', ' ', 'b', ')', '\0', '\1', '\1')); + EXPECT_THAT(result.data(), buffer.data() + 5); + EXPECT_THAT(result.size(), 3); +} diff --git a/kotlin-native/runtime/src/main/cpp/KAssert.cpp b/kotlin-native/runtime/src/main/cpp/KAssert.cpp index 5f7c2235cdd..d7c1a8f57c9 100644 --- a/kotlin-native/runtime/src/main/cpp/KAssert.cpp +++ b/kotlin-native/runtime/src/main/cpp/KAssert.cpp @@ -3,9 +3,13 @@ * that can be found in the LICENSE file. */ +#include "KAssert.h" + +#include #include -#include "Porting.h" +#include "cpp_support/Span.hpp" +#include "Format.h" #include "StackTrace.hpp" using namespace kotlin; @@ -13,22 +17,20 @@ using namespace kotlin; namespace { void PrintAssert(bool allowStacktrace, const char* location, const char* format, std::va_list args) noexcept { - char buf[1024]; - int written = -1; + std::array bufferStorage; + std_support::span buffer(bufferStorage); // Write the title with a source location. if (location != nullptr) { - written = konan::snprintf(buf, sizeof(buf), "%s: runtime assert: ", location); + buffer = FormatToSpan(buffer, "%s: runtime assert: ", location); } else { - written = konan::snprintf(buf, sizeof(buf), "runtime assert: "); + buffer = FormatToSpan(buffer, "runtime assert: "); } // Write the message. - if (written >= 0 && static_cast(written) < sizeof(buf)) { - konan::vsnprintf(buf + written, sizeof(buf) - written, format, args); - } + buffer = VFormatToSpan(buffer, format, args); - konan::consoleErrorUtf8(buf, konan::strnlen(buf, sizeof(buf))); + konan::consoleErrorUtf8(bufferStorage.data(), bufferStorage.size() - buffer.size()); konan::consoleErrorf("\n"); if (allowStacktrace) { kotlin::PrintStackTraceStderr(); diff --git a/kotlin-native/runtime/src/main/cpp/ObjCExceptions.cpp b/kotlin-native/runtime/src/main/cpp/ObjCExceptions.cpp index a3c3ebdbec2..21d7ab2b502 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCExceptions.cpp +++ b/kotlin-native/runtime/src/main/cpp/ObjCExceptions.cpp @@ -10,6 +10,7 @@ extern "C" OBJ_GETTER(Kotlin_Throwable_getStackTrace, KRef throwable); +// TODO: Consider refactoring to `FormatToSpan`. static void writeStackTraceToBuffer(KRef throwable, char* buffer, unsigned long bufferSize) { if (bufferSize < 2) return;