Passing reference to STL vector over dll boundary
我有一个很好的库,用于管理需要返回特定字符串列表的文件。因为我将要使用的唯一代码是C++(和Java,但通过JNI使用C++),所以我决定使用标准库中的向量。库函数看起来有点像这样(其中文件管理器导出是平台定义的导出要求):
1 2 3 4 5 6 7 8 | 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真的不喜欢我在C++返回类型周围有"C"(谁知道为什么,我的理解是,所有的外部C"都是防止编译器中的名字篡改)。不管怎样,使用其他C++的代码一般如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #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; } |
库是通过cmake使用add_library(file_manager shared file_manager.cpp)编译的。该程序使用add_可执行文件(file_manager_command_wrapper command_wrapper.cpp)在单独的cmake项目中编译。没有为这两个命令指定编译标志,只有那些命令。
现在,这个程序在Mac和Linux上都运行得很好。问题是Windows。当运行时,我得到这个错误:
Debug Assertion Failed!
...
Expression: _pFirstBlock == _pHead
我发现这是因为可执行文件和加载的DLL之间的内存堆是分开的。我相信当内存在一个堆中分配,而在另一个堆中释放时,就会发生这种情况。问题是,在我的生活中,我不知道出了什么问题。内存在可执行文件中分配,并作为对dll函数的引用传递,通过引用添加值,然后对这些值进行处理,最后释放回可执行文件中。
如果我可以的话,我会透露更多的代码,但是我公司的知识产权声明我不能,所以上面所有的代码都只是例子。
任何对这个主题有更多了解的人都能帮助我理解这个错误,并指出正确的方向来调试和修复它?不幸的是,我无法使用Windows机器进行调试,因为我是在Linux上开发的,然后将任何更改提交给一个通过Jenkins触发构建和测试的Gerrit服务器。我可以在编译和测试时访问输出控制台。
我确实考虑过使用非STL类型,将C++中的向量复制到char **中,但是内存分配是一个噩梦,我努力让它在Linux上运行得很好,更不用说Windows和它的可怕的多堆了。
编辑:一旦文件向量超出范围,它肯定会崩溃。我目前的想法是,放入向量的字符串在dll堆上分配,在可执行堆上释放。如果是这样的话,有人能告诉我一个更好的解决方案吗?
您的主要问题是,跨越DLL边界传递C++类型是困难的。你需要以下内容
等等
如果这是你想要的,我只写了一个头文件库,称为CppMe饰件HTTPS://GITHUBCOM/JBANELLA/CPPPORKS,它提供了在C++中最简单的方法。你需要一个对C++ 11有强大支持的编译器。一般合同条款第4.7.2款或第4.8款适用。Visual C++ 2013预览也可以工作。
我将引导您使用CPP组件来解决您的问题。
你选择的目录中的
创建一个名为
输入以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #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; |
接下来创建一个实现。为此,创建
添加以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #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(); |
最后,这里是使用上述文件的文件。创建EDOCX1[4]
添加以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include"interfaces.h" #include <iostream> int main(){ Files f; auto vec_files = f.GetFiles(); for(auto& name:vec_files){ std::cout << name <<" "; } } |
现在可以编译了。为了显示我们在编译器之间是兼容的,我们将使用EDCOX1×5×Visual C++编译器将EDCOX1×4码编译成EDCOX1×7。我们将使用mingw-gcc把
其中
没有链接步骤。只需确保
现在用
CPP组件也可以在Linux上工作。主要的更改是在编译exe时,需要将
如果你还有问题,请告诉我。
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函数完成,该函数将使用库的内存分配器。
除此之外,明显的编译设置必须完全匹配,当然它们依赖于特定的编译器。你很可能要保持它们在编译方面的同步。
在这里,似乎每个人都被臭名昭著的DLL编译器不兼容问题所困扰,但我认为您认为这与堆分配有关是正确的。我怀疑发生的事情是向量(分配在主exe的堆空间中)包含分配在dll堆空间中的字符串。当向量超出作用域并被释放时,它还试图释放字符串-而所有这些都发生在.exe端,这会导致崩溃。
我有两个本能的建议:
用
1 2 3 4 5 6 7 8 | 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))); } } |
将向量保持在dll一侧,并返回对它的引用。可以通过dll边界传递引用:
1 2 3 4 5 6 7 8 9 10 11 | 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; } |
半相关:"向下投射"
出现此问题的原因是MS语言中的动态(共享)库使用的堆与主可执行文件不同。在dll中创建字符串或更新导致重新分配的向量将导致此问题。
这个问题最简单的解决方法是将库更改为静态库(不确定如何使cmake这样做),因为所有的分配都将发生在可执行文件和单个堆上。当然,你拥有MSC++的所有静态库兼容性问题,这使得你的库不那么吸引人。
JohnBandela响应顶部的需求都与静态库实现的需求相似。
另一种解决方案是在头中实现接口(从而在应用程序空间中编译),并让这些方法使用DLL中提供的C接口调用纯函数。
那里的向量使用默认的std::allocator,它的分配使用::operator new。
问题是,当向量在dll的上下文中使用时,它是用该dll的向量代码编译的,该代码了解该dll提供的::operator new。
exe中的代码将尝试使用exe的::operator new。
我敢打赌,这在Mac/Linux上工作而不是在Windows上工作的原因是Windows需要在编译时解析所有符号。
例如,您可能看到Visual Studio给出了一个错误,说"未解析的外部符号"。这意味着"您告诉我存在名为foo()的函数,但我在任何地方都找不到它。"
这与Mac/Linux不同。它要求在加载时解析所有符号。这意味着你可以编译一个。所以用一个缺少的::operator new。并且您的程序可以加载到.so中,并向.so提供它的::operator new,以便对其进行解析。默认情况下,所有符号都在gcc中导出,所以::operator new将由程序导出,并可能由.so加载。
这里有一件有趣的事情,Mac/Linux允许循环依赖。程序可以依赖.so提供的符号,也可以依赖.so提供的符号。循环依赖是一件很糟糕的事情,所以我非常喜欢Windows方法强制您不要这样做。
但是,真正的问题是,你试图跨越边界使用C++对象。那绝对是个错误。只有当dll和exe中使用的编译器相同且设置相同时,它才能工作。"extern"c"可能试图防止名称损坏(不确定它对非C类型(如std::vector)做了什么)。但这并没有改变另一方可能有一个完全不同的std::vector实现的事实。
一般来说,如果它像那样通过边界,您希望它是一个普通的旧C类型。如果是像ints和简单类型的东西,事情就不那么困难了。在您的例子中,您可能希望传递一个char*数组。这意味着您仍然需要注意内存管理。
dll/.so应该管理自己的内存。所以函数可能是这样的:
1 2 3 4 5 | Foo *bar = nullptr; int barCount = 0; getFoos( bar, &barCount ); // use your foos releaseFoos(bar); |
缺点是您将有额外的代码来在边界处将内容转换为C-sharable类型。有时这会泄漏到您的实现中,以加速实现。
但是现在人们可以使用任何语言、任何编译器版本和任何设置来为您编写一个DLL。而且,对于正确的内存管理和依赖性,您会更加小心。
我知道这是额外的工作。但这是跨越国界做事的正确方式。
您可能遇到了二进制兼容性问题。在Windows上,如果你想在DLL之间使用C++接口,你必须确保有很多事情是有序的。
- 所有涉及的DLL都必须使用同一版本的Visual Studio编译器生成。
- 所有的DLL都必须具有与C++运行库相同的版本(在VS的大多数版本中,这是配置下的运行库设置-在项目属性中代码生成>代码生成)
- 所有生成的迭代器调试设置必须相同(这是不能混合使用发布和调试dll的部分原因)
不幸的是,这不是一份详尽的清单:(
我的部分解决方案是在dll框架中实现所有默认的构造函数,因此根据程序的不同,显式地添加(强制)复制、赋值运算符甚至移动构造函数。这将导致调用正确的::new(假设指定了uu declspec(dllexport))。也包括用于匹配删除的析构函数实现。不要在(dll)头文件中包含任何实现代码。我仍然收到关于使用非dll接口类(带有stl容器)作为dll接口类的基础的警告,但它是有效的。这显然是在Windows上对本机代码使用了vs2013 rc。