关于c ++:在dll边界上传递对STL向量的引用

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++类型是困难的。你需要以下内容

  • 同编译程序
  • 同一标准库
  • 例外情况的相同设置
  • 在VisualC++中,需要相同版本的编译器
  • 在VisualC++中,需要相同的调试/发布配置
  • 在VisualC++中,需要相同的迭代器调试级别。
  • 等等

    如果这是你想要的,我只写了一个头文件库,称为CppMe饰件HTTPS://GITHUBCOM/JBANELLA/CPPPORKS,它提供了在C++中最简单的方法。你需要一个对C++ 11有强大支持的编译器。一般合同条款第4.7.2款或第4.8款适用。Visual C++ 2013预览也可以工作。

    我将引导您使用CPP组件来解决您的问题。

  • 你选择的目录中的git clone https://github.com/jbandela/cppcomponents.git。我们将把运行此命令的目录称为localgit

  • 创建一个名为interfaces.hpp的文件。在这个文件中,您将定义可以跨编译器使用的接口。

  • 输入以下内容

    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;

    接下来创建一个实现。为此,创建Files.cpp

    添加以下代码

    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把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运行可执行文件

    CPP组件也可以在Linux上工作。主要的更改是在编译exe时,需要将-ldl添加到标志中,而在编译.so文件时,需要将-fPIC添加到标志中。

    如果你还有问题,请告诉我。


    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端,这会导致崩溃。

    我有两个本能的建议:

  • std::unique_ptr包住每个字符串。它包含一个"deleter",当唯一指针超出范围时,它处理其内容的释放。当在dll端创建唯一的指针时,其删除程序也是如此。因此,当向量超出范围并调用其内容的析构函数时,字符串将由其绑定到dll的删除程序释放,不会发生堆冲突。

    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;
    }
  • 半相关:"向下投射"unique_ptrunique_ptr(穿过dll边界):


    出现此问题的原因是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。