11#include < cstdarg>
22#include < cstdio>
33#include < cstdint>
4+ #include < cstdlib>
5+ #include < string_view>
6+ #include < new>
47
58using 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+
7202extern " 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