19

我有一个很好的库来管理需要返回特定字符串列表的文件。由于我将使用它的唯一代码将是 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);
    }
}

我使用向量作为参考而不是返回值的原因是试图保持内存分配的合理性,并且因为 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 中都可以正常工作。问题是窗户。运行时,我收到此错误:

调试断言失败!

...

表达式:_pFirstBlock == _pHead

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

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

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

我确实考虑过使用非 stl 类型,将 c++ 中的向量复制到 char**,但是内存分配是一场噩梦,我很难让它在 linux 上很好地工作,更不用说 windows,而且它是可怕的多堆。

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

4

7 回答 7

15

您的主要问题是跨 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";
    }

}

现在你可以编译了。只是为了表明我们在编译器之间是兼容的,我们将使用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.dllUseFiles.exe在同一个目录中。

现在运行可执行文件UseFiles

cppcomponents 也可以在 Linux 上运行。主要的变化是编译exe的时候需要加flags -ldl,编译.so文件的时候需要加-fPICflags。

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

于 2013-07-29T12:46:23.483 回答
6

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

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

除此之外,你有明显的 compile-settings-must-match-exactly ,当然它们是编译器特定的依赖。您很可能必须在编译方面使它们保持同步。

于 2013-07-29T09:45:27.380 回答
6

每个人似乎都对这里臭名昭著的 DLL-compiler-incompatibility 问题感到困惑,但我认为您认为这与堆分配有关是正确的。我怀疑正在发生的事情是向量(在主 exe 的堆空间中分配)包含在 DLL 的堆空间中分配的字符串。当向量超出范围并被释放时,它也会尝试释放字符串 - 所有这些都发生在 .exe 端,这会导致崩溃。

我有两个本能的建议:

  1. 将每个字符串包装在一个std::unique_ptr. 它包括一个“删除器”,它在 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;
    }
    

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

于 2014-01-18T01:40:13.073 回答
3

The problem occurs because dynamic (shared) libraries in MS languages use a different heap than the main executable. Creating a string in the DLL or updating the vector that causes a reallocation will cause this issue.

The simplest fix for THIS issue is to change the library to a static lib (not certain how one makes CMAKE do that) because then all the allocations will occur in the executable and on a single heap. Of course then you have all of the static library compatibility issues of MS C++ which make your library less attractive.

The requirements at the top of John Bandela's response are all similar to those for the static library implementation.

Another solution is to implement the interface in the header (thereby compiled in the application space) and have those methods call pure functions with a C interface provided in the DLL.

于 2013-07-29T16:17:44.500 回答
3

那里的向量使用默认的 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 提供给 .so,从而使其得以解析。默认情况下,所有符号都在 GCC 中导出,因此 ::operator new 将由程序导出并可能由您的 .so 加载。

这里有一个有趣的地方,Mac/Linux 允许循环依赖。该程序可能依赖于 .so 提供的符号,而相同的 .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 可共享类型。有时这会泄漏到您的实施中以加快实施。

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

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

于 2013-07-29T11:05:54.487 回答
2

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

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

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

于 2013-07-26T15:32:28.030 回答
0

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

于 2013-10-06T21:14:03.223 回答