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

Sma*_*Guy 17 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用向量作为参考而不是返回值的原因是试图保持内存分配的合理性,因为我真的不高兴我在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;
}
Run Code Online (Sandbox Code Playgroud)

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

现在该程序在mac和linux中都运行良好.问题是窗户.运行时,我收到此错误:

调试断言失败!

...

表达式:_pFirstBlock == _pHead

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

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

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

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

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

Joh*_*ela 14

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

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

等等

如果这就是你想要的,我写了一个名为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;
Run Code Online (Sandbox Code Playgroud)

接下来创建一个实现.要做到这一点创建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();
Run Code Online (Sandbox Code Playgroud)

最后这里是使用上面的文件.创建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";
    }

}
Run Code Online (Sandbox Code Playgroud)

现在你可以编译了.只是为了显示我们是跨编译器兼容,我们将使用clVisual C++的编译器编译UseFiles.cppUseFiles.exe.我们将使用Mingw Gcc编译Files.cppFiles.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.dll并且UseFiles.exe位于同一目录中.

现在运行可执行文件 UseFiles

cppcomponents也适用于Linux.主要的变化是当你编译exe时,你需要添加-ldl到标志,当你编译.so文件时,你需要添加-fPIC到标志.

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


das*_*ndy 6

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

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

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


d7s*_*rai 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)));
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

半相关:"向下转换" unique_ptr<Base>unique_ptr<Derived>(跨越DLL边界):


Nat*_*one 2

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

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

不幸的是,这并不是一个详尽的列表:(