通过dll边界传递对STL向量的引用

我有一个很好的库来pipe理需要返回特定的string列表的文件。 由于我将要使用的唯一代码将是C ++(和Java,但通过JNI使用C ++),我决定使用标准库中的向量。 库函数看起来有点像这样(其中FILE_MANAGER_EXPORT是平台定义的导出要求):

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); } } 

我使用vector作为引用而不是返回值的原因是为了保持内存分配的健全,并且因为windows对于c ++返回types有一个extern“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; } 

该库是通过cmake使用add_library(file_manager SHARED file_manager.cpp)进行编译的。 该程序使用add_executable(file_manager_command_wrapper command_wrapper.cpp)在单独的cmake项目中编译。 没有为这两个命令指定编译标志。

现在,这个程序在Mac和Linux上都可以正常工作。 问题是Windows。 运行时,我得到这个错误:

debugging断言失败!

expression式:_pFirstBlock == _pHead

这个,我已经发现和种类的理解,是因为可执行文件和加载的DLL之间的单独的内存堆。 我相信这发生在内存分配在一个堆中,并释放在另一个堆中。 问题是,在我的生活中,我无法弄清楚什么是错的。 内存分配在可执行文件中,并作为对dll函数的引用传递,通过引用添加值,然后处理这些内存并最终释放回到可执行文件中。

如果可以的话,我会透露更多的代码,但是我公司的知识产权说我不能,所以上面的代码都只是个例子。

任何对这个主题有更多了解的人都能帮助我理解这个错误,并指出我正确的方向来debugging和修复它。 很遗憾,我不能使用Windows机器进行debugging,因为我在linux上开发,然后将任何更改提交给gerrit服务器,通过jenkins触发生成和testing。 编译和testing后,我可以访问输出控制台。

我曾经考虑过使用非stltypes,将c ++中的vector拷贝到char **中,但是内存分配是一场噩梦,我正努力让它在linux上更好地工作,更不用说windows了,而且它是可怕的多堆。

编辑:只要文件vector超出范围,它肯定崩溃。 我目前的想法是,在vector的string被分配在DLL堆,并释放在可执行堆。 如果是这样,任何人都可以启发我一个更好的解决scheme?

Solutions Collecting From Web of "通过dll边界传递对STL向量的引用"

你的主要问题是跨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 ++编译器将UseFiles.cpp编译到UseFiles.exe 我们将使用Mingw Gcc将Files.cpp编译成Files.dll

cl /EHsc UseFiles.cpp /I localgit\cppcomponents

其中localgit是您在其中运行git clone的目录,如上所述

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

没有链接步骤。 只要确保Files.dllUseFiles.exe在相同的目录中。

现在用UseFiles运行可执行文件

cppcomponents也将在Linux上工作。 主要的变化是当你编译exe时,你需要在标志中加上-ldl ,当你编译.so文件时,你需要在标志中加上-fPIC

如果您还有其他问题,请告诉我。

内存分配在可执行文件中,并作为对dll函数的引用传递,通过引用添加值,然后处理这些内存并最终释放回到可执行文件中。

如果没有剩余空间(容量)则添加值意味着重新分配,所以旧的将被重新分配,新的将被分配。 这将通过库的std :: vector :: push_back函数完成,该函数将使用库的内存分配器。

除此之外,你已经有了明显的编译设置必须匹配,当然它们是依赖于编译器的。 你很可能必须保持它们在编译方面的同步。

大家似乎都挂在臭名昭着的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>unique_ptr<Derived> (跨DLL边界) :

您可能正在遇到二进制兼容性问题。 在Windows上,如果你想在DLL之间使用C ++接口,你必须确保很多事情是有序的,例如。

  • 所涉及的所有DLL必须使用相同版本的Visual Studio编译器构建
  • 所有DLL必须链接相同版本的C ++运行时(在VS的大多数版本中,这是在项目属性中的配置 – > C ++ – >代码生成下的运行时库设置)
  • 迭代器的调试设置对于所有的构建必须相同(这是你不能混合发布和调试DLL的原因的一部分)

这不是一个详尽的列表不幸的是任何弹性:(

那里的vector使用默认的std :: allocator,它使用:: operator new来进行分配。

问题是,当在DLL的上下文中使用该矢量时,将使用该DLL的矢量代码进行编译,该矢量代码知道由该DLL提供的::运算符new。

EXE中的代码将尝试使用EXE :: operator new。

我敢打赌,这在Mac / Linux上,而不是在Windows上的原因是因为Windows需要在编译时解决所有的符号。

例如,您可能已经看到Visual Studio提供了一个类似“无法解析的外部符号”的错误。 这意味着“你告诉我这个函数名为foo()存在,但我找不到它。”

这与Mac / Linux不一样。 它要求在加载时解决所有符号。 这意味着你可以用缺少的::运算符new来编译.so。 而且你的程序可以加载你的.so文件,并且把它的::操作符new提供给.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-sharable类型。 有时这会泄漏到您的实施中,以加快实施。

但是好处是现在人们可以使用任何语言和任何编译器版本以及任何设置为您编写DLL。 而且你对正确的内存管理和依赖关系更加小心。

我知道这是额外的工作。 但这是跨越边界做事的正确方法。

发生该问题是因为MS语言中的动态(共享)库使用与主可执行文件不同的堆。 在DLL中创建一个字符串或更新导致重新分配的向量将导致此问题。

对于这个问题,最简单的解决方法是将库更改为静态库(不确定CMAKE如何执行),因为所有的分配都将在可执行文件和单个堆中进行。 当然,那么你有所有的MS C ++的静态库兼容性问题,使您的库不那么有吸引力。

John Bandela的回应中的要求都与静态库实现的要求类似。

另一种解决方案是在头文件中实现接口(从而在应用程序空间中编译),并使这些方法使用DLL中提供的C接口调用纯函数。

我的部分解决方案是实现所有默认的构造函数在DLL框架,所以显式添加(推理)复制,赋值运算符,甚至移动构造函数,根据您的程序。 这将导致正确的:: new被调用(假设你指定了__declspec(dllexport))。 包含析构函数实现以及匹配删除。 不要在(dll)头文件中包含任何实现代码。 我仍然收到有关使用非dll接口类(以stl容器)作为dll接口类的基础的警告,但是它起作用。 这是使用VS2013 RC本地代码,显然,Windows。