[英]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.