Передача ссылки на вектор 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 и освобождаются от исполняемой кучи. Если это так, может ли кто-нибудь просветить меня относительно лучшего решения?
Ответы
Ответ 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 для собственного кода, на, очевидно, в окнах.