从dll返回std :: string / std :: list

时间:2010-08-25 10:38:59

标签: c++ winapi dll

简短的问题。

我只是得到了一个我应该与之接口的dll。 Dll使用来自msvcr90D.dll的crt(注意D),并返回std :: strings,std :: lists和boost :: shared_ptr。操作员new / delete不会在任何地方超载。

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

我是对还是不对?

4 个答案:

答案 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的东西” - 这可以通过仔细使用具有适当分配器/删除器功能的共享指针等效来管理。

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

当我需要这种功能时,我经常使用跨边界的虚拟接口类。然后,您可以为std::stringlist等提供包装,以便您通过界面安全地使用它们。然后,您可以使用您的实施或使用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现在可以由任何容器实现。