虽然它似乎是一个非常常见的问题,但我没有收集到太多信息:如何在DLL边界之间创建有关内存分配的安全接口?
众所周知
// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b
MyObject *o = getObject();
delete o;
肯定会导致崩溃。但是,由于像上面那样的交互 - 我敢说 - 并不罕见,因此必须有一种方法来确保安全的内存分配。
当然,人们可以提供
// in DLL a
DLLEXPORT void deleteObject(MyObject* o) { delete o; }
但也许有更好的方法(例如smart_ptr?)。我也读到了在处理STL容器时使用自定义分配器的问题。
所以我的调查更多的是关于处理这个主题的文章和/或文献的一般指示。是否存在需要注意的特殊谬误(异常处理?)并且这个问题仅限于DLL还是“受到”的UNIX共享对象?
正如你的建议,你可以使用boost::shared_ptr来处理这个问题。在构造函数中,您可以传递自定义清理函数,该函数可以是创建指针的dll的deleteObject-Method。例:
boost::shared_ptr< MyObject > Instance( getObject( ), deleteObject );
如果你的dll不需要C接口,你可以让getObject
返回一个shared_ptr。
超载operator new
,operator delete
等。为您的所有DLL类,并在DLL中实现它们:
void* MyClass::operator new(size_t numb) {
return ::operator new(num_bytes);
}
void MyClass::operator delete(void* p) {
::operator delete(p);
}
...
这可以很容易地放在DLL导出的所有类的公共基类中。
这样,分配和释放完全在DLL堆上完成。老实说,我不确定它是否有任何严重的陷阱或可移植性问题 - 但它对我有用。
你可能会说它“肯定会导致崩溃”。有趣 - “可能”意味着与“肯定”完全相反。
现在,无论如何,声明大部分都是历史性的。有一个非常简单的解决方案:使用1个编译器,1个编译器设置,并链接到CRT的DLL形式。 (你可以逃脱后者)
没有特定的文章可以链接到,因为现在这是一个非问题。无论如何,你需要1个编译器,1个设置规则。简单的事情,如sizeof(std::string)
依赖它,否则你会有大规模的ODR违规。
在某些情况下可能适用的另一个选项是保留DLL内的所有分配和解除分配,并防止对象穿过该边界。您可以通过提供句柄来执行此操作,以便创建MyObject
在DLL代码中创建它并返回一个简单的句柄(例如unsigned int
),通过该句柄执行客户端的所有操作:
// Client code
ObjHandle h=dllPtr->CreateObject();
dllPtr->DoOperation(h);
dllPtr->DestroyObject(h);
由于所有分配都发生在dll中,因此您可以通过包装在shared_ptr中来确保它被清除。这几乎是John Lakos在大规模C ++中提出的方法。
众所周知
// in DLL a DLLEXPORT MyObject* getObject() { return new MyObject(); } // in DLL b MyObject *o = getObject(); delete o;
肯定会导致崩溃。
上述是否具有明确定义的特征取决于MyObject
type的定义方式。
Iff这个类有一个虚拟析构函数(并且析构函数没有内联定义),它不会崩溃并且会表现出明确定义的行为。
通常引用为什么这次崩溃的原因是delete
做了两件事:
operator delete(void* ...)
)对于具有非虚拟析构函数的类,它可以“内联”执行这些操作,这会导致DLL“b”中的delete
可能尝试从“a”堆==崩溃中释放内存。
但是,如果MyObject
的析构函数是virtual
然后在调用“free”函数之前,编译器needs to determine the actual run-time class of the pointer之前它可以将正确的指针传递给operator delete()
:
C ++要求您必须将完全相同的地址传递给operator delete,因为operator new返回。当您使用new分配对象时,编译器会隐式地知道对象的具体类型(例如,编译器用于将正确的内存大小传递给operator new)。
但是,如果您的类具有带有虚拟析构函数的基类,并且您的对象通过指向基类的指针被删除,则编译器不知道调用站点上的具体类型,因此无法计算要传递的正确地址to operator delete()。为什么,你可能会问?因为存在多重继承,基类指针的地址可能与内存中对象的地址不同。
因此,在这种情况下会发生的情况是,当您删除具有虚拟析构函数的对象时,编译器会调用所谓的删除析构函数,而不是调用普通析构函数的通常序列,然后是operator delete()来回收记忆。
由于删除析构函数是一个虚函数,因此在运行时将调用具体类型的实现,并且该实现能够计算内存中对象的正确地址。该实现的作用是调用常规析构函数,计算对象的正确地址,然后在该地址上调用operator delete()。
似乎GCC(来自链接文章)和MSVC通过从“删除析构函数”的上下文中调用dtor以及“free”函数来实现这一点。这个帮助器必然存在于你的DLL中,并且总是使用正确的堆,即使“a”和“b”有不同的堆。
在“分层”架构(非常常见的场景)中,最深的谎言组件负责提供问题的策略(可能如上所述返回shared_ptr<>
或“调用者负责删除此”或“从不删除此项,但是调用releaseFooObject()
完成后不再访问“或......”,并且靠近用户的组件负责遵循该策略。
双向信息流使得责任更难以表征。
这个问题仅限于DLL还是“受到”的UNIX共享对象?
实际上它比这更糟糕:你可以通过静态链接库轻松解决这个问题。在单个执行上下文中存在代码边界,这使得有可能滥用或误传某些工具。
我写过an article关于使用C ++ 11的unique_ptr自定义删除工具来通过DLL边界(或Linux中的共享对象库)传递对象。本文中描述的方法不会使用删除器“污染”unique_ptr签名。