4
\$\begingroup\$

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 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 <dll.h>

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?

\$\endgroup\$
2
\$\begingroup\$

The standard (and recommended) way to solve this is for each module to provide its own exported 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.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your detailed explanation. Do both exported AllocMem/FreeMem and LocalAlloc/LocalFree solutions need cross_boundaries_int_buffer function to return also handle to allocated memory block to let client correctly free it? \$\endgroup\$ – user1832484 Jan 25 '17 at 10:13
  • \$\begingroup\$ If you're C++11, use std::shared_ptr Is it safe to exchange STL objects between different compilers? In any case I have to deal with Jurassic VS6, so the best I can afford is auto_ptr. \$\endgroup\$ – user1832484 Jan 25 '17 at 10:26
  • \$\begingroup\$ No. I have no idea what cross_boundaries_int_buffer is supposed to do, other than initialize the buffer. The client can initialize the buffer just fine. There are no cross-boundary problems with initialization of ints! The issue is making sure that the same module that allocated the memory is the one that freed it—this is what my proposal provides for. Good point about VS 6, I'm not sure if Boost still supports that old version. No, standard library objects aren't interchangeable, but there's no problem here with returning an object. It's just like returning any user-defined class object. \$\endgroup\$ – Cody Gray Jan 25 '17 at 10:31
  • \$\begingroup\$ Thank you again for your patience. cross_boundaries_int_buffer is supposed to return a compile-time unknown number of ints, but if there's no problem here with returning an object am I safe just returning a vector<int>? Or I misunderstood your statement? \$\endgroup\$ – user1832484 Jan 25 '17 at 11:01
  • \$\begingroup\$ It sounds like you still have far too many questions for a Code Review. The reason you generally can't use the C++ standard library is because it is a template library and you don't have control over how the memory is being allocated and freed. An auto-deleting smart pointer solves that problem, as I described in the answer. cross_boundaries_int_buffer does nothing, it is just a fancy way of calling a caller-defined allocator. The core of the problem is that allocator, making sure that it matches across DLL boundaries. Maybe you will like someone else's answer better. \$\endgroup\$ – Cody Gray Jan 25 '17 at 11:07
0
\$\begingroup\$

A simpler solution would be to let the calling code be responsible for all memory buffer allocations.

This changes the example code to:

void client_func()
{
  size_t buffer_size = cross_boundaries_int_buffer_required_size()
  int *buffer = new int[buffer_size];
  cross_boundaries_int_buffer(buffer, buffer_size); // just takes the buffer and its length
  /* use buffer elements as needed */
  delete[] buffer;
}

This requires that the required length of the buffer must be queryable. But having the length explicitly in code like that make it easier to avoid buffer overflow bugs.

The other option is to make an int_buffer a object that requires initialization:

void client_func()
{
  int_buffer buffer={};
  cross_boundaries_int_buffer_init(&buffer);

  int *buffer_ptr = cross_boundaries_int_buffer_get_pointer(&buffer);

  /* use buffer elements as needed */
  cross_boundaries_int_buffer_destroy(&buffer);
}

Here the dll is the one doing all the allocations and calling code queries the pointer only to access the elements.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.