Using Shared_Ptr in Dll-Interfaces

Is it OK to use boost::shared ptr in DLL interface?

According to Scott Meyers in Effective C++ (3rd Edition), shared_ptrs are safe across dll boundaries. The shared_ptr object keeps a pointer to the destructor from the dll that created it.

In his book in Item 18 he states, "An especially nice feature of
tr1::shared_ptr is that it automatically uses its per-pointer deleter
to eliminate another potential client error, the "cross-DLL problem."
This problem crops up when an object is created using new in one
dynamically linked library (DLL) but is deleted in a different DLL. On
many platforms, such cross-DLL new/delete pairs lead to runtime
errors. tr1::shared_ptr avoid the problem, because its default deleter
uses delete from the same DLL where the tr1::shared_ptr is created."

Tim Lesher has an interesting gotcha to watch for, though, that he mentions here. You need to make sure that the DLL that created the shared_ptr isn't unloaded before the shared_ptr finally goes out of scope. I would say that in most cases this isn't something you have to watch for, but if you're creating dlls that will be loosely coupled then I would recommend against using a shared_ptr.

Another potential downside is making sure both sides are created with compatible versions of the boost library. Boost's shared_ptr has been stable for a long while. At least since 1.34 it's been tr1 compatible.

How to create a shared_ptr in dll and export it via a factory function?

Straight to the point, to return C++ object from C function - just returns it via output arguments:

extern "C"
{
DLL_API void CreatreMyClassWrapper(SIZE inputSize, SomeClass* outputPtr)
{
*outputPtr = SomeClass(....);
}
}

In your example SomeClass == MyClassWrapperPtr, so:

extern "C"
{
DLL_API void CreatreMyClassWrapper(SIZE inputSize, MyClassWrapperPtr* outputPtr)
{
*outputPtr = make_shared<MyClassWrapper>(inputSize);
}
}

Consider however to change your interface a little, because in current shape you need to be sure that your applications and DLLs shall use the same compiler, linker, settings, libraries...*

You might want to export Create and Delete from your DLL to be sure memory management will occur in your DLL (this is based on this answer:

DLL

extern "C"
{
DLL_API MyClassWrapper* CreateMyClassWrapper(SIZE inputSize)
{
return new MyClassWrapper(inputSize);
}
DLL_API void DeleteMyClassWrapper(MyClassWrapper* wrapper)
{
delete wrapper;
}
}

Application

     shared_ptr<MyClassWrapper> myClassWrapper(CreateMyClassWrapper(inputSize), 
DeleteMyClassWrapper);

Correct use of shared_ptr to eliminate deallocation across DLL boundaries

You're right with both statements. A second correct way would be to return a raw pointer by createObject(..), initialize a shared_ptr with it and pass a custom deleter to the shared_ptr. The custom deleter is a library function like releaseObject(..).

Edit:
With your version (createObject(..) returns a shared_ptr<..>) you're bound to a specific shared_ptr implementation of the library and the library user. In my proposed way this restriction is gone.

C++ Dll Boundaries Using an Abstract Interface - smart pointer in header? calling delete?

1.) No.

extern "C" MyAPI Interface* APIENTRY MakeInterface()
{
return new InterfaceImpl{};
}

This new is used from the dll-runtime and can be incompatible with new from the executable. Destroying the object by delete (executable) may not work due to different memory managers. Rule of thumb: If you provide a create/make-function (factory) then you should provide a destroy/free/delete function. Your Release-method will work but is on a different abstraction level than MakeInterface. Rule of 2 thumbs: Export independent standard layout types in combination with pimpl.

2.) Yes.

Is it safe to use STL (TR1) shared_ptr's between modules (exes and dlls)

Freeing the memory is safe, so long as it all came from the same memory management context. You've identified the most common issue (different C++ runtimes); having separate heaps is another less-common issue you can run into.

Another issue which you didn't mention, but which can be exascerbated by shared pointers, is when an object's code exists in the DLL and is created by the DLL, but another object outside the DLL ends up with a reference to it (via shared pointer). If that object is destroyed after the DLL is unloaded (for example, if it's a module-level static, or if the DLL is explicitly unloaded by FreeLibrary(), the shared object's destructor will crash.

This can bite you if you attempt to write DLL-based, loosely-coupled plugins. It's also the reason that COM lets DLLs decide when they can be unloaded, rather than letting COM servers demand-unload them.

Using std::weak_ptr after DLL unload

I have solved variations of this.

In general, you should not keep pointers to data allocated in a dll around past the lifetime of that dll.

But you can solve this specific problem. Basically, replace calls to make sharedand shared ptr from raw pointer creation in your program. The easiest method may be to ban shared ptr entirely, and write a wrapping subclass that does the redirect.

Then, make a "mysharedptr" dll. It offers a two functions; dll safe make shared and dll safe shared ptr creation (from a ptr).

The from ptr is easy. Header has a template function, which calls a create void shared and passes in ptr to deletion function. The dll that creates that deletion function has to persist long enough; see below.

It then uses the aliasing constructor to return a shared ptr to T with the void pointer control block.

For the make shared replacement, write a dll safe functikn one that make shares a buffer of various fixed sizes. Like powers of two. It also holds a function poonter that destroys the contents. Now in the template, call the fixed buffer make shared, the placement construct in the buffer, then install a pointer to a destruction function (in that order).

Finally, to make the deleter and destruction functions dll safe, use ADL and tag dispatching.

using cleanup=void(*)(void*);
template<class T>struct tag_t{};
cleanup dll_safe_delete_for(tag_t<Bob>);
cleanup dll_safe_destroy_for(tag_t<Bob>);

those two functions exported by the dlls the types come from and in the namespaces of said types. The dll safe shared ptr finds them via tag dispatching.

std::shared_ptr<void> safe_void_shared( void*, void(*)(void*) ); // export from "safe shared ptr util" dll
// API for dll safe shared ptrs:
template<class T>
std::shared_ptr<T> safe_shared( T* t ) {
auto pvoid = safe_void_shared( t, dll_safe_delete_for(tag_t<T>{}) );
return std::shared_ptr<T>( std::move(pvoid), t ); // aliasing ctor
}

then inside the safe shared ptr dll:

std::shared_ptr<void> safe_void_shared( void* ptr, void(*dtor)(void*) ){
return {ptr, dtor};
}

For a type to work, they need to do this:

namespace FooNS{
struct some_type {/*blah*/};
cleanup dll_safe_delete_for(tag_t<some_type>);// exported from dll
}
// in cpp in dll for some_type
cleanup FooNS::dll_safe_delete_for(tag_t<some_type>){
return [](void* pvoid){if(pvoid) delete static_cast<some_type*>(pvoid);};
}

and done. Users just:

auto ptr=safe_shared<FooNS::some_type>( pSomeType );

and the dtor code lives in the some_type dll, while the control block code lives in the dll safe shared dll (different dlls). So your weak ptr can outlive the some_type dll.

Similarly for make shared support, you have a beader file with glue, a void based dll safe function that ensures the control block code is in a safe dll, and some fancy footwork to get a pointer to the destructor out of the class's personal dll.

// exported from dll
// creates an approx bytes sized buffer using make_shared, then emplaces and installs dtor and reutns pointer at object
std::shared_ptr<void> emplace_shared_ptr( std::size_t bytes, std::function<void*(void*)> ctor, void(* dtor)(void*) );

template<class T, class...Args>
std::shared_ptr<T> make_dll_safe_shared( Args&&...args ){
auto pvoid = emplace_shared_ptr(
sizeof(T),
[&](void* here){ return ::new(here) T(std::forward<Args>(args)...); },
dll_safe_destroy_for(tag_t<T>{})
);
return std::shared_ptr<T>(std::move(pvoid), static_cast<T*>(pvoid.get()) );
}

Now the emplace void is a bit tricky.

template<std::size_t sz>
struct buffer{
std::array<char, sz> data;
void(*dtor)(void*)=nullptr;
~buffer(){ if (dtor) dtor(data.data()) }
};
trmplate<std::size_t...mags>
std::shared_ptr<void> emplace_shared_ptr_impl( std::index_sequence<Is...>, std::size_t magnitude, std::function<void*(void*)> ctor, void(* dtor)(void*) ){
using factory=std::shared_ptr<void>(*)( std::function<void*(void*)>, void(*)(void*) );
static factory factories[]={
[](std::function<void*(void*)> ctor, void(*dtor)(void*))->std::shared_ptr<void>{
auto pbuff=std::make_shared<buffer<1<<mags>();
void* pvoid=ctor(pbuff->data.data());
if(!pvoid)return{};
pbuff->dtor=dtor;
return std::shared_ptr<void>( std::move(pbudd), pvoid );
}...
};
return factories[magnitude](ctor, dtor);
}

std::shared_ptr<void> emplace_shared_ptr( std::size_t bytes, std::function<void*(void*)> ctor, void(* dtor)(void*) ){
return emplace_shared_ptr_impl(std::make_index_sequence<40>{}, pow_of_2_at_least_as_big_as(bytes), ctor, dtor);
}

Lots of typoes, efficiency tweaks and error checking. But that is it. 1 terabyte max object size for the make code (1<<40).

The dtor/delete code lives in dll of the type. It is fetched via the dll_safe_destriy_for functions you are responsible for writing for each type you want to support.

The control block code lives in a different dll; namely, a special one that implements those shared ptr void stuff above. It needs to outlive your weak ptrs.

I have used a variation of this (in my case, it was because DLL B was wrapping objects from DLL A in shared ptrs (in template code, so didn't even know shose classesit was); DLL B was unloaded before A, and some shared ptrs that B made outlived it. Boom. The same trick above moved the actual shared ptr creation back into A. This case just has to the trick twice, as we need the dtor to live in A, and the control block code to outlive A.



Related Topics



Leave a reply



Submit