Подтвердить что ты не робот

Возвращение std::string/std:: list из dll

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

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

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

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

4b9b3361

Ответ 1

Главное, чтобы иметь в виду, что 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.

Вот почему в ответе Саймона он говорит:

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

потому что, если по какой-либо причине копия string s отличается между a.dll и b.dll, будут происходить странные вещи. Еще хуже, если сама строка отличается между a.dll и b.dll, а деструктор в одном знает, чтобы очистить дополнительную память, которую другой игнорирует... вам может быть трудно отследить утечки памяти. Возможно, еще хуже... a.dll, возможно, был создан против совершенно другой версии STL (т.е. STLPort), в то время как b.dll создается с использованием Microsoft STL.

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

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

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

Ответ 2

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

Здесь определение класса 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 и free, но с использованием одной и той же функции между всеми 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 не был там.

Ответ 3

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

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

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

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

Ответ 4

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