1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include <SDL3/SDL_test.h>
22
23#ifdef HAVE_LIBUNWIND_H
24#define UNW_LOCAL_ONLY
25#include <libunwind.h>
26#ifndef unw_get_proc_name_by_ip
27#define SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
28static bool s_unwind_symbol_names = true;
29#endif
30#endif
31
32#ifdef SDL_PLATFORM_WIN32
33#include <windows.h>
34#include <dbghelp.h>
35
36static struct {
37 SDL_SharedObject *module;
38 BOOL (WINAPI *pSymInitialize)(HANDLE hProcess, PCSTR UserSearchPath, BOOL fInvadeProcess);
39 BOOL (WINAPI *pSymFromAddr)(HANDLE hProcess, DWORD64 Address, PDWORD64 Displacement, PSYMBOL_INFO Symbol);
40 BOOL (WINAPI *pSymGetLineFromAddr64)(HANDLE hProcess, DWORD64 qwAddr, PDWORD pdwDisplacement, PIMAGEHLP_LINE64 Line);
41} dyn_dbghelp;
42
43/* older SDKs might not have this: */
44__declspec(dllimport) USHORT WINAPI RtlCaptureStackBackTrace(ULONG FramesToSkip, ULONG FramesToCapture, PVOID* BackTrace, PULONG BackTraceHash);
45#define CaptureStackBackTrace RtlCaptureStackBackTrace
46
47#endif
48
49/* This is a simple tracking allocator to demonstrate the use of SDL's
50 memory allocation replacement functionality.
51
52 It gets slow with large numbers of allocations and shouldn't be used
53 for production code.
54*/
55
56#define MAXIMUM_TRACKED_STACK_DEPTH 32
57
58typedef struct SDL_tracked_allocation
59{
60 void *mem;
61 size_t size;
62 Uint64 stack[MAXIMUM_TRACKED_STACK_DEPTH];
63 struct SDL_tracked_allocation *next;
64#ifdef SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
65 char stack_names[MAXIMUM_TRACKED_STACK_DEPTH][256];
66#endif
67} SDL_tracked_allocation;
68
69static SDLTest_Crc32Context s_crc32_context;
70static SDL_malloc_func SDL_malloc_orig = NULL;
71static SDL_calloc_func SDL_calloc_orig = NULL;
72static SDL_realloc_func SDL_realloc_orig = NULL;
73static SDL_free_func SDL_free_orig = NULL;
74static int s_previous_allocations = 0;
75static int s_unknown_frees = 0;
76static SDL_tracked_allocation *s_tracked_allocations[256];
77static bool s_randfill_allocations = false;
78static SDL_AtomicInt s_lock;
79
80#define LOCK_ALLOCATOR() \
81 do { \
82 if (SDL_CompareAndSwapAtomicInt(&s_lock, 0, 1)) { \
83 break; \
84 } \
85 SDL_CPUPauseInstruction(); \
86 } while (true)
87#define UNLOCK_ALLOCATOR() do { SDL_SetAtomicInt(&s_lock, 0); } while (0)
88
89static unsigned int get_allocation_bucket(void *mem)
90{
91 CrcUint32 crc_value;
92 unsigned int index;
93 SDLTest_Crc32Calc(&s_crc32_context, (CrcUint8 *)&mem, sizeof(mem), &crc_value);
94 index = (crc_value & (SDL_arraysize(s_tracked_allocations) - 1));
95 return index;
96}
97
98static SDL_tracked_allocation* SDL_GetTrackedAllocation(void *mem)
99{
100 SDL_tracked_allocation *entry;
101 LOCK_ALLOCATOR();
102 int index = get_allocation_bucket(mem);
103 for (entry = s_tracked_allocations[index]; entry; entry = entry->next) {
104 if (mem == entry->mem) {
105 UNLOCK_ALLOCATOR();
106 return entry;
107 }
108 }
109 UNLOCK_ALLOCATOR();
110 return NULL;
111}
112
113static size_t SDL_GetTrackedAllocationSize(void *mem)
114{
115 SDL_tracked_allocation *entry = SDL_GetTrackedAllocation(mem);
116
117 return entry ? entry->size : SIZE_MAX;
118}
119
120static bool SDL_IsAllocationTracked(void *mem)
121{
122 return SDL_GetTrackedAllocation(mem) != NULL;
123}
124
125static void SDL_TrackAllocation(void *mem, size_t size)
126{
127 SDL_tracked_allocation *entry;
128 int index = get_allocation_bucket(mem);
129
130 if (SDL_IsAllocationTracked(mem)) {
131 return;
132 }
133 entry = (SDL_tracked_allocation *)SDL_malloc_orig(sizeof(*entry));
134 if (!entry) {
135 return;
136 }
137 LOCK_ALLOCATOR();
138 entry->mem = mem;
139 entry->size = size;
140
141 /* Generate the stack trace for the allocation */
142 SDL_zeroa(entry->stack);
143#ifdef HAVE_LIBUNWIND_H
144 {
145 int stack_index;
146 unw_cursor_t cursor;
147 unw_context_t context;
148
149 unw_getcontext(&context);
150 unw_init_local(&cursor, &context);
151
152 stack_index = 0;
153 while (unw_step(&cursor) > 0) {
154 unw_word_t pc;
155#ifdef SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
156 unw_word_t offset;
157 char sym[236];
158#endif
159
160 unw_get_reg(&cursor, UNW_REG_IP, &pc);
161 entry->stack[stack_index] = pc;
162
163#ifdef SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
164 if (s_unwind_symbol_names && unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
165 SDL_snprintf(entry->stack_names[stack_index], sizeof(entry->stack_names[stack_index]), "%s+0x%llx", sym, (unsigned long long)offset);
166 }
167#endif
168 ++stack_index;
169
170 if (stack_index == SDL_arraysize(entry->stack)) {
171 break;
172 }
173 }
174 }
175#elif defined(SDL_PLATFORM_WIN32)
176 {
177 Uint32 count;
178 PVOID frames[63];
179 Uint32 i;
180
181 count = CaptureStackBackTrace(1, SDL_arraysize(frames), frames, NULL);
182
183 count = SDL_min(count, MAXIMUM_TRACKED_STACK_DEPTH);
184 for (i = 0; i < count; i++) {
185 entry->stack[i] = (Uint64)(uintptr_t)frames[i];
186 }
187 }
188#endif /* HAVE_LIBUNWIND_H */
189
190 entry->next = s_tracked_allocations[index];
191 s_tracked_allocations[index] = entry;
192 UNLOCK_ALLOCATOR();
193}
194
195static void SDL_UntrackAllocation(void *mem)
196{
197 SDL_tracked_allocation *entry, *prev;
198 int index = get_allocation_bucket(mem);
199
200 LOCK_ALLOCATOR();
201 prev = NULL;
202 for (entry = s_tracked_allocations[index]; entry; entry = entry->next) {
203 if (mem == entry->mem) {
204 if (prev) {
205 prev->next = entry->next;
206 } else {
207 s_tracked_allocations[index] = entry->next;
208 }
209 SDL_free_orig(entry);
210 UNLOCK_ALLOCATOR();
211 return;
212 }
213 prev = entry;
214 }
215 s_unknown_frees += 1;
216 UNLOCK_ALLOCATOR();
217}
218
219static void rand_fill_memory(void* ptr, size_t start, size_t end)
220{
221 Uint8* mem = (Uint8*) ptr;
222 size_t i;
223
224 if (!s_randfill_allocations)
225 return;
226
227 for (i = start; i < end; ++i) {
228 mem[i] = SDLTest_RandomUint8();
229 }
230}
231
232static void * SDLCALL SDLTest_TrackedMalloc(size_t size)
233{
234 void *mem;
235
236 mem = SDL_malloc_orig(size);
237 if (mem) {
238 SDL_TrackAllocation(mem, size);
239 rand_fill_memory(mem, 0, size);
240 }
241 return mem;
242}
243
244static void * SDLCALL SDLTest_TrackedCalloc(size_t nmemb, size_t size)
245{
246 void *mem;
247
248 mem = SDL_calloc_orig(nmemb, size);
249 if (mem) {
250 SDL_TrackAllocation(mem, nmemb * size);
251 }
252 return mem;
253}
254
255static void * SDLCALL SDLTest_TrackedRealloc(void *ptr, size_t size)
256{
257 void *mem;
258 size_t old_size = 0;
259 if (ptr) {
260 old_size = SDL_GetTrackedAllocationSize(ptr);
261 SDL_assert(old_size != SIZE_MAX);
262 }
263 mem = SDL_realloc_orig(ptr, size);
264 if (ptr) {
265 SDL_UntrackAllocation(ptr);
266 }
267 if (mem) {
268 SDL_TrackAllocation(mem, size);
269 if (size > old_size) {
270 rand_fill_memory(mem, old_size, size);
271 }
272 }
273 return mem;
274}
275
276static void SDLCALL SDLTest_TrackedFree(void *ptr)
277{
278 if (!ptr) {
279 return;
280 }
281
282 if (s_previous_allocations == 0) {
283 SDL_assert(SDL_IsAllocationTracked(ptr));
284 }
285 SDL_UntrackAllocation(ptr);
286 SDL_free_orig(ptr);
287}
288
289void SDLTest_TrackAllocations(void)
290{
291 if (SDL_malloc_orig) {
292 return;
293 }
294
295 SDLTest_Crc32Init(&s_crc32_context);
296
297 s_previous_allocations = SDL_GetNumAllocations();
298 if (s_previous_allocations < 0) {
299 SDL_Log("SDL was built without allocation count support, disabling free() validation");
300 } else if (s_previous_allocations != 0) {
301 SDL_Log("SDLTest_TrackAllocations(): There are %d previous allocations, disabling free() validation", s_previous_allocations);
302 }
303#ifdef SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
304 do {
305 /* Don't use SDL_GetHint: SDL_malloc is off limits. */
306 const char *env_trackmem = SDL_getenv_unsafe("SDL_TRACKMEM_SYMBOL_NAMES");
307 if (env_trackmem) {
308 if (SDL_strcasecmp(env_trackmem, "1") == 0 || SDL_strcasecmp(env_trackmem, "yes") == 0 || SDL_strcasecmp(env_trackmem, "true") == 0) {
309 s_unwind_symbol_names = true;
310 } else if (SDL_strcasecmp(env_trackmem, "0") == 0 || SDL_strcasecmp(env_trackmem, "no") == 0 || SDL_strcasecmp(env_trackmem, "false") == 0) {
311 s_unwind_symbol_names = false;
312 }
313 }
314 } while (0);
315
316#elif defined(SDL_PLATFORM_WIN32)
317 do {
318 dyn_dbghelp.module = SDL_LoadObject("dbghelp.dll");
319 if (!dyn_dbghelp.module) {
320 goto dbghelp_failed;
321 }
322 dyn_dbghelp.pSymInitialize = (void *)SDL_LoadFunction(dyn_dbghelp.module, "SymInitialize");
323 dyn_dbghelp.pSymFromAddr = (void *)SDL_LoadFunction(dyn_dbghelp.module, "SymFromAddr");
324 dyn_dbghelp.pSymGetLineFromAddr64 = (void *)SDL_LoadFunction(dyn_dbghelp.module, "SymGetLineFromAddr64");
325 if (!dyn_dbghelp.pSymInitialize || !dyn_dbghelp.pSymFromAddr || !dyn_dbghelp.pSymGetLineFromAddr64) {
326 goto dbghelp_failed;
327 }
328 if (!dyn_dbghelp.pSymInitialize(GetCurrentProcess(), NULL, TRUE)) {
329 goto dbghelp_failed;
330 }
331 break;
332dbghelp_failed:
333 if (dyn_dbghelp.module) {
334 SDL_UnloadObject(dyn_dbghelp.module);
335 dyn_dbghelp.module = NULL;
336 }
337 } while (0);
338#endif
339
340 SDL_GetMemoryFunctions(&SDL_malloc_orig,
341 &SDL_calloc_orig,
342 &SDL_realloc_orig,
343 &SDL_free_orig);
344
345 SDL_SetMemoryFunctions(SDLTest_TrackedMalloc,
346 SDLTest_TrackedCalloc,
347 SDLTest_TrackedRealloc,
348 SDLTest_TrackedFree);
349}
350
351void SDLTest_RandFillAllocations(void)
352{
353 SDLTest_TrackAllocations();
354
355 s_randfill_allocations = true;
356}
357
358void SDLTest_LogAllocations(void)
359{
360 char *message = NULL;
361 size_t message_size = 0;
362 char line[256], *tmp;
363 SDL_tracked_allocation *entry;
364 int index, count, stack_index;
365 Uint64 total_allocated;
366
367 if (!SDL_malloc_orig) {
368 return;
369 }
370
371 message = SDL_realloc_orig(NULL, 1);
372 if (!message) {
373 return;
374 }
375 *message = 0;
376
377#define ADD_LINE() \
378 message_size += (SDL_strlen(line) + 1); \
379 tmp = (char *)SDL_realloc_orig(message, message_size); \
380 if (!tmp) { \
381 return; \
382 } \
383 message = tmp; \
384 SDL_strlcat(message, line, message_size)
385
386 SDL_strlcpy(line, "Memory allocations:\n", sizeof(line));
387 ADD_LINE();
388
389 count = 0;
390 total_allocated = 0;
391 for (index = 0; index < SDL_arraysize(s_tracked_allocations); ++index) {
392 for (entry = s_tracked_allocations[index]; entry; entry = entry->next) {
393 (void)SDL_snprintf(line, sizeof(line), "Allocation %d: %d bytes\n", count, (int)entry->size);
394 ADD_LINE();
395 /* Start at stack index 1 to skip our tracking functions */
396 for (stack_index = 1; stack_index < SDL_arraysize(entry->stack); ++stack_index) {
397 char stack_entry_description[256] = "???";
398
399 if (!entry->stack[stack_index]) {
400 break;
401 }
402#ifdef HAVE_LIBUNWIND_H
403 {
404#ifdef SDLTEST_UNWIND_NO_PROC_NAME_BY_IP
405 if (s_unwind_symbol_names) {
406 (void)SDL_snprintf(stack_entry_description, sizeof(stack_entry_description), "%s", entry->stack_names[stack_index]);
407 }
408#else
409 char name[256] = "???";
410 unw_word_t offset = 0;
411 unw_get_proc_name_by_ip(unw_local_addr_space, entry->stack[stack_index], name, sizeof(name), &offset, NULL);
412 (void)SDL_snprintf(stack_entry_description, sizeof(stack_entry_description), "%s+0x%llx", name, (long long unsigned int)offset);
413#endif
414 }
415#elif defined(SDL_PLATFORM_WIN32)
416 {
417 DWORD64 dwDisplacement = 0;
418 char symbol_buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
419 PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)symbol_buffer;
420 DWORD lineColumn = 0;
421 pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
422 pSymbol->MaxNameLen = MAX_SYM_NAME;
423 IMAGEHLP_LINE64 dbg_line;
424 dbg_line.SizeOfStruct = sizeof(dbg_line);
425 dbg_line.FileName = "";
426 dbg_line.LineNumber = 0;
427
428 if (dyn_dbghelp.module) {
429 if (!dyn_dbghelp.pSymFromAddr(GetCurrentProcess(), entry->stack[stack_index], &dwDisplacement, pSymbol)) {
430 SDL_strlcpy(pSymbol->Name, "???", MAX_SYM_NAME);
431 dwDisplacement = 0;
432 }
433 dyn_dbghelp.pSymGetLineFromAddr64(GetCurrentProcess(), (DWORD64)entry->stack[stack_index], &lineColumn, &dbg_line);
434 }
435 SDL_snprintf(stack_entry_description, sizeof(stack_entry_description), "%s+0x%I64x %s:%u", pSymbol->Name, dwDisplacement, dbg_line.FileName, (Uint32)dbg_line.LineNumber);
436 }
437#endif
438 (void)SDL_snprintf(line, sizeof(line), "\t0x%" SDL_PRIx64 ": %s\n", entry->stack[stack_index], stack_entry_description);
439
440 ADD_LINE();
441 }
442 total_allocated += entry->size;
443 ++count;
444 }
445 }
446 (void)SDL_snprintf(line, sizeof(line), "Total: %.2f Kb in %d allocations", total_allocated / 1024.0, count);
447 ADD_LINE();
448 if (s_unknown_frees != 0) {
449 (void)SDL_snprintf(line, sizeof(line), ", %d unknown frees", s_unknown_frees);
450 ADD_LINE();
451 }
452 (void)SDL_snprintf(line, sizeof(line), "\n");
453 ADD_LINE();
454#undef ADD_LINE
455
456 SDL_Log("%s", message);
457 SDL_free_orig(message);
458}
459