Подтвердить что ты не робот

Передача ссылки на вектор STL через границу dll

У меня есть хорошая библиотека для управления файлами, которые должны возвращать определенные списки строк. Поскольку единственный код, который я когда-либо буду использовать, будет С++ (и Java, но с использованием С++ через JNI), я решил использовать вектор из стандартных библиотек. Функции библиотеки выглядят примерно так (где 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);
    }
}

Причина, по которой я использовал вектор в качестве ссылки вместо возвращаемого значения, является попыткой сохранить распределение памяти в здравом смысле и потому, что окна были действительно недовольны тем, что у меня есть extern "C" вокруг возвращаемого типа С++ (кто знает, почему, я понимаю, что все extern "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). Программа скомпилирована в отдельный проект cmake, используя add_executable (file_manager_command_wrapper command_wrapper.cpp). Флагов компиляции не указано ни для одного, ни для тех команд.

Теперь программа отлично работает как в mac, так и в linux. Проблема - это окна. При запуске я получаю эту ошибку:

Не удалось выполнить отладочную проверку!

...

Выражение: _pFirstBlock == _pHead

Это, я выяснил и понял, из-за разброса памяти между исполняемыми файлами и загруженными DLL. Я считаю, что это происходит, когда память распределяется в одной куче и освобождается в другой. Проблема в том, что для жизни я не могу понять, что происходит не так. Память выделяется в исполняемом файле и передается как ссылка на функцию dll, значения добавляются через ссылку, а затем обрабатываются и, наконец, освобождаются обратно в исполняемый файл.

Я бы раскрыл больше кода, если бы мог, но интеллектуальная собственность в моей компании утверждает, что я не могу, поэтому весь приведенный выше код - всего лишь примеры.

Кто-нибудь, у кого больше знаний о предмете, который может помочь мне понять эту ошибку, и указать мне в правильном направлении, чтобы отлаживать и исправлять ее? К сожалению, я не могу использовать Windows-машину для отладки, так как я разрабатываю Linux, а затем фиксирую любые изменения на сервере gerrit, который запускает сборки и тесты через jenkins. У меня есть доступ к выходной консоли при компиляции и тестировании.

Я рассмотрел использование не-stl-типов, скопировав вектор в С++ на char **, но выделение памяти было кошмаром, и я изо всех сил пытался заставить его работать красиво на linux, не говоря уже о окнах, и это ужасно много отвалы.

EDIT: он определенно сбой, как только вектор файлов выходит из области видимости. Моя текущая мысль состоит в том, что строки, помещенные в вектор, выделяются на кучу dll и освобождаются от исполняемой кучи. Если это так, может ли кто-нибудь просветить меня относительно лучшего решения?

4b9b3361

Ответ 1

Основная проблема заключается в том, что передача типов С++ через границы DLL затруднена. Вам потребуется следующее

  • Тот же компилятор
  • Такая же стандартная библиотека
  • Те же настройки для исключений
  • В Visual С++ вам нужна такая же версия компилятора
  • В Visual С++ вам нужна такая же конфигурация Debug/Release
  • В Visual С++ вам нужен одинаковый уровень отладки Iterator

И так далее

Если это то, что вы хотите, я написал библиотеку только для заголовка, называемую cppcomponents https://github.com/jbandela/cppcomponents, которая обеспечивает самый простой способ сделать это в С++. Вам нужен компилятор с сильной поддержкой С++ 11. Gcc 4.7.2 или 4.8 будет работать. Предварительный просмотр Visual С++ 2013 также работает.

Я проведу вас через cppcomponents, чтобы решить вашу проблему.

  • git clone https://github.com/jbandela/cppcomponents.git в каталоге по вашему выбору. Мы будем ссылаться на каталог, в котором вы запускали эту команду как localgit

  • Создайте файл с именем 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";
    }

}

Теперь вы можете скомпилировать. Чтобы показать, что мы совместимы между компиляторами, мы будем использовать cl компилятор Visual С++ для компиляции 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.dll и UseFiles.exe находятся в одном каталоге.

Теперь запустите исполняемый файл с помощью UseFiles

cppcomponents также будут работать в Linux. Основное изменение - это когда вы компилируете exe, вам нужно добавить флаг -ldl в флаг, а когда вы скомпилируете .so файл, вам нужно добавить -fPIC к флагам.

Если у вас есть дополнительные вопросы, дайте мне знать.

Ответ 2

Память выделяется в исполняемом файле и передается как ссылка на функцию dll, значения добавляются через ссылку, а затем обрабатываются и, наконец, освобождаются обратно в исполняемый файл.

Добавление значений, если нет свободного места (емкость), означает перераспределение, поэтому старое будет освобождено и будет выделено новое. Это будет сделано с помощью функции std::vector:: push_back библиотеки, которая будет использовать распределитель памяти библиотеки.

Кроме этого, у вас есть очевидные параметры компиляции-must-match-точно и, конечно, они зависят от специфики компилятора. Скорее всего, вам нужно будет синхронизировать их с точки зрения компиляции.

Ответ 3

Кажется, что все люди повесились на печально известной проблеме несовместимости DLL-компилятора, но я думаю, что вы правы в этом отношении к распределению кучи. Я подозреваю, что происходит то, что вектор (выделенный в основном пространстве кучи exe) содержит строки, выделенные в куче библиотеки DLL. Когда вектор выходит за пределы области действия и освобождается, он также пытается освободить строки - и все это происходит на стороне .exe, что приводит к сбою.

У меня есть два инстинктивных предложения:

  • Оберните каждую строку в std::unique_ptr. Он включает в себя "deleter", который обрабатывает освобождение его содержимого, когда unique_ptr выходит за рамки. Когда unique_ptr создается на стороне DLL, также является его дебетером. Поэтому, когда вектор выходит за пределы области действия и вызывается деструкторы его содержимого, строки будут освобождены их 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)));
        }
    }
    
  • Держите вектор на стороне 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;
    }
    

Полусвязь: "Downcasting" unique_ptr<Base> до unique_ptr<Derived> (через границу DLL):

Ответ 4

Вероятно, вы столкнулись с проблемами совместимости с двоичными файлами. В Windows, если вы хотите использовать С++-интерфейсы между DLL, вы должны убедиться, что все в порядке, например.

  • Все задействованные DLL должны быть созданы с той же версией компилятора Visual Studio
  • Все библиотеки DLL должны иметь ссылку на ту же версию среды выполнения С++ (в большинстве версий VS это параметр Runtime Library в разделе Configuration → С++ → Code Generation в свойствах проекта)
  • Параметры отладки Iterator должны быть одинаковыми для всех построений (это часть причины, по которой вы не можете смешивать DLL файлы Release и Debug)

Это не исчерпывающий список по любому поводу, к сожалению: (

Ответ 5

В этом векторе используется стандартный std:: allocator, который использует:: operator new для его выделения.

Проблема заключается в том, что, когда вектор используется в контексте DLL, он скомпилирован с помощью этого векторного кода DLL, который знает о новом:: этом объекте, предоставленном этой DLL.

Код в EXE будет пытаться использовать новый EXE:: operator.

Я уверен, что это работает на Mac/Linux, а не на Windows, потому что Windows требует, чтобы все символы были разрешены во время компиляции.

Например, вы, возможно, видели, что Visual Studio выдает сообщение об ошибке "Неразрешенный внешний символ". Это означает: "Вы сказали мне, что эта функция называется foo() существует, но я не могу ее найти нигде".

Это не то же самое, что и Mac/Linux. Он требует, чтобы все символы были разрешены во время загрузки. Это означает, что вы можете скомпилировать .so с отсутствующим:: operator new. И ваша программа может загрузиться в ваш .so и предоставить ее:: operator new для .so, позволяя ему быть разрешенным. По умолчанию все символы экспортируются в GCC, и поэтому:: оператор new будет экспортироваться программой и потенциально загружен вашим .so.

Здесь интересная вещь, где Mac/Linux допускает круговые зависимости. Программа может полагаться на символ, предоставляемый .so, и тот же .so может полагаться на символ, предоставляемый программой. Круговые зависимости - это ужасная вещь, и мне очень нравится, что метод Windows заставляет вас не делать этого.

Но, тем не менее, настоящая проблема заключается в том, что вы пытаетесь использовать объекты С++ через границы. Это определенно ошибка. Он будет работать ТОЛЬКО, если компилятор, используемый в DLL и EXE, будет таким же, с теми же настройками. "Extern" C "'может попытаться предотвратить изменение имени (не уверен, что он делает для не-C-типов, таких как std::vector). Но это не меняет того факта, что другая сторона может иметь совершенно другую реализацию std::vector.

Вообще говоря, если он передается через такие границы, вы хотите, чтобы он был в обычном старом C-типе. Если это такие вещи, как ints и простые типы, все не так сложно. В вашем случае вы, вероятно, захотите передать массив char *. Это означает, что вам все равно нужно быть осторожным в управлении памятью.

DLL/.so должен управлять собственной памятью. Таким образом, функция может быть такой:

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

Недостаток заключается в том, что у вас будет дополнительный код для преобразования вещей в C-разделяемые типы на границах. И иногда это протекает в вашу реализацию, чтобы ускорить реализацию.

Но в настоящее время люди могут использовать любой язык и любую версию компилятора и любые настройки для записи DLL для вас. И вы более осторожны в правильном управлении памятью и зависимостях.

Я знаю, что это дополнительная работа. Но это правильный способ сделать что-то через границы.

Ответ 6

Проблема возникает из-за того, что динамические (общие) библиотеки на языках MS используют другую кучу, чем основной исполняемый файл. Эта проблема вызывает создание строки в DLL или обновление вектора, вызывающего перераспределение.

Самое простое исправление для этой проблемы - это изменить библиотеку на статическую lib (не определенно, как это делает CMAKE), потому что тогда все распределения будут выполняться в исполняемом файле и в одной куче. Конечно, у вас есть все проблемы статической совместимости с библиотекой MS С++, которые делают вашу библиотеку менее привлекательной.

Требования, лежащие в верхней части ответа Джона Бандела, все аналогичны требованиям для реализации статической библиотеки.

Еще одно решение - реализовать интерфейс в заголовке (тем самым скомпилированный в прикладном пространстве), и эти методы вызывают чистые функции с интерфейсом C, предоставляемым в DLL.

Ответ 7

My-partial - решение заключается в реализации всех конструкторов по умолчанию в фрейме dll, поэтому явным образом добавьте (impelement) копию, оператор присваивания и даже переместите конструкторы в зависимости от вашей программы. Это вызовет вызов правильного:: new (при условии, что вы укажете __declspec (dllexport)). Включите реализации деструктора, а также для сопоставления удалений. Не включайте код реализации в заголовочный файл (dll). Я все еще получаю предупреждения об использовании классов, не связанных с dll-интерфейсом (с stl-контейнерами) в качестве базы для dll-сопряженных классов, но он работает. Это использует VS2013 RC для собственного кода, на, очевидно, в окнах.