From 366f3d26e50932095c5e4838438116e49e68ffbe Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 8 Jul 2026 20:25:13 +0200 Subject: [PATCH] Replace snprintf with a branch-free writer for \uXXXX escapes (#5235) * Replace snprintf with a branch-free writer for \uXXXX escapes dump_escaped called std::snprintf(..., "\u%04x", ...) once per escaped code point in the string serialization hot path. snprintf re-parses the format string and pulls in locale/printf machinery on every call, which is far heavier than the fixed 6-/12-byte output warrants. This is hot for any string containing control characters, and for all non-ASCII text when ensure_ascii is set. Replace it with write_u_escape, a small helper that writes the escape directly into string_buffer via a nibble-to-hex lookup table, mirroring the existing hand-rolled dump_integer fast path in the same file. Signed-off-by: Niels Lohmann * Fix clang-tidy avoid-c-arrays warning in write_u_escape Use a const char* rather than a char[] lookup table, matching the existing hex_bytes helper in the same file. Signed-off-by: Niels Lohmann * :recycle: adjust write_u_escape signature Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/output/serializer.hpp | 38 ++++++++++++++----- single_include/nlohmann/json.hpp | 38 ++++++++++++++----- tests/src/unit-convenience.cpp | 26 +++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 25a64d648..0b608f8e2 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -465,18 +465,12 @@ class serializer { if (codepoint <= 0xFFFF) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x", - static_cast(codepoint))); - bytes += 6; + write_u_escape(bytes, static_cast(codepoint)); } else { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", - static_cast(0xD7C0u + (codepoint >> 10u)), - static_cast(0xDC00u + (codepoint & 0x3FFu)))); - bytes += 12; + write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } else @@ -683,6 +677,32 @@ class serializer return result; } + /*! + * @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer + * + * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the + * string escaping hot path. It writes exactly six characters ('\\', 'u' and + * four hex digits) at position @a pos of @a string_buffer via a nibble + * lookup table, avoiding the format-string parsing and locale machinery of + * `snprintf`. Advances @a pos by the number of bytes written (6). + * + * @param[in] pos position in @a string_buffer to write at; there must + * be at least 6 bytes of headroom + * @param[in] codeunit 16-bit value to encode + */ + void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept + { + JSON_ASSERT(string_buffer.size() - pos >= 6); + constexpr const char* nibble_to_hex = "0123456789abcdef"; + string_buffer[pos + 0] = '\\'; + string_buffer[pos + 1] = 'u'; + string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; + string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; + string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; + string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + pos += 6; + } + // templates to avoid warnings about useless casts template ::value, int> = 0> bool is_negative_number(NumberType x) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5c022253a..a63e3326b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19962,18 +19962,12 @@ class serializer { if (codepoint <= 0xFFFF) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x", - static_cast(codepoint))); - bytes += 6; + write_u_escape(bytes, static_cast(codepoint)); } else { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - static_cast((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", - static_cast(0xD7C0u + (codepoint >> 10u)), - static_cast(0xDC00u + (codepoint & 0x3FFu)))); - bytes += 12; + write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } else @@ -20180,6 +20174,32 @@ class serializer return result; } + /*! + * @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer + * + * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the + * string escaping hot path. It writes exactly six characters ('\\', 'u' and + * four hex digits) at position @a pos of @a string_buffer via a nibble + * lookup table, avoiding the format-string parsing and locale machinery of + * `snprintf`. Advances @a pos by the number of bytes written (6). + * + * @param[in] pos position in @a string_buffer to write at; there must + * be at least 6 bytes of headroom + * @param[in] codeunit 16-bit value to encode + */ + void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept + { + JSON_ASSERT(string_buffer.size() - pos >= 6); + constexpr const char* nibble_to_hex = "0123456789abcdef"; + string_buffer[pos + 0] = '\\'; + string_buffer[pos + 1] = 'u'; + string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; + string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; + string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; + string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + pos += 6; + } + // templates to avoid warnings about useless casts template ::value, int> = 0> bool is_negative_number(NumberType x) diff --git a/tests/src/unit-convenience.cpp b/tests/src/unit-convenience.cpp index f5104855f..037a4e589 100644 --- a/tests/src/unit-convenience.cpp +++ b/tests/src/unit-convenience.cpp @@ -168,6 +168,32 @@ TEST_CASE("convenience functions") CHECK_THROWS_WITH_AS(check_escaped("\xC2"), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2", json::type_error&); } + SECTION("string escape with ensure_ascii") + { + // control characters are escaped regardless of ensure_ascii + check_escaped("\x01", "\\u0001", true); + check_escaped("\x1f", "\\u001f", true); + + // non-ASCII code points in the Basic Multilingual Plane are emitted as + // a single lowercase \uXXXX escape (exercises every nibble position) + check_escaped("\xC2\x80", "\\u0080", true); // U+0080 + check_escaped("\xC3\xBF", "\\u00ff", true); // U+00FF (ÿ) + check_escaped("\xDF\xBF", "\\u07ff", true); // U+07FF + check_escaped("\xE4\xBD\xA0", "\\u4f60", true); // U+4F60 (你) + check_escaped("\xEA\xAF\x8D", "\\uabcd", true); // U+ABCD + check_escaped("\xEF\xBF\xBD", "\\ufffd", true); // U+FFFD (replacement char, all-f nibbles) + + // code points outside the BMP are emitted as a UTF-16 surrogate pair + // of two lowercase \uXXXX escapes + check_escaped("\xF0\x90\x80\x80", "\\ud800\\udc00", true); // U+10000 (lowest astral) + check_escaped("\xF0\x9F\x98\x80", "\\ud83d\\ude00", true); // U+1F600 (😀) + check_escaped("\xF4\x8F\xBF\xBF", "\\udbff\\udfff", true); // U+10FFFF (highest code point) + + // with ensure_ascii disabled, non-ASCII input is passed through verbatim + check_escaped("\xE4\xBD\xA0", "\xE4\xBD\xA0", false); + check_escaped("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", false); + } + SECTION("string concat") { using nlohmann::detail::concat;