возвращение 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 (т.е. что-либо, что могло бы вызвать delete в моем коде для блока памяти, который был выделен (возможно, с другим crt) в dll).

Прав ли я или нет?

12
задан SigTerm 25 August 2010 в 13:47
поделиться

3 ответа

Главное, о чем следует помнить, это то, что dll содержат код, а не память. Выделенная память принадлежит процессу(1). Когда вы создаете экземпляр объекта в своем процессе, вы вызываете код конструктора. В течение жизни этого объекта вы будете вызывать другие фрагменты кода (методы) для работы с памятью этого объекта. Затем, когда объект уходит, вызывается код деструктора.

Шаблоны STL явно не экспортируются из dll. Код статически связан с каждой dll. Поэтому, когда std::string создается в a.dll и передается в b.dll, каждая dll будет иметь два разных экземпляра метода string::copy. copy, вызванное в dll, вызывает метод копирования 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) Да, я знаю, что статические и локальные файлы создаются/удаляются при загрузке/выгрузке dll.

12
ответ дан 2 December 2019 в 06:43
поделиться

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

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

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

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

2
ответ дан 2 December 2019 в 06:43
поделиться

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

Я решил эту проблему с помощью шаблонного класса с именем pod (POD означает обычные старые данные, такие как символы и целые числа, которые обычно хорошо передаются между DLL). Задача этого класса состоит в том, чтобы упаковать свой параметр шаблона в согласованный двоичный формат, а затем распаковать его на другом конце. Например, вместо функции в DLL, возвращающей std::vector, вы возвращаете pod>. Существует специализация шаблона для pod>, которая выделяет буфер памяти и копирует элементы.Он также предоставляет оператор std::vector(), так что возвращаемое значение может быть прозрачно сохранено обратно в 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> — обратите внимание, частичная специализация используется, поэтому этот класс работает для любого типа T. Также обратите внимание, на самом деле он хранит буфер памяти pod, а не просто 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 и free, но с использованием одной и той же функции для всех DLL. В моем случае все библиотеки DLL используют malloc и свободны от исполняемого файла хоста, поэтому все они используют одну и ту же кучу, что решает проблему с памятью кучи. (Как именно вы это поймете, зависит от вас.)

Также обратите внимание, что вам нужны специализации для pod, pod и pod для все основные типы (pod, pod и т. д.), чтобы их можно было хранить в «векторе pod» и других контейнерах pod. Они должны быть достаточно простыми для написания, если вы понимаете приведенный выше пример.

Этот метод подразумевает копирование всего объекта.Однако вы можете передавать ссылки на типы подов, поскольку существует operator=, который является безопасным между двоичными файлами. Однако реальной передачи по ссылке не существует, поскольку единственный способ изменить тип модуля — это скопировать его обратно в исходный тип, изменить его, а затем переупаковать как модуль. Кроме того, копии, которые он создает, означают, что это не обязательно самый быстрый способ, но он работает.

Тем не менее, вы также можете специализировать свои собственные типы, что означает, что вы можете эффективно возвращать сложные типы, такие как std::map>, если есть специализация для pod и частичная специализация для std::map, std::vector и std::basic_string (которую вам нужно написать только один раз).

Конечный результат использования выглядит следующим образом. Определен общий интерфейс:

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();

Поэтому, как только он настроен, вы можете использовать его почти так, как если бы класса стручка не было.

9
ответ дан 2 December 2019 в 06:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: