2010-08-25 2 views
12

Короткий вопрос.возвращение std :: string/std :: list from dll

Я просто получил dll, с которым я должен взаимодействовать. Dll использует crt из msvcr90D.dll (уведомление D) и возвращает std :: strings, std :: lists и boost :: shared_ptr. Оператор new/delete не перегружен нигде.

Я предполагаю, что crt mixup (msvcr90.dll в выпуске сборки или если один из компонентов перестроен с новым crt и т. Д.) Неизбежно вызовет проблемы в конечном итоге, а dll следует переписать, чтобы избежать возврата всего, что могло бы вызвать новое/delete (т.е. все, что может вызвать удаление в моем коде в блоке памяти, который был выделен (возможно, с другим crt) в dll).

Я прав или нет?

ответ

12

Главное, чтобы иметь в виду, что DLLs содержит код и не память. Выделенная память принадлежит процессу (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 стоит случайной проблемы, которая возникает.

Для разоблачения dlls третьим сторонам, это совсем другая история. Если вы не хотите строго требовать определенные настройки сборки от клиентов, вам не нужно экспортировать шаблоны STL. Я не рекомендую строго применять ваши клиенты к определенным настройкам сборки ... у них может быть другой сторонний инструмент, который ожидает, что вы будете использовать абсолютно противоположные настройки сборки.

(1) Да, я знаю, что статические и локальные объекты создаются или удаляются при загрузке/выгрузке dll.

2

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

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

Когда мне нужна такая функциональность, я часто использую класс виртуального интерфейса через границу. Затем вы можете предоставить обертки для std::string, list и т. Д., Которые позволяют вам безопасно использовать их через интерфейс. Затем вы можете управлять распределением и т. Д., Используя вашу реализацию, или используя shared_ptr.

Сказав все это, единственное, что я использую в своих DLL-интерфейсах, - это shared_ptr, так как это слишком полезно. У меня еще не было никаких проблем, но все построено с помощью той же инструментальной цепочки. Я жду, когда это укусит меня, так что, без сомнения, это произойдет. См. Предыдущий вопрос: Using shared_ptr in dll-interfaces

+0

«Все, что можно было бы назвать новым/удалить» Я хотел сказать «все, что может вызвать удаление в моем коде на блоке памяти, который был выделен в dll». – SigTerm

8

У меня есть эта точная проблема в проекте, над которым я работаю - классы STL передаются в библиотеки DLL и из них. Проблема заключается не только в разной памяти, но и в том, что классы STL не имеют двоичного стандарта (ABI). Например, в отладочных сборках некоторые реализации STL добавляют дополнительную информацию для отладки в классы STL, такие как sizeof(std::vector<T>) (выпускная сборка)! = sizeof(std::vector<T>) (сборка отладки). Ой! Нет никакой надежды, что вы можете положиться на двоичную совместимость этих классов. Кроме того, если ваша DLL была скомпилирована в другом компиляторе с некоторой другой реализацией STL, которая использовала другие алгоритмы, у вас может быть и другой бинарный формат в сборках релизов.

Способ, которым я решил эту проблему, - использовать класс шаблонов под названием pod<T> (POD означает Plain Old Data, например, chars и ints, которые обычно передают между DLL). Задача этого класса - упаковать его параметр шаблона в согласованный двоичный формат, а затем распаковать его на другом конце. Например, вместо функции в DLL, возвращающей номер std::vector<int>, вы возвращаете номер pod<std::vector<int>>. Существует специальная спецификация шаблона для pod<std::vector<T>>, которая заполняет буфер памяти и копирует элементы. Он также предоставляет operator std::vector<T>(), так что возвращаемое значение может быть прозрачно сохранено обратно в std :: vector, построив новый вектор, скопировав в него сохраненные элементы и вернув его. Поскольку он всегда использует один и тот же двоичный формат, его можно безопасно скомпилировать для разделения двоичных файлов и сохранения бинарных данных. Альтернативным именем для pod может быть make_binary_compatible.

Вот определение класса стручка:

// 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 - это просто таНос и бесплатно, но с использованием той же функции между всеми библиотеками DLL. В моем случае все библиотеки DLL используют malloc и свободны от EXE-хоста, поэтому все они используют одну и ту же кучу, которая решает проблему памяти кучи. (Точно, как вы понять это до вас.)

Также обратите внимание, что вам нужны специализации для pod<T*>, pod<const T*> и стручок для всех основных типов (pod<int>, pod<short> и т.д.), так что они могут быть сохранены в " pod vector "и другие контейнеры для контейнеров. Они должны быть достаточно просты, чтобы писать, если вы понимаете приведенный выше пример.

Этот метод означает копирование всего объекта. Однако вы можете передавать ссылки на типы контейнеров, поскольку есть 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 не был там.

+0

Удивительный ответ, это решит проблемы, с которыми я столкнулся, спасибо, что написал это! – Contango

+0

Для всех поисковых систем там появляется ошибка «Отладка подтверждения ошибки! Выражение: _pFirstBlock == pHead», если вы передадите std :: string обратно через границу .dll, если вы компилируете в MSVC с помощью «Runtime library: Multi-threaded Debug (/ MTd) "(или Release). – Contango

0

Для std::string вы можете вернуться с помощью c_str. В случае более сложного материала, вариант может быть что-то вроде

class ContainerValueProcessor 
    { 
    public: 
     virtual void operator()(const trivial_type& value)=0; 
    }; 

Затем (если вы хотите использовать зЬй :: список), вы можете использовать интерфейс

class List 
    { 
    public: 
     virtual void processItems(ContainerValueProcessor&& proc)=0; 
    }; 

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

Смежные вопросы