2010-08-25 81 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; 
    }; 

注意,名單可現在由任何容器實施。