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.