2013-07-26 73 views
15

我有一個很好的庫來管理需要返回特定的字符串列表的文件。由於我將要使用的唯一代碼將是C++(和Java,但通過JNI使用C++),我決定使用標準庫中的向量。庫函數看起來有點像這樣(其中FILE_MANAGER_EXPORT爲平臺定義的出口要求):通過dll邊界傳遞引用STL向量

extern "C" FILE_MANAGER_EXPORT void get_all_files(vector<string> &files) 
{ 
    files.clear(); 
    for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i) 
    { 
     files.push_back(i->full_path); 
    } 
} 

我用向量作爲參考,而不是返回值的原因是試圖保持內存分配清醒,因爲windows真的很不高興,我用extern「C」圍繞C++返回類型(誰知道爲什麼,我的理解是所有extern「C」都會阻止編譯器中的名稱混亂)。無論如何,使用這種與其他C++代碼通常如下:

#if defined _WIN32 
    #include <Windows.h> 
    #define GET_METHOD GetProcAddress 
    #define OPEN_LIBRARY(X) LoadLibrary((LPCSTR)X) 
    #define LIBRARY_POINTER_TYPE HMODULE 
    #define CLOSE_LIBRARY FreeLibrary 
#else 
    #include <dlfcn.h> 
    #define GET_METHOD dlsym 
    #define OPEN_LIBRARY(X) dlopen(X, RTLD_NOW) 
    #define LIBRARY_POINTER_TYPE void* 
    #define CLOSE_LIBRARY dlclose 
#endif 

typedef void (*GetAllFilesType)(vector<string> &files); 

int main(int argc, char **argv) 
{ 
    LIBRARY_POINTER_TYPE manager = LOAD_LIBRARY("library.dll"); //Just an example, actual name is platform-defined too 
    GetAllFilesType get_all_files_pointer = (GetAllFilesType) GET_METHOD(manager, "get_all_files"); 
    vector<string> files; 
    (*get_all_files_pointer)(files); 

    // ... Do something with files ... 

    return 0; 
} 

庫被使用add_library(file_manager SHARED file_manager.cpp)通過cmake的編譯。該程序使用add_executable(file_manager_command_wrapper command_wrapper.cpp)在單獨的cmake項目中編譯。沒有爲這兩個命令指定編譯標誌。

現在該程序在mac和linux中都能很好地工作。問題在於windows。在運行時,我得到這個錯誤:

Debug Assertion Failed!

...

Expression: _pFirstBlock == _pHead

這一點,我已經找到了,善良的理解,是因爲可執行文件和加載的DLL之間單獨的內存堆。我相信這發生在內存分配在一個堆中並在另一個堆中釋放時。問題是,對於我的生活,我無法弄清楚發生了什麼問題。內存在可執行文件中分配並作爲對dll函數的引用傳遞,通過引用添加值,然後處理這些內存並最終釋放回可執行文件中。

我會透露更多的代碼,如果我可以但在我的公司的知識產權狀態我不能,所以上述代碼僅僅是例子。

任何人都對這個問題有了更多的瞭解,能夠幫助我理解這個錯誤,並指出我正確的方向來調試和修復它?很遺憾,我無法使用Windows機器進行調試,因爲我在linux上開發,然後對gerrit服務器提交任何更改,從而通過jenkins觸發生成和測試。編譯和測試後,我可以訪問輸出控制檯。

我確實考慮過使用非stl類型,將C++中的向量複製到char **中,但內存分配是一場噩夢,我努力讓它在Linux上很好地工作,更不用說窗口了,它是多個可怕的堆。

編輯:只要文件矢量超出範圍它肯定崩潰。我目前的想法是,放入向量的字符串被分配在dll堆上並在可執行堆上釋放。如果是這樣,任何人都可以啓發我一個更好的解決方案?

+0

1.路過值是好的,C++ 0x中和了具有移動semantix那將使所有的內存複製「理智」。 2.這是一個猜測,但你可能只是遇到「地獄」。解決它的最好方法是導出STL類,請參見[http://stackoverflow.com/questions/767579/exporting-classes-containing-std-objects-vector-map-etc-from-a-dll] – IdeaHat

+0

@MadScienceDreams我剛開始這樣做,但得到了同樣的錯誤。這促使我轉向通過引用傳遞,試圖在可執行文件中保留所有內存分配/釋放。 – SmallDeadGuy

回答

2

您可能會遇到二進制兼容性問題。在Windows上,如果你想在DLL之間使用C++接口,你必須確保很多東西都是有序的,例如。

  • 參與所有的DLL必須使用相同版本的Visual Studio編譯器
  • 所有DLL文件必須有鏈接C++運行時的相同版本(在VS的大多數版本,這是運行時庫下設置的建配置 - > C++ - >代碼生成在項目屬性)
  • 迭代器調試設置必須是相同的所有構建(這是不能混用的部分原因發佈和調試的DLL)

這不是一個詳盡的列表,不幸的是:

+0

不幸的是,構建是通過cmake自動實現的,儘管編譯器來自visual studio,所以我沒有任何設置。它實際上只使用add_library和add_executable命令而沒有指定標誌。我將通過visual studio研究cmake,並參見 – SmallDeadGuy

+1

有一點研究讓我使用/ MD標誌來確保C++運行時是動態鏈接的。使用'set(CMAKE_SHARED_LINKER_FLAGS「$ {CMAKE_SHARED_LINKER_FLAGS}/MD」)'將此項添加到cmake項目中。雖然 – SmallDeadGuy

+0

到目前爲止還沒有運氣您是否僅將此標誌添加到共享庫或可執行文件中? –

6

The memory is allocated in the executable and passed as a reference to the dll function, values are added via the reference, and then those are processed and finally deallocated back in the executable.

添加值,如果不存在左(容量)的空間意味着重新分配,所以老將會被釋放&新將被分配。這將通過庫的std :: vector :: push_back函數完成,該函數將使用庫的內存分配器。

除此之外,你已經有了明顯的編譯設置必須匹配,當然它們是依賴於編譯器特定的。你很可能必須保持它們在編譯方面的同步。

2

那裏的向量使用默認的std :: allocator,它使用:: operator new來進行分配。

問題是,當在DLL的上下文中使用該矢量時,它將與該DLL的矢量代碼一起編譯,該矢量代碼知道該DLL提供的::運算符new。

EXE中的代碼將嘗試使用EXE :: operator new。

我敢打賭,這在Mac/Linux而不是在Windows上的原因是因爲Windows需要在編譯時解決所有符號。

例如,您可能已經看到Visual Studio提供了一個錯誤,如「未解析的外部符號」。這意味着「你告訴我這個名爲foo()的函數存在,但我無法在任何地方找到它。」

這與Mac/Linux不一樣。它要求在加載時解決所有符號。這意味着你可以使用缺少的::運算符new來編譯.so。你的程序可以加載你的.so文件,併爲.so提供新的::操作符,以便解決它。默認情況下,所有的符號都在GCC中導出,所以:: operator new會被程序導出,並可能被你的.so加載。

這裏有一個有趣的事情,Mac/Linux允許循環依賴。程序可以依賴.so提供的符號,並且可以依賴程序提供的符號。循環依賴是一件可怕的事情,所以我真的很喜歡Windows方法強制你不這樣做。

但是,這就是說,真正的問題是你試圖跨越邊界使用C++對象。這絕對是一個錯誤。只有在DLL和EXE中使用的編譯器相同且具有相同設置的情況下,它纔會工作。 'extern'C''可能會試圖阻止名稱變形(不知道它對非C類型如std :: vector的作用)。但它並沒有改變另一方可能有完全不同的std :: vector實現的事實。

一般來說,如果它通過這樣的邊界傳遞,你希望它是一個普通的舊C類型。如果是整數和簡單類型的東西,事情並不那麼困難。在你的情況下,你可能想要傳遞一個char *數組。這意味着你仍然需要小心內存管理。

DLL/.so應該管理自己的內存。 因此函數可能是這樣的:

Foo *bar = nullptr; 
int barCount = 0; 
getFoos(bar, &barCount); 
// use your foos 
releaseFoos(bar); 

的缺點是,你將有額外的代碼來的東西在邊界處轉換爲C-共享類型。有時這會泄漏到您的實施中,以加速實施。

但是好處是人們現在可以使用任何語言和任何編譯器版本以及任何設置爲您編寫DLL。而且你對正確的內存管理和依賴關係更加小心。

我知道這是額外的工作。但這是跨越邊界做事的正確方法。

+0

正在創建的DLL與CMAKE項目中的可執行文件同時編譯,因此編譯器和所有設置完全相同。 – SmallDeadGuy

12

您的主要問題是跨DLL邊界傳遞C++類型很困難。 您需要以下

  1. 相同的編譯器
  2. 相同的標準庫
  3. 爲++您需要的編譯器
  4. 在Visual C的同一版本++需要同調試異常
  5. 在Visual C相同的設置/發佈配置
  6. 在Visual C++中,您需要相同的迭代器調試級別

等等

如果這就是你想要的,我寫了一個名爲cppcomponents的頭文件庫,提供了最簡單的方法在C++中完成它。 您需要一個支持C++ 11的編譯器。海灣合作委員會4.7.2或4.8將工作。 Visual C++ 2013預覽也適用。

我會引導你使用cppcomponents來解決你的問題。

  1. git clone https://github.com/jbandela/cppcomponents.git在您選擇的目錄。我們將引用您運行此命令的目錄爲localgit

  2. 創建一個名爲interfaces.hpp的文件。在這個文件中,您將定義可在編譯器中使用的接口。

輸入以下

#include <cppcomponents/cppcomponents.hpp> 

using cppcomponents::define_interface; 
using cppcomponents::use; 
using cppcomponents::runtime_class; 
using cppcomponents::use_runtime_class; 
using cppcomponents::implement_runtime_class; 
using cppcomponents::uuid; 
using cppcomponents::object_interfaces; 

struct IGetFiles:define_interface<uuid<0x633abf15,0x131e,0x4da8,0x933f,0xc13fbd0416cd>>{ 

    std::vector<std::string> GetFiles(); 

    CPPCOMPONENTS_CONSTRUCT(IGetFiles,GetFiles); 


}; 

inline std::string FilesId(){return "Files!Files";} 
typedef runtime_class<FilesId,object_interfaces<IGetFiles>> Files_t; 
typedef use_runtime_class<Files_t> Files; 

接下來,創建一個實現。要做到這一點創建Files.cpp

添加以下代碼

#include "interfaces.h" 


struct ImplementFiles:implement_runtime_class<ImplementFiles,Files_t>{ 
    std::vector<std::string> GetFiles(){ 
    std::vector<std::string> ret = {"samplefile1.h", "samplefile2.cpp"}; 
    return ret; 

    } 

    ImplementFiles(){} 


}; 

CPPCOMPONENTS_DEFINE_FACTORY(); 

最後在這裏使用上述的文件。創建UseFiles.cpp

添加以下代碼

#include "interfaces.h" 
#include <iostream> 

int main(){ 

    Files f; 
    auto vec_files = f.GetFiles(); 
    for(auto& name:vec_files){ 
     std::cout << name << "\n"; 
    } 

} 

現在你可以編譯。爲了說明我們在編譯器之間兼容,我們將使用Visual C++編譯器clUseFiles.cpp編譯爲UseFiles.exe。我們將用MinGW的GCC編譯器Files.cppFiles.dll

cl /EHsc UseFiles.cpp /I localgit\cppcomponents

其中localgit是在上述

g++ -std=c++11 -shared -o Files.dll Files.cpp -I localgit\cppcomponents

沒有鏈接步驟描述你跑git clone的目錄。只要確保Files.dllUseFiles.exe位於相同的目錄中即可。

現在運行與UseFiles

cppcomponents可執行文件也將在Linux上運行。主要的變化是當你編譯exe時,你需要將-ldl添加到標誌中,並且當你編譯.so文件時,你需要將-fPIC添加到標誌中。

如果您還有其他問題,請告訴我。

+0

你寫道:*「...等等,如果那是你想要的,我寫了一個名爲cppcomponents的頭文件庫...」*你的意思是「如果你不想確保這一切。 ..「?您的圖書館不是爲了避免需要所有這些和*允許*互操作性嗎? –

3

發生此問題的原因是MS語言中的動態(共享)庫使用與主可執行文件不同的堆。在DLL中創建字符串或更新導致重新分配的向量將導致此問題。

對於這個問題,最簡單的解決方法是將庫更改爲靜態庫(不確定如何讓CMAKE執行該操作),因爲所有分配都將發生在可執行文件和單個堆中。當然,你有MS C++的所有靜態庫兼容性問題,這使得你的庫不夠吸引人。

John Bandela的迴應中的要求都與靜態庫實現的要求類似。

另一種解決方案是在頭文件中實現接口(從而在應用程序空間中編譯),並讓這些方法使用DLL中提供的C接口調用純函數。

+0

輝煌,我認爲這已經解決了我的問題! – Contango

0

我的部分解決方案已經實現了dll框架中的所有默認構造函數,因此根據您的程序顯式添加(推理)複製,賦值運算符甚至移動構造函數。這將導致正確的:: new被調用(假設你指定__declspec(dllexport))。包含析構函數實現以及匹配刪除。 不要在(dll)頭文件中包含任何實現代碼。 我仍然收到有關使用非dll接口的類(以stl容器)作爲dll接口類的基礎的警告,但它有效。這是使用VS2013 RC的本地代碼,顯然,Windows。

+1

如果任何導出的函數導致realloc,這仍然會爆炸,這不是一個解決方案 – paulm

5

大家似乎都掛在這裏臭名昭着的DLL編譯器不兼容問題,但我認爲你是正確的這與堆分配有關。我懷疑發生了什麼是矢量(分配在主要的exe堆空間)包含分配在DLL的堆空間中的字符串。當向量超出範圍並被釋放時,它也試圖釋放字符串 - 並且所有這些都發生在.exe端,這會導致崩潰。

我有兩個本能建議:

  1. 裹在std::unique_ptr每個字符串。它包含一個'deleter',它在unique_ptr超出範圍時處理其內容的重新分配。當在DLL端創建unique_ptr時,其刪除器也是如此。因此,當向量超出範圍並調用其內容的析構函數時,這些字符串將被其DLL綁定的刪除器釋放,並且不會發生堆衝突。

    extern "C" FILE_MANAGER_EXPORT void get_all_files(vector<unique_ptr<string>>& files) 
    { 
        files.clear(); 
        for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i) 
        { 
         files.push_back(unique_ptr<string>(new string(i->full_path))); 
        } 
    } 
    
  2. 保持DLL側的矢量,並只是返回一個對它的引用。您可以通過跨邊界的DLL參考:

    vector<string> files; 
    
    extern "C" FILE_MANAGER_EXPORT vector<string>& get_all_files() 
    { 
        files.clear(); 
        for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i) 
        { 
         files.push_back(i->full_path); 
        } 
        return files; 
    } 
    

半相關:「Downcasting」 unique_ptr<Base> to unique_ptr<Derived> (across DLL boundary)