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

21

我有一个很好的文件管理库,需要返回特定的字符串列表。由于我将与之使用的唯一代码将是C++(以及通过JNI使用C ++的Java),因此我决定使用标准库中的向量。库函数看起来有点像这样(其中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);
    }
}

我使用向量作为引用而不是返回值的原因是为了保持内存分配的合理性,因为Windows对我在C++返回类型周围使用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。运行时,出现以下错误:

Debug Assertion Failed!

...

Expression: _pFirstBlock == _pHead

我已经发现并且有点理解了,这是由于可执行文件和加载的dll之间存在独立的内存堆。我相信当内存在一个堆中分配并在另一个堆中释放时就会出现这个问题。问题是,我实在找不出哪里出错了。内存在可执行文件中被分配并作为引用传递给dll函数,在引用上添加值,然后处理这些值并最终在可执行文件中释放。

如果可以,我会透露更多代码,但是该公司的知识产权规定我不能这样做,所以上面的所有代码只是例子。

对于这个错误有更多了解的人能否帮助我理解并指导我调试和修复它?不幸的是,我无法在Windows机器上进行调试,因为我是在Linux上开发的,然后将任何更改提交到gerrit服务器,该服务器通过jenkins触发构建和测试。我可以访问编译和测试的输出控制台。

我考虑使用非STL类型,将C++中的向量复制到char **中,但是内存分配非常困难,我在Linux上都很难搞定,更不用说Windows和它可怕的多个堆了。

编辑:一旦files矢量超出范围,它肯定会崩溃。我目前的想法是,放入矢量的字符串在dll堆上分配,并在可执行文件堆上释放。如果是这种情况,有谁能告诉我更好的解决方法吗?


  1. 传值是可以的,c++0x及以上版本有移动语义,可以使所有内存复制“合理化”。
  2. 这只是一个猜测,但你可能只是遇到了“dll地狱”。解决它的最好方法是导出STL类,请参见[https://dev59.com/EnRA5IYBdhLWcg3w9izq]。
- IdeaHat
@MadScienceDreams,起初我也是这么做的,但总是出现同样的错误。这促使我尝试通过引用传递来保持内存分配和释放都在可执行文件中进行。 - SmallDeadGuy
7个回答

16

你的主要问题是在DLL边界传递C++类型比较困难。你需要以下:

  1. 相同的编译器
  2. 相同的标准库
  3. 相同的异常设置
  4. 在Visual C++中,需要相同版本的编译器
  5. 在Visual C++中,需要相同的调试/发布配置
  6. 在Visual C++中,需要相同的迭代器调试级别

等等

如果这正是你想要的,我写了一个名为cppcomponents的头文件库https://github.com/jbandela/cppcomponents,它提供了在C++中实现最简单的方法。你需要一个对C++11有强大支持的编译器。Gcc 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++编译器cl来编译UseFiles.cpp成为UseFiles.exe。我们将使用Mingw Gcc来编译Files.cpp成为Files.dllcl /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标志。
如果您有进一步的问题,请告诉我。

你写道:“...等等。如果这是你想要的,我写了一个名为cppcomponents的仅头文件库...” 你的意思是“如果你不想确保所有这些,那么...”吗?你的库难道不是旨在避免需要所有这些并允许互操作性吗? - Adi Shavit

8

大家似乎都在关注臭名昭著的DLL编译器不兼容问题,但我认为你对堆分配有所指正。我怀疑发生的情况是,向量(分配在主应用程序的堆空间中)包含在DLL的堆空间中分配的字符串。当向量超出范围并被释放时,它也试图释放这些字符串,而所有这些都发生在.exe侧,导致崩溃。

我有两个直觉建议:

  1. Wrap each string in a std::unique_ptr. It includes a 'deleter' which handles the deallocation of its contents when the unique_ptr goes out of scope. When the unique_ptr is created on the DLL side, so is its deleter. So when the vector goes out of scope and the destructors of its contents are called, the strings will be deallocated by their DLL-bound deleters and no heap conflict occurs.

    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. Keep the vector on the DLL side and just return a reference to it. You can pass the reference across the DLL boundary:

    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边界):


选项#2是如何工作的?我对所有这些都有非常有限的理解,但我认为DLL边界问题的原因是因为即使给出相同的C++源代码,编译器也可以以不同的方式组织类的数据。我的问题是:在#2中,当我在EXE中使用[]运算符访问返回向量的元素时,它会使用DLL的[]代码还是自己的代码?如果是自己的代码,那不就可能是错误的吗? - Raj K

6
内存分配在可执行文件中,并作为引用传递给dll函数,值通过引用添加,然后这些值被处理并最终在可执行文件中释放。 如果没有剩余空间(容量),则添加值会导致重新分配,因此旧的将被释放,并分配一个新的内存。这将由库的std::vector::push_back函数完成,该函数将使用库的内存分配器。 除此之外,你需要确保编译设置完全匹配,并且它们显然依赖于特定编译器。你很可能需要在编译方面进行同步。

4
那里的向量使用默认的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中导出,因此程序和可能由您的.so加载的:: operator new将被导出。
这里有一个有趣的事情,那就是Mac / Linux允许循环依赖关系。程序可能依赖于一个由.so提供的符号,并且同一.so可能依赖于程序提供的符号。循环依赖关系是一件可怕的事情,因此我真的很喜欢Windows的方法,它迫使您不要这样做。
但是,话虽如此,真正的问题在于您正在尝试跨界使用C ++对象。那绝对是一个错误。它只有在DLL和EXE中使用相同的编译器和相同的设置时才能工作。'extern“C”'可能会尝试防止名称混合(不确定它对像std :: vector这样的非C类型做了什么)。但它并不改变另一边可能拥有完全不同实现的std :: vector的事实。
一般来说,如果将其传递到边界外,您希望它是裸的C类型。如果是像int和简单类型之类的东西,则不那么困难。在您的情况下,您可能要传递char *的数组。这意味着您仍然需要注意内存管理。
DLL / .so应该管理自己的内存。 因此,函数可能会像这样:
Foo *bar = nullptr;
int barCount = 0;
getFoos( bar, &barCount );
// use your foos
releaseFoos(bar);

缺点是您需要额外的代码将事物转换为可在C#边界处使用的类型。有时,这会泄漏到您的实现中,以加快实现速度。
但好处是现在人们可以使用任何语言、任何编译器版本和任何设置来为您编写DLL。并且您更加谨慎地管理内存和依赖项。
我知道这需要额外的工作。但这是跨越边界进行操作的正确方式。

正在创建的 DLL 与可执行文件同时编译在同一 CMAKE 项目中,因此编译器和所有设置完全相同。 - SmallDeadGuy

3
问题出现的原因是MS语言中的动态(共享)库使用不同的堆栈,与主可执行文件不同。在DLL中创建字符串或更新导致重新分配的向量会导致此问题。
最简单的解决方法是将库更改为静态库(不确定如何让CMAKE执行此操作),因为所有分配都将在可执行文件中进行,并在单个堆上进行。当然,这样你就有了所有MS C++的静态库兼容性问题,这使得你的库不那么吸引人。
John Bandela回答顶部的要求与静态库实现的要求非常相似。
另一种解决方案是在头文件中实现接口(在应用程序空间中编译),并使这些方法调用由DLL提供的带有C接口的纯函数。

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

  • 所有涉及的DLL都必须使用相同版本的Visual Studio编译器构建
  • 所有DLL都必须链接相同版本的C++运行时(在大多数VS版本中,这是项目属性下的 Configuration -> C++ -> 代码生成中的 Runtime Library 设置)
  • 所有构建的迭代器调试设置必须相同(这就是您不能混合使用Release和Debug DLL的部分原因)

不幸的是,这并不是详尽无遗的清单 :(


构建过程是通过cmake自动化的,不幸的是,编译器来自Visual Studio,所以我没有任何类似的设置。它只使用add_library和add_executable命令,没有指定任何标志。我会研究一下在Visual Studio中使用cmake。 - SmallDeadGuy
1
一点研究让我使用/MD标志来确保C++运行时是动态链接的。将其添加到cmake项目中,使用set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /MD")。但目前为止还没有成功。 - SmallDeadGuy
你是只在共享库中添加了这个标志,还是也添加到可执行文件中了? - Benjamin Kloster
针对整个 CMake 项目,它可以编译共享库和可执行文件。 - SmallDeadGuy

0

我的-部分-解决方案是在dll框架中实现所有默认构造函数,因此根据您的程序显式添加(实现)复制、赋值运算符甚至移动构造函数。这将导致正确的::new被调用(假设您指定__declspec(dllexport))。还应包括匹配的析构函数实现。

不要在(dll)头文件中包含任何实现代码。

我仍然会收到有关将非dll接口类(具有stl容器)用作dll接口类基础的警告,但它可以工作。这是使用VS2013 RC进行本机代码,在显然的Windows上。


1
如果任何导出函数引起重新分配,这仍然会崩溃,这不是一个解决方案。 - paulm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接