在dll边界上传递对STL向量的引用

时间:2013-07-26 15:14:36

标签: c++ windows dll cmake

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

我使用向量作为参考而不是返回值的原因是试图保持内存分配的合理性,因为我真的不高兴我在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上都运行得很好。问题是窗户。运行时,我收到此错误:

  

Debug Assertion失败!

     

...

     

表达式:_pFirstBlock == _pHead

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

如果可以,我会透露更多代码,但我公司的知识产权表明我不能,所以上述所有代码仅仅是示例。

对这个主题有更多了解的人能够帮助我理解这个错误,并指出我正确的方向来调试和修复它吗?遗憾的是,我在linux上开发时无法使用Windows机器进行调试,然后对gerrit服务器进行任何更改,通过jenkins触发构建和测试。我可以在编译和测试时访问输出控制台。

我确实考虑过使用非stl类型,将c ++中的向量复制到char **中,但是内存分配是一场噩梦,我很难让它在linux上运行得很好,更不用说Windows了,这是多么可怕的堆。

编辑:一旦文件向量超出范围,它肯定会崩溃。我目前的想法是放入向量中的字符串在dll堆上分配并在可执行堆上解除分配。如果是这种情况,任何人都可以告诉我更好的解决方案吗?

7 个答案:

答案 0 :(得分:14)

您的主要问题是难以在DLL边界上传递C ++类型。 您需要以下

  1. 相同的编译器
  2. 相同的标准库
  3. 相同的例外设置
  4. 在Visual C ++中,您需要相同版本的编译器
  5. 在Visual C ++中,您需要相同的调试/发布配置
  6. 在Visual C ++中,您需要相同的Iterator调试级别
  7. 等等

    如果这就是你想要的,我写了一个名为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. 的形式引用您运行此命令的目录
    3. 创建名为interfaces.hpp的文件。在此文件中,您将定义可在编译器之间使用的接口。

    4. 输入以下内容

      #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";
          }
      
      }
      

      现在你可以编译了。为了表明我们在编译器之间兼容,我们将使用cl 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

      如果您还有其他问题,请与我们联系。

答案 1 :(得分:6)

  

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

如果没有剩余空间(容量),则添加值意味着重新分配,因此旧的将被取消分配&amp;将分配新的。这将由库的std :: vector :: push_back函数完成,该函数将使用库的内存分配器。

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

答案 2 :(得分: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;
    }
    

  3. 半相关:“Downcasting” unique_ptr<Base> to unique_ptr<Derived> (across DLL boundary)

答案 3 :(得分:3)

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

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

John Bandela的响应顶部的要求都与静态库实现的要求类似。

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

答案 4 :(得分:2)

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

  • 所有涉及的DLL必须使用相同版本的visual studio编译器构建
  • 所有DLL必须链接相同版本的C ++运行时(在VS的大多数版本中,这是配置下的运行时库设置 - &gt; C ++ - &gt;项目属性中的代码生成)
  • 所有版本的迭代器调试设置必须相同(这是你不能混合Release和Debug DLL的部分原因)

不幸的是,这并不是一个详尽的清单:(

答案 5 :(得分:2)

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

问题是,当在DLL的上下文中使用向量时,它使用该DLL的向量代码进行编译,该代码知道该DLL提供的:: operator new。

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

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

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

这与Mac / Linux的功能不同。它要求在加载时解析所有符号。这意味着您可以使用missing :: operator new编译.so。并且您的程序可以加载到.so中并将其:: operator new提供给.so,以便解析它。默认情况下,所有符号都在GCC中导出,因此:: operator new将由程序导出,并可能由.so加载。

这里有一个有趣的事情,Mac / Linux允许循环依赖。该程序可以依赖于.so提供的符号,同样.so可能依赖于程序提供的符号。循环依赖是一件很糟糕的事情,所以我真的很喜欢Windows方法强迫你不要这样做。

但是,那就是说,真正的问题是你试图跨边界使用C ++对象。这绝对是个错误。只有在DLL和EXE中使用的编译器相同且具有相同设置时,它才会起作用。 'extern“C”'可能会尝试阻止名称修改(不确定它对非C类型如std :: vector的作用)。但它并没有改变另一方可能有一个完全不同的std :: vector实现的事实。

一般来说,如果它跨越那样的边界,你希望它是一个普通的旧C类型。如果它是像int和简单类型之类的东西,事情并不那么困难。在您的情况下,您可能想要传递一个char *数组。这意味着您仍需要小心内存管理。

DLL / .so应该管理自己的内存。 所以函数可能是这样的:

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

缺点是你将有额外的代码在边界处将事物转换为C-sharable类型。有时这会泄漏到您的实现中,以加快实施。

但现在人们可以使用任何语言和任何编译器版本以及任何设置来为您编写DLL。并且您对正确的内存管理和依赖关系更加谨慎。

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

答案 6 :(得分:0)

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