Возвращение std::string/std::list из dll
Короткий вопрос
Я только что получил dll, с которым я должен общаться. Dll использует crt из msvcr90D.dll (заметка D) и возвращает std::strings, std::lists и boost::shared_ptr. Оператор new/delete нигде не перегружен.
Я предполагаю, что смешивание crt (msvcr90.dll в сборке релиза, или если один из компонентов перестраивается с более новой версией crt и т. Д.) Неизбежно вызовет проблемы, и dll следует переписать, чтобы избежать возврата всего, что могло бы вызвать new/delete (т.е. все, что может вызвать удаление в моем коде в блоке памяти, который был выделен (возможно, с другим crt) в dll).
Я прав или нет?
4 ответа
Главное, что нужно иметь в виду, это то, что библиотеки содержат код, а не память. Выделенная память принадлежит процессу (1). Когда вы создаете экземпляр объекта в своем процессе, вы вызываете код конструктора. В течение времени жизни этого объекта вы будете вызывать другие части кода (методы) для работы с памятью этого объекта. Затем, когда объект исчезает, вызывается код деструктора.
Шаблоны STL явно не экспортируются из DLL. Код статически связан с каждой DLL. Поэтому, когда std::string s создается в a.dll и передается в b.dll, у каждой dll будет два разных экземпляра метода string::copy. Копирование, вызываемое в a.dll, вызывает метод копирования a.dll... Если мы работаем с s в b.dll и вызываем copy, будет вызван метод копирования в b.dll.
Вот почему в ответе Саймона он говорит:
Плохие вещи произойдут, если вы всегда не сможете гарантировать, что весь ваш набор двоичных файлов собран с использованием одной и той же цепочки инструментов.
потому что если по какой-то причине копия строки s отличается между a.dll и b.dll, произойдут странные вещи. Еще хуже, если сама строка отличается между a.dll и b.dll, и деструктор в одном знает, как очистить лишнюю память, которую другой игнорирует... вам может быть трудно отследить утечки памяти. Может быть, даже хуже... a.dll мог быть построен на совершенно другой версии STL (т.е. STLPort), в то время как b.dll построен с использованием реализации Microsoft STL.
Итак, что нужно делать? Там, где мы работаем, мы строго контролируем набор инструментов и настройки сборки для каждой библиотеки DLL. Поэтому, когда мы разрабатываем внутренние библиотеки DLL, мы свободно передаем шаблоны STL. У нас все еще есть проблемы, которые в редких случаях возникают, потому что кто-то не правильно настроил свой проект. Однако мы считаем, что удобство STL стоит случайной проблемы, которая возникает.
Для того, чтобы выставлять dll третьим лицам, это совсем другая история. Если вы не хотите строго требовать определенных настроек сборки от клиентов, вам следует избегать экспорта шаблонов STL. Я не рекомендую строго предписывать вашим клиентам использовать определенные параметры сборки... у них может быть другой сторонний инструмент, который ожидает, что вы будете использовать совершенно противоположные параметры сборки.
(1) Да, я знаю, что static и localals создаются / удаляются при загрузке / выгрузке dll.
У меня есть именно эта проблема в проекте, над которым я работаю - классы STL часто передаются в и из DLL. Проблема не просто в кучах памяти, а в том, что у классов STL нет двоичного стандарта (ABI). Например, в отладочных сборках некоторые реализации STL добавляют дополнительную информацию об отладке к классам STL, так что sizeof(std::vector<T>)
(выпустить сборку)! = sizeof(std::vector<T>)
(отладочная сборка). Ой! Нет надежды, что вы можете положиться на двоичную совместимость этих классов. Кроме того, если ваша DLL была скомпилирована в другом компиляторе с какой-то другой реализацией STL, которая использовала другие алгоритмы, у вас также может быть другой двоичный формат в сборках выпуска.
Я решил эту проблему, используя шаблонный класс pod<T>
(POD означает простые старые данные, такие как символы и целые числа, которые обычно прекрасно переносятся между библиотеками DLL). Работа этого класса состоит в том, чтобы упаковать его параметр шаблона в согласованный двоичный формат, а затем распаковать его на другом конце. Например, вместо функции в DLL, возвращающей std::vector<int>
возвращаешь pod<std::vector<int>>
, Есть специализация шаблона для pod<std::vector<T>>
, который неправильно размещает буфер памяти и копирует элементы. Это также обеспечивает operator std::vector<T>()
, так что возвращаемое значение может быть прозрачно сохранено обратно в std::vector, создав новый вектор, скопировав в него свои сохраненные элементы и вернув его. Поскольку он всегда использует один и тот же двоичный формат, его можно безопасно скомпилировать в отдельные двоичные файлы и сохранить двоичную совместимость. Альтернативное имя для pod
может быть make_binary_compatible
,
Вот определение класса pod:
// All members are protected, because the class *must* be specialization
// for each type
template<typename T>
class pod {
protected:
pod();
pod(const T& value);
pod(const pod& copy); // no copy ctor in any pod
pod& operator=(const pod& assign);
T get() const;
operator T() const;
~pod();
};
Вот частичная специализация для pod<vector<T>>
- обратите внимание, частичная специализация используется, так что этот класс работает для любого типа T. Также обратите внимание, что он на самом деле хранит буфер памяти pod<T>
а не просто T - если вектор содержит другой тип STL, такой как std::string, мы бы хотели, чтобы он также был двоично-совместимым!
// Transmit vector as POD buffer
template<typename T>
class pod<std::vector<T> > {
protected:
pod(const pod<std::vector<T> >& copy); // no copy ctor
// For storing vector as plain old data buffer
typename std::vector<T>::size_type size;
pod<T>* elements;
void release()
{
if (elements) {
// Destruct every element, in case contained other cr::pod<T>s
pod<T>* ptr = elements;
pod<T>* end = elements + size;
for ( ; ptr != end; ++ptr)
ptr->~pod<T>();
// Deallocate memory
pod_free(elements);
elements = NULL;
}
}
void set_from(const std::vector<T>& value)
{
// Allocate buffer with room for pods of T
size = value.size();
if (size > 0) {
elements = reinterpret_cast<pod<T>*>(pod_malloc(sizeof(pod<T>) * size));
if (elements == NULL)
throw std::bad_alloc("out of memory");
}
else
elements = NULL;
// Placement new pods in to the buffer
pod<T>* ptr = elements;
pod<T>* end = elements + size;
std::vector<T>::const_iterator iter = value.begin();
for ( ; ptr != end; )
new (ptr++) pod<T>(*iter++);
}
public:
pod() : size(0), elements(NULL) {}
// Construct from vector<T>
pod(const std::vector<T>& value)
{
set_from(value);
}
pod<std::vector<T> >& operator=(const std::vector<T>& value)
{
release();
set_from(value);
return *this;
}
std::vector<T> get() const
{
std::vector<T> result;
result.reserve(size);
// Copy out the pods, using their operator T() to call get()
std::copy(elements, elements + size, std::back_inserter(result));
return result;
}
operator std::vector<T>() const
{
return get();
}
~pod()
{
release();
}
};
Обратите внимание, что используются функции выделения памяти: pod_malloc и pod_free - они просто malloc и бесплатны, но используют одну и ту же функцию для всех библиотек DLL. В моем случае все библиотеки DLL используют malloc и свободны от EXE-файла узла, поэтому все они используют одну и ту же кучу, что решает проблему с кучей памяти. (Как именно вы это выясните, зависит от вас.)
Также обратите внимание, что вам нужны специализации для pod<T*>
, pod<const T*>
и стручок для всех основных типов (pod<int>
, pod<short>
и т. д.), чтобы они могли храниться в "векторе pod" и других контейнерах pod. Это должно быть достаточно просто, чтобы написать, если вы понимаете приведенный выше пример.
Этот метод означает копирование всего объекта. Вы можете, однако, передавать ссылки на типы стручков, так как есть operator=
который является безопасным между двоичными файлами. Однако никакой реальной передачи по ссылке нет, поскольку единственный способ изменить тип модуля - это скопировать его обратно в исходный тип, изменить, а затем упаковать как пакет. Кроме того, копии, которые он создает, означают, что это не обязательно самый быстрый способ, но он работает.
Однако вы также можете специализировать свои собственные типы, что означает, что вы можете эффективно возвращать сложные типы, такие как std::map<MyClass, std::vector<std::string>>
при условии, что есть специализация для pod<MyClass>
и частичные специализации для std::map<K, V>
, std::vector<T>
а также std::basic_string<T>
(который вам нужно написать только один раз).
Использование конечного результата выглядит следующим образом. Общий интерфейс определяется:
class ICommonInterface {
public:
virtual pod<std::vector<std::string>> GetListOfStrings() const = 0;
};
DLL может реализовать это так:
pod<std::vector<std::string>> MyDllImplementation::GetListOfStrings() const
{
std::vector<std::string> ret;
// ...
// pod can construct itself from its template parameter
// so this works without any mention of pod
return ret;
}
И вызывающий, отдельный двоичный файл, может называть это так:
ICommonInterface* pCommonInterface = ...
// pod has an operator T(), so this works again without any mention of pod
std::vector<std::string> list_of_strings = pCommonInterface->GetListOfStrings();
Поэтому, когда он настроен, вы можете использовать его почти так, как если бы класс pod отсутствовал.
Я не уверен насчет "всего, что может вызвать new/delete" - этим можно управлять путем осторожного использования разделяемых эквивалентов указателей с соответствующими функциями выделения / удаления.
Однако в целом я бы не передавал шаблоны через границы DLL - реализация класса шаблона заканчивается в обеих сторонах интерфейса, что означает, что вы можете использовать разные реализации. Плохие вещи произойдут, если вы всегда не сможете гарантировать, что весь ваш набор двоичных файлов собран с использованием одной и той же цепочки инструментов.
Когда мне нужна такая функциональность, я часто использую класс виртуального интерфейса через границу. Затем вы можете предоставить упаковщики для std::string
, list
и т.д., которые позволяют безопасно использовать их через интерфейс. Затем вы можете контролировать распределение и т. Д., Используя вашу реализацию или используя shared_ptr
,
Сказав все это, я использую в своих интерфейсах DLL одну вещь: shared_ptr
это слишком полезно, чтобы не У меня еще не было проблем, но все построено с использованием одной и той же цепочки инструментов. Я жду этого, чтобы укусить меня, как, без сомнения, это будет. Смотрите этот предыдущий вопрос: Использование shared_ptr в dll-интерфейсах
За std::string
вы можете вернуться с помощью c_str
, В случае более сложных вещей, вариант может быть что-то вроде
class ContainerValueProcessor
{
public:
virtual void operator()(const trivial_type& value)=0;
};
Затем (если вы хотите использовать std::list), вы можете использовать интерфейс
class List
{
public:
virtual void processItems(ContainerValueProcessor&& proc)=0;
};
Обратите внимание, что List теперь может быть реализован любым контейнером.