24

Although it seems to be a very common issue, I did not harvest much information: How can I create a safe interface between DLL boundaries regarding memory alloction?

It is quite well-known that

// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b 
MyObject *o = getObject();
delete o;

might certainly lead to crashes. But since interactions like the one above are - as I dare say - not uncommon, there has to be a way to ensure safe memory allocation.

Of course, one could provide

// in DLL a
DLLEXPORT void deleteObject(MyObject* o) { delete o; }

but maybe there are better ways (e.g. smart_ptr?). I read about using custom allocators when dealing with STL containers as well.

So my inquiry is more about general pointers to articles and/or literature dealing with this topic. Are there special fallacies to look out for (exception handling?) and is this problem limited to only DLLs or are UNIX shared objects "inflicted" too?

mats
  • 1,818
  • 3
  • 18
  • 26

8 Answers8

14

As you suggested, you can use a boost::shared_ptr to handle that problem. In the constructor you can pass a custom cleanup function, which could be the deleteObject-Method of the dll that created the pointer. Example:

boost::shared_ptr< MyObject > Instance( getObject( ), deleteObject );

If you do not need a C-Interface for your dll, you can have getObject return a shared_ptr.

Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
9

Overload operator new, operator delete et. al for all your DLL classes and implement them within the DLL:

 void* MyClass::operator new(size_t numb) {
    return ::operator new(num_bytes);
 }

 void MyClass::operator delete(void* p) {
    ::operator delete(p);
 }
 ...

This can easily be placed in a common base class for all classes exported by the DLL.

This way, allocation and deallocation are done entirely on the DLL heap. Honestly, I'm unsure whether it has any serious pitfalls or portability issues - but it works for me.

Alexander Gessler
  • 45,603
  • 7
  • 82
  • 122
  • That's probably not a good idea: https://stackoverflow.com/questions/11846511/new-and-delete-operator-overloading-for-dll – jaba Dec 06 '17 at 10:39
5

You may state that it "might certainly lead to crashes". Funny - "might" means the exact opposite of "certainly".

Now, the statement is mostly historical anyway. There is a very simple solution: Use 1 compiler, 1 compiler setting, and link against the DLL form of the CRT. (And you can probably get away skipping the latter)

There are no specific articles to link to, as this is a non-problem nowadays. You'd need the 1 compiler, 1 setting rule anyway. Simple things as sizeof(std::string) depend on it, and you'd have massive ODR violations otherwise.

MSalters
  • 173,980
  • 10
  • 155
  • 350
3

Some pointers:

legends2k
  • 31,634
  • 25
  • 118
  • 222
dirkgently
  • 108,024
  • 16
  • 131
  • 187
3

It is quite well-known that

// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b 
MyObject *o = getObject();
delete o;

might certainly lead to crashes.

Whether or not the above has a well-defined characteristic depends on how the MyObjecttype is defined.

Iff the class has a virtual destructor, (and that destructor is not defined inline), than it will not crash and will exhibit well-defined behavior.

The reason normally cited for why this crashes is that delete does two things:

  • call destructor
  • free memory (by calling operator delete(void* ...))

For a class with a non-virtual destructor, it can do these things "inline", which leads to the situation that delete inside DLL "b" may try to free memory from the "a" heap == crash.

However, if the destructor of MyObject is virtual then before calling the "free" function, the compiler needs to determine the actual run-time class of the pointer before it can pass the correct pointer to operator delete():

C++ mandates that you must pass the exact same address to operator delete as what operator new returns. When you’re allocating an object using new, the compiler implicitly knows the concrete type of the object (which is what the compiler uses to pass in the correct memory size to operator new, for example.)

However, if your class has a base class with a virtual destructor, and your object is deleted through a pointer to the base class, the compiler doesn’t know the concrete type at the call site, and therefore cannot compute the correct address to pass to operator delete(). Why, you may ask? Because in presence of multiple inheritance, the base class pointer’s address may be different to the object’s address in memory.

So, what happens in that case is that when you delete an object which has a virtual destructor, the compiler calls what is called a deleting destructor instead of the usual sequence of a call to the normal destructor followed by operator delete() to reclaim the memory.

Since the deleting destructor is a virtual function, at runtime the implementation of the concrete type will be called, and that implementation is capable of computing the correct address for the object in memory. What that implementation does is call the regular destructor, compute the correct address of the object, and then call operator delete() on that address.

It seems that both GCC (from the linked article) and MSVC achieve this by calling both the dtor as well as the "free" function from the context of a "deleting destructor". And this helper by necessity lives inside your DLL and will always use the correct heap, even if "a" and "b" have a different one.

Community
  • 1
  • 1
Martin Ba
  • 37,187
  • 33
  • 183
  • 337
2

Another option which may be applicable in some circumstances is to keep all allocation and deallocate inside the DLL and prevent the object crossing that boundary. You can do this by providing a handle so that creating a MyObject creates it inside the DLL code and returns a simple handle (eg an unsigned int) through which all operations by the client are performed:

// Client code
ObjHandle h=dllPtr->CreateObject();
dllPtr->DoOperation(h);
dllPtr->DestroyObject(h);

Since all the allocation happens inside the dll you can ensure that it gets cleaned up by wrapping in a shared_ptr. This is pretty much the method suggested by John Lakos in Large Scale C++.

the_mandrill
  • 29,792
  • 6
  • 64
  • 93
  • Thank you for reminding me to pick up and read/skim through Lakos! Its reputation is seemingly shadowed by the "two Meyers", but then again it's a different topic. – mats Feb 15 '10 at 13:50
1

I have written an article about using C++11's custom deleter facilities of unique_ptr to pass objects through DLL boundaries (or shared object libraries in Linux). The method described in the article do not "pollute" the unique_ptr signature with the deleter.

  • 1
    I read through this article and it seems quite useful, but it leaves a lot of questions. I think it would be really helpful to add an example that doesn't involve class + singleton + static getInstance() function, but instead defines a class and some more global factory method. I still am not quite clear whether that inline func is running in DLL or in main as well. It is interesting though! – netpoetica Apr 27 '21 at 20:33
  • @netpoetica The singleton is indeed totally superfluous and you are right, I should update the article without it. In order to find out if the inline function is indeed in the DLL or not you could look at the binaries for the symbol. – Philippe Cayouette Apr 28 '21 at 12:03
  • @netpoetica I did the looking myself about the presence of the create() method in the DLL using Dependencies and to my surprise, it is actually part of the DLL. Thank you for bringing this to my attention! – Philippe Cayouette Apr 28 '21 at 12:47
  • @netpoetica Alright, I fixed it. The problem came from the __declspec(dllexport/dllimport), it was set on the whole factory class, I move it to the doCreate function and now I don't see the create() method in the DLL's exported symbols. The debugger corroborates this as well when I look at the call stack I see ExecutableConsumer.exe!SafeFactory::create() when I am in the create() function. I will update the article with this and the singleton removal. Thanks again! – Philippe Cayouette Apr 28 '21 at 13:34
1

In a "layered" architecture (very common scenario) the deepest lying component is responsible for providing a policy on the question (could be returning shared_ptr<> as suggested above or "the caller is responsible for deleting this" or "never delete this, but call releaseFooObject() when done and don't access it afterwards" or ...) and the component nearer the user is responsible for following that policy.

Bi-directional information flow makes the responsibilities harder to characterize.


is this problem limited to only DLLs or are UNIX shared objects "inflicted" too?

Actually it's worse than that: you can have this problem just as easily with statically linked libraries. It is the existence of code boundaries inside a single execution context that makes for a chance to misuse or mis-communicate about some facility.

dmckee --- ex-moderator kitten
  • 98,632
  • 24
  • 142
  • 234
  • 1
    @happy_emi: Yes. There is. The problem is a consequence of code boundaries not of operating systems. Unix has some coding traditions that make it come up less often, but it is still possible. – dmckee --- ex-moderator kitten May 30 '12 at 16:52