Skip to content

Commit 584184c

Browse files
committed
letsplay_retro_frontend: Rewrite C++ log helper to use a string pool for formatted log messages
This allows a formatted string to be any length above without any issues. Also avoids buffer overflows (especially of the stack kind.) since we size based on what vsnprintf() returns. Note that I haven't found a core that would break the previous implementation, but it was a hack and this feels a bit less not like a hack.
1 parent 6fe404f commit 584184c

File tree

1 file changed

+219
-8
lines changed

1 file changed

+219
-8
lines changed
Lines changed: 219 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,204 @@
11
#include <cstdarg>
22
#include <cstdio>
33
#include <cstdint>
4+
#include <cstdlib>
5+
#include <string_view>
6+
#include <new>
47

58
using LibRetroLogLevel = std::uint32_t;
69

10+
// set to 1 to enable debug messages for the string pool
11+
#define STRINGPOOL_DEBUG 0
12+
13+
#if STRINGPOOL_DEBUG
14+
#define STRINGPOOL_DEBUG_PRINTF(fmt, ...) printf("stringpool debug: " fmt, ##__VA_ARGS__)
15+
#else
16+
#define STRINGPOOL_DEBUG_PRINTF(fmt, ...)
17+
#endif
18+
19+
/// A very simple string pool implemented using a linked list as a very bad freelist.
20+
struct StringPool {
21+
/// The max amount of strings that can be in the string pool's freelist.
22+
constexpr static auto kMaxStringPoolSize = 8;
23+
24+
/// A pooled string.
25+
struct PooledString {
26+
PooledString() = default;
27+
PooledString(const PooledString&) = delete;
28+
PooledString(PooledString&&) = delete;
29+
30+
std::string_view GetView() {
31+
return std::string_view(GetPointer(), length);
32+
}
33+
34+
char* GetPointer() {
35+
// String memory is allocated after this header, so we can just use this.
36+
return reinterpret_cast<char*>(this + 1);
37+
}
38+
39+
std::size_t GetLength() const {
40+
return length;
41+
}
42+
43+
protected:
44+
friend StringPool;
45+
PooledString* freeListNext;
46+
std::size_t length;
47+
};
48+
49+
StringPool() = default;
50+
StringPool(const StringPool&) = delete;
51+
StringPool(StringPool&&) = delete;
52+
53+
~StringPool() {
54+
Clear();
55+
}
56+
57+
/// Either gets a string from the string pool with a suitable capacity,
58+
/// or if no string was found with a suitable capacity, allocates one.
59+
///
60+
/// May return nullptr if allocating a string (if this function had to allocate) fails.
61+
///
62+
/// The return value of this function MUST be provided to [StringPool::ReturnString] when
63+
/// the string is no longer in active use.
64+
PooledString* GetString(std::size_t wantedCapacity) {
65+
PooledString* pIter = freeListHead;
66+
while(pIter) {
67+
// We were able to find a pooled string which
68+
// has a suitable capacity. Simply return that string
69+
if(pIter->length >= wantedCapacity) {
70+
STRINGPOOL_DEBUG_PRINTF("Found string in freelist with suitable capacity %lu!!!\n", pIter->capacity);
71+
return pIter;
72+
}
73+
pIter = pIter->freeListNext;
74+
}
75+
76+
STRINGPOOL_DEBUG_PRINTF("Could not find string for capacity %lu, allocating\n", wantedCapacity);
77+
78+
// Give up and allocate a new string not on the freelist.
79+
return AllocateString(wantedCapacity);
80+
}
81+
82+
/// "Returns" a string from the string pool. If the string is not in the freelist,
83+
/// it is added to the freelist, otherwise nothing happens.
84+
void ReturnString(PooledString* pPooledString) {
85+
if(pPooledString == nullptr)
86+
return;
87+
88+
bool foundInList = false;
89+
90+
if(freeListHead != nullptr) {
91+
PooledString* pFindIter = freeListHead;
92+
while(pFindIter) {
93+
if(pFindIter == pPooledString) {
94+
STRINGPOOL_DEBUG_PRINTF("Pooled string %p was found in freelist (%p)\n", pPooledString, pFindIter);
95+
foundInList = true;
96+
break;
97+
}
98+
99+
pFindIter = pFindIter->freeListNext;
100+
}
101+
102+
if(!foundInList) {
103+
STRINGPOOL_DEBUG_PRINTF("Did not find pooled string %p in list\n", pPooledString);
104+
}
105+
}
106+
107+
// The string is already in the pool's freelist,
108+
// so we do not need to re-add it.
109+
if(foundInList)
110+
return;
111+
112+
STRINGPOOL_DEBUG_PRINTF("Current freelist size: %lu\n", PoolSize());
113+
114+
// Make sure the pool doesn't get so large that we start to be more of a memory leak.
115+
if(PoolSize() >= kMaxStringPoolSize) {
116+
STRINGPOOL_DEBUG_PRINTF("Clearing freelist, it is too large.\n");
117+
Clear();
118+
}
119+
120+
if(freeListHead == nullptr) {
121+
STRINGPOOL_DEBUG_PRINTF("First freelist node\n");
122+
freeListHead = pPooledString;
123+
} else {
124+
STRINGPOOL_DEBUG_PRINTF("Not the first freelist node\n");
125+
126+
// Walk the freelist for a node which has a null next pointer.
127+
// We will insert there.
128+
PooledString* pInsertIter = freeListHead;
129+
while(true) {
130+
if(pInsertIter->freeListNext == nullptr)
131+
break;
132+
133+
STRINGPOOL_DEBUG_PRINTF("DEBUG: node %p, next %p\n", pInsertIter, pInsertIter->freeListNext);
134+
pInsertIter = pInsertIter->freeListNext;
135+
}
136+
137+
pInsertIter->freeListNext = pPooledString;
138+
}
139+
}
140+
141+
/// Clears the pool's freelist.
142+
void Clear() {
143+
PooledString* pIter = freeListHead;
144+
while(pIter) {
145+
// Need to store the possible next (or lack thereof)
146+
// since we are freeing the pooled string immediately.
147+
//
148+
// Bad, but it uses 16 bytes of stack space at the most,
149+
// compared to needing a list of pointers to free or something.
150+
auto next = pIter->freeListNext;
151+
FreeString(pIter);
152+
pIter = next;
153+
}
154+
155+
freeListHead = nullptr;
156+
}
157+
158+
/// Returns the size of the pool's freelist.
159+
std::size_t PoolSize() {
160+
// It is empty.
161+
if(freeListHead == nullptr)
162+
return 0;
163+
164+
PooledString* pInsertIter = freeListHead;
165+
std::size_t i = 0;
166+
while(pInsertIter) {
167+
i++;
168+
pInsertIter = pInsertIter->freeListNext;
169+
}
170+
171+
return i;
172+
}
173+
174+
private:
175+
176+
PooledString* AllocateString(std::size_t length) {
177+
auto pAlloced = calloc((length + 1 * sizeof(char)) + sizeof(PooledString), 1);
178+
if(pAlloced == nullptr)
179+
return nullptr;
180+
181+
// The "hip" way of doing this is e.g: std::start_lifetime_as,
182+
// but placement new works just fine, and accomplishes the same goal.
183+
auto pString = new (pAlloced) PooledString;
184+
185+
// Initialize the pooled string
186+
pString->length = length;
187+
pString->freeListNext = nullptr;
188+
//pString->pString = reinterpret_cast<char*>(pAlloced) + sizeof(PooledString);
189+
return pString;
190+
}
191+
192+
void FreeString(PooledString* pPooled) {
193+
pPooled->~PooledString();
194+
free(pPooled);
195+
}
196+
197+
PooledString* freeListHead;
198+
};
199+
200+
StringPool TheStringPool;
201+
7202
extern "C" {
8203

9204
/// This function is defined in Rust and recieves our formatted log messages.
@@ -14,22 +209,38 @@ extern "C" {
14209
///
15210
/// By implementing it in C++, we can dodge all that and keep using stable rustc.
16211
void letsplay_retro_frontend_libretro_log(LibRetroLogLevel level, const char* format, ...) {
17-
char buf[512]{};
18212
va_list val;
19213

214+
// First query how long the formatted string would be.
20215
va_start(val, format);
21-
auto n = std::vsnprintf(&buf[0], sizeof(buf)-1, format, val);
216+
auto formatLength = std::vsnprintf(nullptr, 0, format, val);
217+
if(formatLength == -1)
218+
return;
22219
va_end(val);
23220

24-
// Failed to format for some reason, just give up.
25-
if(n == -1)
221+
// Try and find (possibly allocating) a string on the string pool with that length.
222+
// If allocating a string fails, give up entirely.
223+
auto* pString = TheStringPool.GetString(formatLength);
224+
if(pString == nullptr)
26225
return;
27226

28-
// Remove the last newline and replace it with a null terminator.
29-
if(buf[n-1] == '\n')
30-
buf[n-1] = '\0';
227+
auto ptr = pString->GetPointer();
228+
229+
va_start(val, format);
230+
// Format the string
231+
std::vsnprintf(ptr, formatLength, format, val);
232+
va_end(val);
233+
234+
// Remove the last newline and replace it with a null terminator, since
235+
// Tracing will write a newline on its own.
236+
if(ptr[formatLength-1] == '\n')
237+
ptr[formatLength-1] = '\0';
31238

32239
// Call the Rust-side reciever.
33-
return letsplay_retro_frontend_log(level, &buf[0]);
240+
letsplay_retro_frontend_log(level, ptr);
241+
242+
// Return the string back to the pool, adding it to the list of
243+
// now freed strings that we can re-use.
244+
TheStringPool.ReturnString(pString);
34245
}
35246
}

0 commit comments

Comments
 (0)