简短的问题。
我只是得到了一个我应该与之接口的dll。 Dll使用来自msvcr90D.dll的crt(注意D),并返回std :: strings,std :: lists和boost :: shared_ptr。操作员new / delete不会在任何地方超载。
我假设crt mixup(发布版本中的msvcr90.dll,或者如果其中一个组件使用较新的crt重建等)最终会导致问题,并且应该重写dll以避免返回任何可能调用new /的内容删除(即任何可以在我的代码中调用dll中已分配的内存块(可能使用不同的crt)的任何内容)。
我是对还是不对?
答案 0 :(得分:12)
要记住的主要事项是dll包含代码而不包含内存。分配的内存属于进程(1)。在进程中实例化对象时,可以调用构造函数代码。在该对象的生命周期中,您将调用其他代码片段(方法)来处理该对象的内存。然后当对象离开时,调用析构函数代码。
未从dll显式导出STL模板。代码静态链接到每个dll。因此,当在a.dll中创建std :: string并传递给b.dll时,每个dll将有两个不同的string :: copy方法实例。在a.dll中调用的副本调用a.dll的复制方法...如果我们在b.dll中使用s并调用copy,则会调用b.dll中的复制方法。
这就是为什么在西蒙的回答中他说:
除非可以,否则会发生坏事 始终保证您的整套 二进制文件都是用相同的方式构建的 工具链。
因为如果由于某种原因,字符串s的副本在a.dll和b.dll之间有所不同,会发生奇怪的事情。更糟糕的是,如果字符串本身在a.dll和b.dll之间是不同的,并且一个中的析构函数知道要清除另一个忽略的额外内存......你可能很难追踪内存泄漏。可能更糟糕的是... a.dll可能是针对完全不同版本的STL(即STLPort)构建的,而b.dll是使用Microsoft的STL实现构建的。
那你该怎么办?在我们工作的地方,我们严格控制工具链并为每个dll构建设置。因此,当我们开发内部dll时,我们可以自由地转移STL模板。我们仍然遇到问题,在极少数情况下会因为某人没有正确设置他们的项目而突然出现。然而,我们发现STL的便利性值得偶然出现问题。
为了让dll暴露给第三方,这完全是另一个故事。除非您想严格要求客户端的特定构建设置,否则您将希望避免导出STL模板。我不建议严格强制您的客户使用特定的构建设置......他们可能有另一个第三方工具,希望您使用完全相反的构建设置。
(1)是的我知道静态和本地化在dll加载/卸载时被实例化/删除。
答案 1 :(得分:9)
我正在处理的项目中存在这个确切的问题 - STL类很多都是从DLL传输的。问题不仅在于不同的内存堆 - 实际上STL类没有二进制标准(ABI)。例如,在调试版本中,一些STL实现向STL类添加额外的调试信息,例如sizeof(std::vector<T>)
(发布版本)!= sizeof(std::vector<T>)
(调试版本)。哎哟!没有希望你可以依赖这些类的二进制兼容性。此外,如果你的DLL是在一个不同的编译器中编译的,而其他一些STL实现使用了其他算法,你也可能在发布版本中使用不同的二进制格式。
我解决这个问题的方法是使用一个名为pod<T>
的模板类(POD代表普通旧数据,如字符和整数,通常在DLL之间传输良好)。此类的工作是将其模板参数打包为一致的二进制格式,然后在另一端解包。例如,代替返回std::vector<int>
的DLL中的函数,您将返回pod<std::vector<int>>
。 pod<std::vector<T>>
有一个模板专门化,它对内存缓冲区进行malloc并复制元素。它还提供operator std::vector<T>()
,以便通过构造一个新的向量,将其存储的元素复制到其中并返回它,可以透明地将返回值存储回std :: vector。因为它总是使用相同的二进制格式,所以可以安全地编译为单独的二进制文件并保持二进制兼容。 pod
的替代名称可以是make_binary_compatible
。
这是pod类的定义:
// All members are protected, because the class *must* be specialization
// for each type
template<typename T>
class pod {
protected:
pod();
pod(const T& value);
pod(const pod& copy); // no copy ctor in any pod
pod& operator=(const pod& assign);
T get() const;
operator T() const;
~pod();
};
这是pod<vector<T>>
的部分特化 - 注意,使用了部分特化,因此这个类适用于任何类型的T.另请注意,它实际上是存储pod<T>
的内存缓冲区而不仅仅是T - 如果向量包含另一个STL类型,如std :: string,我们也希望它也是二进制兼容的!
// Transmit vector as POD buffer
template<typename T>
class pod<std::vector<T> > {
protected:
pod(const pod<std::vector<T> >& copy); // no copy ctor
// For storing vector as plain old data buffer
typename std::vector<T>::size_type size;
pod<T>* elements;
void release()
{
if (elements) {
// Destruct every element, in case contained other cr::pod<T>s
pod<T>* ptr = elements;
pod<T>* end = elements + size;
for ( ; ptr != end; ++ptr)
ptr->~pod<T>();
// Deallocate memory
pod_free(elements);
elements = NULL;
}
}
void set_from(const std::vector<T>& value)
{
// Allocate buffer with room for pods of T
size = value.size();
if (size > 0) {
elements = reinterpret_cast<pod<T>*>(pod_malloc(sizeof(pod<T>) * size));
if (elements == NULL)
throw std::bad_alloc("out of memory");
}
else
elements = NULL;
// Placement new pods in to the buffer
pod<T>* ptr = elements;
pod<T>* end = elements + size;
std::vector<T>::const_iterator iter = value.begin();
for ( ; ptr != end; )
new (ptr++) pod<T>(*iter++);
}
public:
pod() : size(0), elements(NULL) {}
// Construct from vector<T>
pod(const std::vector<T>& value)
{
set_from(value);
}
pod<std::vector<T> >& operator=(const std::vector<T>& value)
{
release();
set_from(value);
return *this;
}
std::vector<T> get() const
{
std::vector<T> result;
result.reserve(size);
// Copy out the pods, using their operator T() to call get()
std::copy(elements, elements + size, std::back_inserter(result));
return result;
}
operator std::vector<T>() const
{
return get();
}
~pod()
{
release();
}
};
请注意,使用的内存分配函数是pod_malloc和pod_free - 这些函数只是malloc和free,但在所有DLL之间使用相同的函数。在我的例子中,所有DLL都使用malloc并从主机EXE中释放,因此它们都使用相同的堆,这解决了堆内存问题。 (你究竟如何解决这个问题取决于你。)
另请注意,您需要针对所有基本类型(pod<T*>
,pod<const T*>
等)pod<int>
,pod<short>
和pod的专精,以便将其存储在“pod vector”和其他pod容器。如果你理解上面的例子,这些应该足够简单。
此方法意味着复制整个对象。但是,您可以传递对pod类型的引用,因为operator=
在二进制文件之间是安全的。但是,没有真正的传递参考,因为更改pod类型的唯一方法是将其复制回原始类型,更改它,然后重新打包为pod。此外,它创建的副本意味着它不一定是最快的方式,但它工作。
但是,您也可以对自己的类型进行pod特化,这意味着您可以有效地返回复杂类型,例如std::map<MyClass, std::vector<std::string>>
,前提是pod<MyClass>
的特化和std::map<K, V>
的部分特化,{ {1}}和std::vector<T>
(您只需要编写一次)。
最终结果用法如下所示。定义了一个通用接口:
std::basic_string<T>
DLL可能会这样实现它:
class ICommonInterface {
public:
virtual pod<std::vector<std::string>> GetListOfStrings() const = 0;
};
调用者是一个单独的二进制文件,可以这样称呼它:
pod<std::vector<std::string>> MyDllImplementation::GetListOfStrings() const
{
std::vector<std::string> ret;
// ...
// pod can construct itself from its template parameter
// so this works without any mention of pod
return ret;
}
因此,一旦设置完毕,您就可以像使用pod类一样使用它。
答案 2 :(得分:2)
我不确定“任何可以调用new / delete的东西” - 这可以通过仔细使用具有适当分配器/删除器功能的共享指针等效来管理。
但是一般情况下,我不会跨越DLL边界传递模板 - 模板类的实现最终会出现在接口的两侧,这意味着您可以使用不同的实现。除非您始终可以保证您的整个二进制文件都使用相同的工具链构建,否则将会发生不好的事情。
当我需要这种功能时,我经常使用跨边界的虚拟接口类。然后,您可以为std::string
,list
等提供包装,以便您通过界面安全地使用它们。然后,您可以使用您的实施或使用shared_ptr
来控制分配等。
说完这一切之后,我在DLL接口中使用的一件事是shared_ptr
,因为它太有用了。我还没有遇到任何问题,但一切都是用相同的工具链构建的。我正在等待这个咬我,因为毫无疑问它会。请参阅上一个问题:Using shared_ptr in dll-interfaces
答案 3 :(得分:0)
对于std::string
,您可以使用c_str
返回。在更复杂的东西的情况下,选项可以是类似
class ContainerValueProcessor
{
public:
virtual void operator()(const trivial_type& value)=0;
};
然后(假设您要使用std :: list),您可以使用接口
class List
{
public:
virtual void processItems(ContainerValueProcessor&& proc)=0;
};
请注意,List现在可以由任何容器实现。