在 DLL 卸载后使用 std::weak_ptr

Using std::weak_ptr after DLL unload

我在使用 std::weak_ptr 卸载另一个 DLL 中的对象时遇到问题。
这是简化的情况:有一个加载 2 个 DLL 的应用程序。 DLL 提供了一些继承公共接口的对象。应用程序保留了这些对象的一种缓存(std::vectorstd::shared_ptrs),以便 DLL 可以间接使用彼此的对象。
当 DLL 卸载时,它会从缓存中删除它的对象。

但是如果另一个 DLL 将 weak_ptr 保存到另一个 DLL 的对象,问题就来了,因为当调用 weak_ptr 的析构函数时,它会尝试删除控制块,这会导致AV 错误,因为所有者 DLL 已经卸载。
我虽然可以强制在应用程序中分配控制块,而不是 DLL,但我不知道该怎么做。我已经尝试在应用程序端创建一个 Allocator 并将其传递给 shared_ptr 构造函数,但控制块仍然保留一些指向 DLL 的 类 的指针,这再次导致 AV 错误。

似乎唯一可行的方法是在 DLL 中创建指向对象的原始指针,然后将它们传递给应用程序,然后应用程序将它们包装在 shared_ptr 中。但这会导致 enable_shared_from_this.

出现一些问题

那么,我的问题是 - 最好的做法是什么?

我已经解决了这个问题。

一般来说,在 dll 的生命周期结束后,您不应保留指向 dll 中分配的数据的指针。

但是你可以解决这个具体问题。基本上,从程序中的原始指针创建中替换调用以创建 shared 和 shared ptr。最简单的方法可能是完全禁止共享 ptr,并编写一个包装 subclass 来进行重定向。

然后,制作一个“mysharedptr”dll。它提供了两个功能; dll safe make shared 和 dll safe shared ptr creation (from a ptr).

from ptr 很简单。 Header有一个template函数,调用了一个create void shared,传入ptr to deletion函数。创建该删除功能的 dll 必须持续足够长的时间;见下文。

然后它使用别名构造函数 return 与空指针控制块共享指向 T 的指针。

对于 make shared replacement,编写一个 dll 安全函数,使 shares 成为各种固定大小的缓冲区。就像二的幂。它还包含一个破坏内容的函数指针。现在在模板中,调用固定缓冲区 make shared,缓冲区中的放置构造,然后安装指向销毁函数的指针(按此顺序)。

最后,为了使删除和销毁函数 dll 安全,使用 ADL 和标签调度。

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>);

这两个函数由 dll 导出,类型来自并位于所述类型的名称空间中。 dll safe shared ptr 通过标签调度找到它们。

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
}

然后在安全的共享 ptr dll 中:

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

要使一种类型起作用,他们需要这样做:

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);};
}

完成了。用户只是:

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

dtor 代码位于 some_type dll 中,而控制块代码位于 dll 安全共享 dll(不同的 dll)中。所以你的 weak ptr 可以比 some_type dll.

与 make shared 支持类似,你有一个带胶水的 beader 文件,一个基于 void 的 dll 安全函数,确保控制块代码在一个安全的 dll 中,以及一些花哨的步法来获得指向析构函数的指针class 的个人 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()) );
}

现在 emplace void 有点棘手。

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);
}

大量错别字、效率调整和错误检查。但仅此而已。生成代码的最大对象大小为 1 TB (1<<40)。

dtor/delete 代码存在于该类型的 dll 中。它是通过您负责为要支持的每种类型编写的 dll_safe_destriy_for 函数获取的。

控制块代码位于不同的 dll 中;即,一个特殊的实现上面那些共享 ptr void 东西的东西。它需要比你的弱点活得更久。

我使用了这个的变体(在我的例子中,这是因为 DLL B 将来自 DLL A 的对象包装在共享指针中(在模板代码中,所以甚至不知道 shose classesit 是); DLL B 在 A 之前被卸载,B 创建的一些共享指针比它还长。Boom。上面的相同技巧将实际的共享指针创建移回 A。这种情况只需要两次技巧,因为我们需要 dtor住在A,控制块代码比A长寿。

(我不太理解亚当的建议;可能他的解决方案很棒...无论如何,这是我的想法:)

您混淆了不兼容的销毁方案。

当您卸载 DLL 时,指向 DLL 中函数的指针将变得无效。这意味着将这些指针放在 std::shared_ptr 中是没有意义的:即使没有人“使用”指向 DLL 中某个函数的指针,它也可能仍在通过其他函数使用。另外,决定何时卸载它可能比“目前没有人持有指向它的函数指针”更复杂。请注意:您使用什么代码来删除共享指针并不重要——这根本不是正确的抽象。

由于 std::shared_ptr 不在 table 之外,std::weak_ptr 也是如此 - 它仅指向由 std::shared_ptr.

管理的指针

恕我直言,您真正需要的是一个用于处理 DLL 的库,它允许您分配不同类型的弱指针,跟踪 DLL 是否仍在加载。