[英]returning std::string/std::list from dll
简短的问题。
我只是得到了一个我应该与之接口的DLL。 Dll使用来自msvcr90D.dll的crt(注意D),并返回std :: strings,std :: lists和boost :: shared_ptr。 操作员new / delete不会在任何地方超载。
我假设crt mixup(发布版本中的msvcr90.dll,或者如果其中一个组件使用较新的crt重建等)最终会导致问题,并且应该重写dll以避免返回任何可能调用new / delete的内容(即任何可以在我的代码中调用删除在dll中分配的内存块(可能使用不同的crt)的任何内容)。
我是对还是不对?
要记住的主要事情是dll包含代码而不是内存 。 分配的内存属于进程(1)。 在进程中实例化对象时,可以调用构造函数代码。 在该对象的生命周期中,您将调用其他代码片段(方法)来处理该对象的内存。 然后,当对象消失时,将调用析构函数代码。
STL模板未显式从dll导出。 代码静态链接到每个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加载/卸载时被实例化/删除。
我正在研究的项目中存在这个确切的问题 - 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的所有基本类型( pod<int>
, pod<short>
等)的专业化,以便它们可以存储在“pod矢量”中“和其他豆荚容器。 如果你理解上面的例子,这些应该足够直接写。
此方法确实意味着复制整个对象。 但是,您可以传递对pod类型的引用,因为有一个operator=
在二进制文件之间是安全的。 但是,没有真正的传递参考,因为更改pod类型的唯一方法是将其复制回原始类型,更改它,然后重新打包为pod。 此外,它创建的副本意味着它不一定是最快的方式,但它的工作原理 。
但是,你也可以pod-specialize你自己的类型,这意味着你可以有效地返回复杂的类型,如std::map<MyClass, std::vector<std::string>>
, pod<MyClass>
和partial的特化std::map<K, V>
, std::vector<T>
和std::basic_string<T>
(你只需要写一次)。
最终结果用法如下所示。 定义了一个通用接口:
class ICommonInterface {
public:
virtual pod<std::vector<std::string>> GetListOfStrings() const = 0;
};
DLL可能会这样实现它:
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;
}
调用者是一个单独的二进制文件,可以这样调用它:
ICommonInterface* pCommonInterface = ...
// pod has an operator T(), so this works again without any mention of pod
std::vector<std::string> list_of_strings = pCommonInterface->GetListOfStrings();
因此,一旦设置完毕,您就可以使用它,就好像pod类不存在一样。
我不确定“任何可以调用new / delete的东西” - 这可以通过仔细使用具有适当分配器/删除器功能的共享指针等效物来管理。
但是一般情况下,我不会跨越DLL边界传递模板 - 模板类的实现最终会在接口的两端进行,这意味着您可以使用不同的实现。 除非您始终可以保证您的整个二进制文件都使用相同的工具链构建,否则将会发生不好的事情。
当我需要这种功能时,我经常使用跨边界的虚拟接口类。 然后,您可以为std::string
, list
等提供包装器,以便您通过接口安全地使用它们。 然后,您可以使用您的实现或使用shared_ptr
来控制分配等。
说完这一切之后,我在DLL接口中使用的一件事就是shared_ptr
,因为它太有用了。 我还没有遇到任何问题,但一切都是用相同的工具链构建的。 我正在等待这个咬我,因为毫无疑问它会。 请参阅前一个问题: 在dll-interfaces中使用shared_ptr
对于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现在可以由任何容器实现。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.