возвращение std :: string / std :: list из dll - PullRequest
12 голосов
/ 25 августа 2010

Короткий вопрос.

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

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

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

Ответы [ 4 ]

12 голосов
/ 25 августа 2010

Главное, что нужно иметь в виду, это то, что DLL содержат код , а не память . Выделенная память принадлежит процессу (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) Да, я знаю, что статические и локальные объекты создаются / удаляются при загрузке / выгрузке dll.

9 голосов
/ 25 августа 2010

У меня есть именно эта проблема в проекте, над которым я работаю - классы 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 для всех основных типов (pod<int>, pod<short> и т. Д.), Чтобы они могли храниться в «векторе 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 отсутствовал.

2 голосов
/ 25 августа 2010

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

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

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

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

0 голосов
/ 02 июля 2016

Для 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 теперь может быть реализован любым контейнером.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...