Custom memory functions and thread safety

Hello,

I’ve been working on GUI application and I’m getting my head around the setting of functions for memory management. The documentation of SDL_SetMemoryFunctions says the following

It is not safe to call this function once any allocations have been made

Given that, I assume that the functions are to be defined in the very beginning. However, chances are my project comes across the problem of having these functions to be called safely from different threads. Given that, I need a mutex, but SDL_CreateMutex requires to allocate some memory. Therefore, the memory functions must be ready by the call of SDL_CreateMutex. Therefore, I have some dependency loop.

I have a toy project with physics simulation. It allocates memory once with mmap and gives this chunk of memory to the allocator which manages it with a free list (link) [1].

Within the project, I defined two global variables - the pointer to the allocator and the pointer to the mutex. The memory functions access both of the pointers. The allocator is initialized before the memory functions are set. However, the pointer to mutex remains null. As long as it’s null, the memory functions aren’t safe for the concurrent access - there’s a simple if which checks it. The mutex is created after the memory functions are set. After that the functions become concurrently safe.

It there any better approach to solve the problem?

Best regards,
Aleksei Markov

Can’t you just call SDL_SetMemoryFunctions() before you create any threads? Kinda seems like something you should call at the top of main() before you really do anything.

edit: AFAIK this isn’t something you need to call for every thread.

2 Likes

I can call SDL_SetMemoryFunctions() before I create any threads. In the example the function is called almost at the top of `main()`. However, how does it make the memory functions safe for concurrent access?

It doesn’t, but the default memory functions (like SDL_calloc) are guaranteed thread safe per the documentation.
The SDL documentation does not specify any special compiler flags required.

I’m curious: what are you trying to solve by changing the memory functions, or is it more to settle your own curiosity?

It’s up to you to make your memory functions thread safe

Question is, how is he supposed to do it?

SDL_CreateMutex allocates memory so he’s not supposed to call it before SDL_SetMemoryFunctions.

The only way I can think of is to use something like C11’s mtx_t or pthreads’ pthread_mutex_t, or maybe atomics or thread-local storage…

I also didn’t see the problem at first, that’s why I “liked” your previous post.

1 Like

First off: too much of SDL relies on these functions, don’t do this unless it’s necessary or you’re educating yourself, here be dragons and all that.
So the answer is to not use SDL_Mutex in the memory allocation functions. Perhaps Mutex from another library might be safe here, but SDL does not use any Mutex in their version. The important bit is to avoid SDL functions or structs that themselves need to allocate memory. I’d even avoid most any SDL api functions here.

I might be misunderstanding, but this is how I read the source files;
SDL kind of cheats in that it checks for and uses the standard C memory functions if available. If somehow these functions are not available then it creates its own thread-safe version (below). Note the utter lack of SDL library access. The odd part to me is that, even when building their own versions to replace malloc, they rely on malloc.h _aligned_* functions.

// file: SDL/src/stdlib/SDL_malloc.h

static void SDLCALL real_free(IN void *pPtr)
{
    if (!pPtr) {
        return;
    }

    PBYTE pVirtualAddr = (PBYTE)pPtr;
    SAFE_HEAP_POINTER *pSafePtr = (SAFE_HEAP_POINTER *)(pVirtualAddr - sizeof(SAFE_HEAP_POINTER));
    ULONG ulOldProtect;
    VirtualProtect(pSafePtr->pAddr + pSafePtr->ulSize - GetPageSize(), GetPageSize(), PAGE_READWRITE, &ulOldProtect);
    _aligned_free(pSafePtr->pAddr);
}

static void *SDLCALL real_malloc(IN size_t dwBytes)
{
    DWORD dwTotalBytes = (DWORD)dwBytes + sizeof(SAFE_HEAP_POINTER);
    DWORD dwPages = (dwTotalBytes / GetPageSize()) + 1;
    DWORD dwAlignedBytesCount = (dwPages + 1) * GetPageSize();
    PBYTE pPtr = (PBYTE)_aligned_malloc(dwAlignedBytesCount, GetPageSize());
    if (!pPtr) {
        return NULL;
    }

    ZeroMemory(pPtr, dwAlignedBytesCount);
    PBYTE pLastPageStart = pPtr + dwPages * GetPageSize();
    ULONG ulOldProtect;
    PBYTE pBlock = (PBYTE)(pLastPageStart - dwBytes);
    if (!VirtualProtect(pLastPageStart, GetPageSize(), PAGE_READWRITE | PAGE_GUARD, &ulOldProtect)) {
        _aligned_free(pPtr);
        return NULL;
    }
    SAFE_HEAP_POINTER *pSafePtr = (SAFE_HEAP_POINTER *)(pBlock - sizeof(SAFE_HEAP_POINTER));
    pSafePtr->pAddr = pPtr;
    pSafePtr->ulSize = dwAlignedBytesCount;
    return pBlock;
}

static void *SDLCALL real_calloc(IN size_t dwElements, IN size_t dwElementSize)
{
    PVOID pPtr = real_malloc(dwElements * dwElementSize);
    if (pPtr) {
        ZeroMemory(pPtr, dwElements * dwElementSize);
    }
    return pPtr;
}




static void *SDLCALL real_realloc(IN void *pPtr, IN size_t dwBytes)
{
    if (!pPtr) {
        return real_malloc(dwBytes);
    }

    PBYTE pVirtualAddr = (PBYTE)pPtr;
    SAFE_HEAP_POINTER *pSafePtr = (SAFE_HEAP_POINTER *)(pVirtualAddr - sizeof(SAFE_HEAP_POINTER));
    SAFE_HEAP_POINTER oldPtr = *pSafePtr;
    ULONG ulPrevSize = real_msize(pPtr);
    if (ulPrevSize == dwBytes) {
        return pPtr;
    }

    // Start working on the addresses
    DWORD dwTotalBytes = (DWORD)dwBytes + sizeof(SAFE_HEAP_POINTER);
    DWORD dwNewPages = (dwTotalBytes / GetPageSize()) + 1;
    DWORD dwAlignedBytesCount = (dwNewPages + 1) * GetPageSize();
    PBYTE pBlock = 0;
    PBYTE pLastPageStart = 0;
    if ((dwAlignedBytesCount <= oldPtr.ulSize) && (dwAlignedBytesCount + GetPageSize() >= oldPtr.ulSize)) {
        // No need to reallocate memory, the allocated pages R enough
        pLastPageStart = pSafePtr->pAddr + dwNewPages * GetPageSize();
        pBlock = (pLastPageStart - dwBytes);
        MoveMemory(pBlock, pPtr, min(ulPrevSize, dwBytes));
        pSafePtr = (SAFE_HEAP_POINTER *)(pBlock - sizeof(SAFE_HEAP_POINTER));
        *pSafePtr = oldPtr;
        return pBlock;
    }

    // Buffer was enlarged or reduced by more than PAGE_SIZE
    PBYTE pNew = (PBYTE)real_malloc(dwBytes);
    CopyMemory(pNew, pPtr, min(ulPrevSize, dwBytes));
    real_free(pPtr);
    return pNew;
}

Also, you can just have your physics sim use your custom allocator; there is no need to have SDL also use it.

That implementation uses Windows-specific code and is only used if WIN32_DETECT_OVERWRITE is defined. This is not the default.

By default, if malloc isn’t available, it falls back to using dlmalloc which is defined elsewhere in the same file. It’s hard to follow all the preprocessor logic but it seems to either use spin locks, critical sections (Windows) or pthreads.

1 Like

So many helping messages, I appreciate it. Let me answer them one by one

About the goal: the problem I’m tackling is hypothetical as of now. It might be real in the future. Given that, I’d like to learn the “right” use of SDL for the case. In short, I’m exploring.

About the problem: I’d like to allocate a chunk of memory only once before initializing SDL. This allows me to avoid hick-ups upon memory allocations. I’m getting my head around making sure that the chunk can be accessed from multiple threads.

About options: SDL supports concurrency primitives which allows a programmer to avoid platform-specific code. Given the support, I assume that there’s a well-known way of using only SDL API to have a preallocated chunk of memory accessible from multiple threads. I understand that my specific problem with the preallocated memory allows me to go without a mutex because allocation/deallocation doesn’t involve IO, totally fair.

I’m very interested in your project, it certainly sounds fun, and I think you could take it in several great directions should you get it working.
Please keep us updated here.

Just repeating a few things here, probably, but to be clear:

SDL_SetMemoryFunctions() must be the absolute first thing your app calls at startup if you plan to use it. Many many things will call SDL_malloc() behind the scenes, even things you might not expect would. Anything that can set an error message can allocate memory.

All this function does is set some internal variables. As soon as it returns, you are the allocator.

So while you’re still at the start of your still-single-threaded app, having just become the allocator, create your allocator’s mutex with SDL_CreateMutex().

“But Ryan!,” you say, “now SDL_CreateMutex() will call my allocator, which wants to use the mutex I’m creating!”

That’s okay.

SDL_LockMutex() and SDL_UnlockMutex() will accept a NULL mutex, and return immediately without error; this is intentional behavior for exactly this sort of startup scenario.

So the first time through your allocator, when creating the mutex, you have no lock, but again, you are still a single-threaded app at this point and don’t need a lock yet. Once SDL_CreateMutex() succeeds and the lock is assigned to your allocator’s variable, future allocations will be thread safe, so go ahead and start spinning threads at this point.

When shutting down the system later, hopefully you’re back to one thread. Move the mutex to a local variable, set the static lock variable you normally use to NULL, and then SDL_DestroyMutex() it. Since you’re down to one thread, you don’t have to lock it, and you don’t have to worry about the allocator holding the lock while it is being destroyed, since it’ll see a NULL lock at this point.

2 Likes

Ah, I totally missed this bit in the man page of SDL_LockMutex in SDL_UnlockMutex. I appreciate the help. Thanks a million!

2 Likes