2010-08-25 84 views
12

简短的问题。从dll返回std :: string/std :: list

我刚刚得到了一个DLL,我应该接口。 Dll使用msvcr90D.dll(注意D)中的crt,并返回std :: strings,std :: lists和boost :: shared_ptr。操作员新建/删除不会在任何地方超载。

我假设crt混用(msvcr90.dll在发布版本中,或者如果其中一个组件是用新crt等重新构建的)最终会导致问题,并且dll应该被重写以避免返回任何可能会调用new的东西/删除(即任何可以调用在我的代码中的一块内存分配(可能与不同的crt)在DLL中的删除)。

我对吗?

回答

12

要记住的主要是dll包含代码而不是内存。分配的内存属于进程(1)。当你在你的进程中实例化一个对象时,你调用构造器代码。在该对象的生命周期中,您将调用其他代码段(方法)来处理该对象的内存。然后,当对象消失时,调用析构函数代码。

STL模板没有从dll明确导出。该代码静态链接到每个DLL。所以当在a.dll中创建std :: string s并传递给b.dll时,每个dll都会有两个不同的string :: copy方法实例。在a.dll中调用的副本调用a.dll的复制方法...如果我们在b.dll中使用s并调用复制,则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加载/卸载实例化/删除。

2

我不确定“任何可以调用新/删除的东西” - 这可以通过小心使用具有适当的分配器/删除器函数的共享指针等价物来管理。

但是,一般情况下,我不会跨越D​​LL边界传递模板 - 模板类的实现会在接口的两侧结束,这意味着您可以使用不同的实现。不好的事情会发生,除非你总能保证你的整套二进制文件都是用相同的工具链构建的。

当我需要这种功能时,我经常在边界上使用虚拟接口类。然后,您可以为std::stringlist等提供包装,以便您可以通过界面安全地使用它们。然后,您可以使用您的实施或使用shared_ptr来控制分配等。说完这一切,我在我的DLL接口中使用的一件事是shared_ptr,因为它不是太有用。我还没有遇到任何问题,但一切都是用相同的工具链构建的。我正在等着这个咬我,毫无疑问它会。看到这个前一个问题:Using shared_ptr in dll-interfaces

+0

“任何可以调用新的/删除的东西”我的意思是说“任何可以调用在我的代码中分配给dll的内存块中的删除”的东西。 – SigTerm 2010-08-25 13:47:22

8

我在我正在处理的项目中有这个确切的问题 - STL类传递给DLL和很多。问题不仅仅是不同的内存堆 - 实际上,STL类没有二进制标准(ABI)。例如,在调试版本中,一些STL实现会向STL类添加额外的调试信息,例如sizeof(std::vector<T>)(发布版本)!= sizeof(std::vector<T>)(调试版本)。哎哟!没有希望你可以依赖这些类的二进制兼容性。此外,如果您的DLL是在其他一些使用其他算法的其他STL实现的编译器中编译的,那么您也可以在发布版本中使用不同的二进制格式。

我解决这个问题的方法是使用一个名为pod<T>(POD代表简单的旧数据,如chars和ints,它们通常在DLL之间转换)的模板类。此类的工作是将其模板参数打包为一致的二进制格式,然后在另一端将其解包。例如,代替返回std::vector<int>的DLL中的函数,您将返回pod<std::vector<int>>pod<std::vector<T>>有一个模板专门化,它将malloc一个内存缓冲区并复制这些元素。它还提供了operator std::vector<T>(),以便返回值可以透明地存回std :: vector中,方法是构造一个新的向量,将其存储的元素复制到它中并返回。因为它总是使用相同的二进制格式,所以它可以安全地编译为单独的二进制文件并保持二进制兼容。 pod的替代名称可能为make_binary_compatible

这里的豆荚类定义:

// 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 - 如果vector包含另一个类似std :: string的STL类型,我们希望它也是二进制兼容的!

// 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类型的引用,因为在二进制文件之间有一个安全的operator=。然而,没有真正的传递引用,因为更改pod类型的唯一方法是将其复制回原始类型,然后将其更改,然后重新打包为pod。此外,它创建的副本意味着它不一定是最快的方式,但它作品

但是,你也可以荚专注自己的类型,这意味着你可以有效地返回复杂的类型,比如std::map<MyClass, std::vector<std::string>>提供有用于pod<MyClass>专业化和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课程一样使用它。

+0

令人惊叹的答案,这将解决我遇到的问题,感谢您写这篇文章! – Contango 2013-09-19 18:25:55

+0

对于那里的所有搜索引擎,如果您通过.dll边界传回std :: string,如果您在MSVC中使用“运行时库”进行编译,则会出现“Debug Assertion Failed!Expression:_pFirstBlock == pHead”多线程调试(/ MTd)“(或Release)集。 – Contango 2013-09-19 18:28:36

0

对于std::string您可以使用c_str返回。在更复杂的东西的情况下,一个选项可以是这样的

class ContainerValueProcessor 
    { 
    public: 
     virtual void operator()(const trivial_type& value)=0; 
    }; 

然后(假设你想使用std ::列表),你可以用一个接口

class List 
    { 
    public: 
     virtual void processItems(ContainerValueProcessor&& proc)=0; 
    }; 

注意,名单可现在由任何容器实施。