patterncppMinor
Safely handling memory allocation across DLL boundaries in Windows
Viewed 0 times
handlingdllboundariesmemoryacrosswindowssafelyallocation
Problem
I know that, in Windows, memory must be deallocated in the same module that allocated it.
I have 2 DLLs built with different C++ compilers (VS 6 and VS 2015). In the VS 2015 module, I have an exported function which returns a variable number of items (let's say of
I'm trying to automate as much as possible the use of the VS 2015 DLL from the VS 6 client, so I came up with this in the VS 2015 project:
and
This allows the VS 6 client DLL to do:
Is this a safe way to deal with the problem? Is there any better way?
I have 2 DLLs built with different C++ compilers (VS 6 and VS 2015). In the VS 2015 module, I have an exported function which returns a variable number of items (let's say of
int type), so I need a variable-sized buffer.I'm trying to automate as much as possible the use of the VS 2015 DLL from the VS 6 client, so I came up with this in the VS 2015 project:
/* dll.h */
#if EXPORTS // EXPORTS is defined just in VC2015 project
# define EXPORTED __declspec(dllexport)
#else
# define EXPORTED __declspec(dllimport)
#endif
typedef int*(*Allocator_int__t)(size_t size);
int* allocator_int(size_t size);
#ifndef EXPORTS
inline int* allocator_int(size_t size){ return new int[size]; }
#endif
EXPORTED void cross_boundaries_int_buffer(int*& buffer, Allocator_int__t a = allocator_int);and
/* dll.cpp */
EXPORTED void cross_boundaries_int_buffer(int*& buffer, Allocator_int__t a/* = allocator_int*/)
{
buffer = a(10);
buffer[0] = 0;
/* ... */
buffer[9] = 9;
}This allows the VS 6 client DLL to do:
#include
void client_func()
{
int *buffer = NULL;
cross_boundaries_int_buffer(buffer); // default argument uses default allocator, **which is defined in client dll**
/* use buffer elements as needed */
delete[] buffer;
}Is this a safe way to deal with the problem? Is there any better way?
Solution
The standard (and recommended) way to solve this is for each module to provide its own exported
This is what Windows DLLs do (for example, the Network API functions provide
Regardless of their names, the implementation of these functions is downright trivial.
Header:
Implementation:
You then follow the simple rule that every call to
For additional robustness, you might want to add error checking and even modify the function signatures to make extra information available to the callee for this purpose and/or return a status code to the caller.
Along similar lines lines, if you don't actually need general-purpose memory allocation, you might modify the function signatures to allocate only a certain type of memory and thus make them harder to use incorrectly. For example,
These wrappers actually give you a lot of power. If diagnostics indicate that you have memory-fragmentation issues, you can add fixed-size allocation logic or switch over to the low fragmentation heap without introducing breaking behavior that would affect your clients.
As far as your goal of making it easy for the client to use, no competent programmer is going to have a hard time understanding how to use
Alternatively, if you don't want to add additional exported functions to your libraries, you can just ensure that all of your code is standardized to use a single external memory allocator. For example, if you are targeting the Windows API, you can use either
I should also point out that if you are writing strictly C++ code and don't have any need for a lowest-common-denominator "C" API, then you can just return a shared smart pointer object from any function that needs to allocate memory. In addition to the obvious benefit of preventing the possibility of memory leaks and vastly simplifying the client code, smart pointer objects remember their associated deleter, thus guaranteeing that the memory will be correctly deallocated, even across module boundaries. If you're C++11, use
AllocMem and FreeMem functions (or whatever you want to call them—the names are not important).This is what Windows DLLs do (for example, the Network API functions provide
NetApiBufferAllocate and NetApiBufferFree to handle memory management), and you should strongly consider following the same model in your own project.Regardless of their names, the implementation of these functions is downright trivial.
AllocMem just needs to call new (or malloc if you're using C), and FreeMem just needs to call delete (or free if you're using C). They are just wrappers.Header:
EXPORTED void* AllocMem(size_t cb);
EXPORTED void FreeMem(void* ptr);Implementation:
EXPORTED void* AllocMem(size_t cb)
{
return new char[cb];
}
EXPORTED void FreeMem(void* ptr)
{
delete[] ptr;
}You then follow the simple rule that every call to
AllocMem is matched with a corresponding call to FreeMem from the same module. This ensures that the block of memory is always deallocated by the same module that allocated it.For additional robustness, you might want to add error checking and even modify the function signatures to make extra information available to the callee for this purpose and/or return a status code to the caller.
Along similar lines lines, if you don't actually need general-purpose memory allocation, you might modify the function signatures to allocate only a certain type of memory and thus make them harder to use incorrectly. For example,
AllocIntArray could always allocate an array of integers, taking as its only parameter the length of the array, and FreeIntArray would free a block of memory allocated by AllocIntArray.These wrappers actually give you a lot of power. If diagnostics indicate that you have memory-fragmentation issues, you can add fixed-size allocation logic or switch over to the low fragmentation heap without introducing breaking behavior that would affect your clients.
As far as your goal of making it easy for the client to use, no competent programmer is going to have a hard time understanding how to use
AllocMem and FreeMem functions. Personally, I make it a rule that all memory a caller needs to be freed must be explicitly allocated by the caller. This makes memory-management responsibilities significantly easier to reason about. But if you don't have such a rule (and I can't encourage you to adopt one, because you care more about simplicity than correctness), you could just have the DLL allocate the memory internally by calling the AllocMem function and return that block. Then, the client will just follow the rule that any memory it gets from the DLL, whether implicitly or explicitly, must be freed by calling FreeMem. There are Win32 functions that follow this model, too.Alternatively, if you don't want to add additional exported functions to your libraries, you can just ensure that all of your code is standardized to use a single external memory allocator. For example, if you are targeting the Windows API, you can use either
LocalAlloc and LocalFree, or CoTaskMemAlloc and CoTaskMemFree. This is safe because these allocators are universal and don't depend on the calling module.I should also point out that if you are writing strictly C++ code and don't have any need for a lowest-common-denominator "C" API, then you can just return a shared smart pointer object from any function that needs to allocate memory. In addition to the obvious benefit of preventing the possibility of memory leaks and vastly simplifying the client code, smart pointer objects remember their associated deleter, thus guaranteeing that the memory will be correctly deallocated, even across module boundaries. If you're C++11, use
std::shared_ptr; otherwise, you can use Boost's shared_ptr.Code Snippets
EXPORTED void* AllocMem(size_t cb);
EXPORTED void FreeMem(void* ptr);EXPORTED void* AllocMem(size_t cb)
{
return new char[cb];
}
EXPORTED void FreeMem(void* ptr)
{
delete[] ptr;
}Context
StackExchange Code Review Q#153559, answer score: 3
Revisions (0)
No revisions yet.