Skip to content

Commit 4b20120

Browse files
committed
feat(util): add StrPrintf for single-allocation printf-style formatting
Two-pass vsnprintf pattern (Chromium/Android standard) that measures exact size first, then allocates once. Prevents heap fragmentation on ESP32 (320KB RAM) for large RAG contexts (4KB+). Includes MSVC guard for __attribute__((format)) and 14 test cases.
1 parent 4f76427 commit 4b20120

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
// Copyright (c) 2026 ForestHub. All rights reserved.
3+
// For commercial licensing, visit https://github.com/ForestHubAI/fh-sdk
4+
5+
#ifndef FORESTHUB_UTIL_STRPRINTF_HPP
6+
#define FORESTHUB_UTIL_STRPRINTF_HPP
7+
8+
/// @file
9+
/// Printf-style string formatting with single heap allocation.
10+
11+
#include <cstdarg>
12+
#include <cstdio>
13+
#include <string>
14+
15+
namespace foresthub {
16+
namespace util {
17+
18+
/// Builds a std::string using printf-style formatting.
19+
///
20+
/// Uses a two-pass vsnprintf approach: first measures the exact output size,
21+
/// then allocates once and writes. This avoids heap fragmentation from
22+
/// repeated std::string concatenation on memory-constrained embedded devices.
23+
///
24+
/// @warning The format string must not contain untrusted user input.
25+
/// Pass user data as arguments, never as the format string.
26+
///
27+
/// @param fmt printf-style format string
28+
/// @param ... arguments matching the format specifiers
29+
/// @return formatted std::string (empty on encoding error)
30+
#if defined(__GNUC__) || defined(__clang__)
31+
// GCC/Clang: Enable compile-time format string checking.
32+
// Argument indices: 1 = format string, 2 = first vararg (free function, no implicit 'this')
33+
inline std::string StrPrintf(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
34+
#else
35+
inline std::string StrPrintf(const char* fmt, ...);
36+
#endif
37+
38+
inline std::string StrPrintf(const char* fmt, ...) {
39+
va_list args1, args2;
40+
va_start(args1, fmt);
41+
va_copy(args2, args1);
42+
43+
int len = vsnprintf(nullptr, 0, fmt, args1); // Pass 1: measure exact size
44+
va_end(args1);
45+
46+
if (len <= 0) {
47+
va_end(args2);
48+
return "";
49+
}
50+
51+
std::string result(static_cast<size_t>(len), '\0'); // ONE allocation, exact size
52+
vsnprintf(&result[0], static_cast<size_t>(len) + 1, fmt, args2); // Pass 2: write
53+
va_end(args2);
54+
55+
return result; // RVO - no copy
56+
}
57+
58+
} // namespace util
59+
} // namespace foresthub
60+
61+
#endif // FORESTHUB_UTIL_STRPRINTF_HPP

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ gtest_discover_tests(run_agent_tests)
4848
add_executable(run_util_tests
4949
util/optional_test.cpp
5050
util/schema_test.cpp
51+
util/strprintf_test.cpp
5152
util/ticker_test.cpp
5253
)
5354

tests/util/strprintf_test.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
// Copyright (c) 2026 ForestHub. All rights reserved.
3+
// For commercial licensing, visit https://github.com/ForestHubAI/fh-sdk
4+
5+
#include "foresthub/util/strprintf.hpp"
6+
7+
#include <gtest/gtest.h>
8+
9+
#include <string>
10+
11+
using foresthub::util::StrPrintf;
12+
13+
// ==========================================================================
14+
// 1. Basic Formatting
15+
// ==========================================================================
16+
17+
TEST(StrPrintfTest, EmptyFormatString) {
18+
EXPECT_EQ(StrPrintf(""), "");
19+
}
20+
21+
TEST(StrPrintfTest, PlainTextNoArgs) {
22+
EXPECT_EQ(StrPrintf("hello"), "hello");
23+
}
24+
25+
TEST(StrPrintfTest, StringArg) {
26+
EXPECT_EQ(StrPrintf("name: %s", "Alice"), "name: Alice");
27+
}
28+
29+
TEST(StrPrintfTest, IntArg) {
30+
EXPECT_EQ(StrPrintf("count: %d", 42), "count: 42");
31+
}
32+
33+
TEST(StrPrintfTest, FloatArg) {
34+
std::string result = StrPrintf("temp: %f", 3.14);
35+
EXPECT_EQ(result.substr(0, 10), "temp: 3.14");
36+
}
37+
38+
TEST(StrPrintfTest, MixedArgs) {
39+
EXPECT_EQ(StrPrintf("%s = %d", "x", 5), "x = 5");
40+
}
41+
42+
TEST(StrPrintfTest, PercentEscaping) {
43+
EXPECT_EQ(StrPrintf("100%%"), "100%");
44+
}
45+
46+
// ==========================================================================
47+
// 2. Type-Specific Formatting
48+
// ==========================================================================
49+
50+
TEST(StrPrintfTest, NegativeInt) {
51+
EXPECT_EQ(StrPrintf("n: %d", -42), "n: -42");
52+
}
53+
54+
TEST(StrPrintfTest, BoolAsInt) {
55+
EXPECT_EQ(StrPrintf("flag: %d", static_cast<int>(true)), "flag: 1");
56+
EXPECT_EQ(StrPrintf("flag: %d", static_cast<int>(false)), "flag: 0");
57+
}
58+
59+
TEST(StrPrintfTest, FloatPrecision) {
60+
EXPECT_EQ(StrPrintf("val: %.2f", 3.14159), "val: 3.14");
61+
}
62+
63+
// ==========================================================================
64+
// 3. std::string Integration
65+
// ==========================================================================
66+
67+
TEST(StrPrintfTest, StdStringCStr) {
68+
std::string name = "Bob";
69+
EXPECT_EQ(StrPrintf("hello %s", name.c_str()), "hello Bob");
70+
}
71+
72+
TEST(StrPrintfTest, MultipleStringArgs) {
73+
std::string a = "foo";
74+
std::string b = "bar";
75+
EXPECT_EQ(StrPrintf("%s und Kontext: %s", a.c_str(), b.c_str()), "foo und Kontext: bar");
76+
}
77+
78+
// ==========================================================================
79+
// 4. Large Strings
80+
// ==========================================================================
81+
82+
TEST(StrPrintfTest, LargeString) {
83+
std::string payload(2048, 'X');
84+
std::string result = StrPrintf("data: %s", payload.c_str());
85+
EXPECT_EQ(result.size(), 6 + 2048); // "data: " + payload
86+
EXPECT_EQ(result.substr(0, 6), "data: ");
87+
EXPECT_EQ(result.back(), 'X');
88+
}
89+
90+
TEST(StrPrintfTest, LargeFormatResult) {
91+
// Simulate RAG-style output >4KB
92+
std::string ctx(4096, 'A');
93+
std::string serial(1024, 'B');
94+
std::string result = StrPrintf("%s und Kontext: %s", serial.c_str(), ctx.c_str());
95+
EXPECT_EQ(result.size(), 1024 + 14 + 4096); // serial + " und Kontext: " + ctx
96+
}

0 commit comments

Comments
 (0)