Смешивание версий MSVCRT

Итак, у меня есть библиотека C++ со статически связанной копией MSVCRT. Я хочу, чтобы каждый мог использовать мою библиотеку с любой версией среды выполнения MSVC. Как лучше всего достичь этой цели?

Я уже достаточно осторожен с тем, как все делается.

  1. Память никогда не преодолевает барьер DLL для освобождения
  2. Объекты среды выполнения C++ не передаются через барьеры (например, векторы, карты и т. д., если только они не были созданы на той стороне барьера).
  3. Между барьерами не передаются дескрипторы файлов или дескрипторы ресурсов.

Тем не менее, у меня все еще есть простой код, который вызывает повреждение кучи.

У меня есть такой объект в моей библиотеке:

class Foos
{
public: //There is an Add method, but it's not used, so not relevant here
    DLL_API Foos();
    DLL_API ~Foos();

private:
    std::map<std::wstring, Foo*> map;
};

Foos::~Foos()
{
    // start at the begining and go to the end deleting the data object
    for(std::map<std::wstring, Foo*>::iterator it = map.begin(); it != map.end(); it++)
    {
        delete it->second;
    }
    map.clear();
}

И затем я использую его из своего приложения следующим образом:

void bar() {
    Foos list;
}

После вызова этой функции из любого места я получаю отладочное предупреждение о повреждении стека. И если я действительно позволю ему закончиться, это действительно повредит стек и segfault.

Мое вызывающее приложение скомпилировано с помощью инструментов платформы Visual Studio 2012. Библиотека скомпилирована с помощью инструментов платформы Visual Studio 2010.

Это просто то, что мне абсолютно не следует делать, или я на самом деле нарушаю правила использования нескольких сред выполнения?


person Earlz    schedule 14.11.2013    source источник
comment
Ошибка по-прежнему возникает, если вы удаляете код из деструктора? (Я спрашиваю, потому что этот код не должен работать)   -  person anatolyg    schedule 14.11.2013
comment
@anatolyg да, я все еще получаю повреждение стека при удалении всего кода из деструктора   -  person Earlz    schedule 14.11.2013
comment
Все, что он делает, это создает поле map (потому что это не указатель), а затем уничтожает его. Это, по-видимому, вызывает повреждение стека. Если я перейду к VS2010 и создам свое приложение таким образом, оно будет работать нормально.   -  person Earlz    schedule 14.11.2013


Ответы (4)


Память никогда не преодолевает барьер DLL

Но это так. На самом деле много раз. Ваше приложение создало хранилище для объекта класса, в данном случае в стеке. А затем передает указатель на методы в библиотеке. Начиная с вызова конструктора. Это указатель this внутри кода библиотеки.

Что не так в сценарии, подобном этому, так это то, что он не создал правильный объем хранилища. У вас есть компилятор VS2012, чтобы просмотреть объявление вашего класса. Он использует реализацию std::map в VS2012. Однако ваша библиотека была скомпилирована с помощью VS2010, она использует совершенно другую реализацию std::map. С совсем другим размером. Огромные изменения благодаря C++11.

Это просто полное повреждение памяти на работе, код в вашем приложении, который записывает переменные стека, повредит std::map. И наоборот.

Предоставление классов C++ через границы модуля наполнено подобными ловушками. Когда-либо рассматривайте это только тогда, когда вы можете гарантировать, что все скомпилировано с одной и той же версией компилятора и точно такими же настройками. Никаких ярлыков, вы также не можете смешивать код сборки Debug и Release. Создание библиотеки таким образом, чтобы никакие детали реализации не раскрывались, безусловно, возможно, но вы должны соблюдать следующие правила:

  • Предоставляйте только чистые интерфейсы с виртуальными методами, типы аргументов должны быть простыми типами или указателями интерфейса.
  • Используйте фабрику классов для создания экземпляра интерфейса
  • Используйте подсчет ссылок для управления памятью, чтобы всегда освобождалась библиотека.
  • Зафиксируйте основные детали, такие как упаковка и соглашение о вызовах, с жесткими правилами.
  • Никогда не позволяйте исключениям пересекать границу модуля, используйте только коды ошибок.

К тому времени вы уже будете на пути к написанию COM-кода, а также к стилю, который вы видите, например, в DirectX.

person Hans Passant    schedule 14.11.2013

Переменная-член map по-прежнему создается приложением с некоторыми внутренними данными, выделенными приложением, а не DLL (и они могут использовать разные реализации map). Как правило, не используйте объекты стека из DLL, добавьте что-то вроде Foos * CreateFoos() в свою DLL.

person Alex Watson    schedule 14.11.2013
comment
Ну, тогда это в основном обертка всех распределений. Я попробовал new Foos(), и тогда в delete list было бы повреждение кучи. - person Earlz; 14.11.2013
comment
@Earlz: это сложнее, как правило, вы не хотите раскрывать внутренности какой-либо структуры данных, которая может иметь другую двоичную компоновку или может выделять память с помощью функций CRT (и подобъекты STL будут иметь разную двоичную компоновку и выделить память с помощью new). На практике это означает, что вам придется прибегать к идиоме PIMPL практически везде. - person Matteo Italia; 14.11.2013
comment
@MatteoItalia На самом деле это (ваш комментарий) лучший ответ! - person ; 14.11.2013

Объекты среды выполнения C++ не передаются через барьеры (например, векторы, карты и т. д., если только они не были созданы на той стороне барьера).

Вы делаете именно это. Ваш объект Foos создается основной программой в стеке, а затем используется в библиотеке. Объект содержит карту как его часть...

Когда вы компилируете основную программу, она просматривает файлы заголовков и т. д., чтобы определить, сколько места в стеке нужно выделить для объекта Foos. И вызывает конструктор, который определен в библиотеке... Который, возможно, ожидал совершенно другого макета/размера объекта.

person jcoder    schedule 14.11.2013
comment
Обратите внимание, что это зависит не только от версии CRT, но даже от флагов компиляции. Объекты STL имеют другое расположение памяти при компиляции в режиме отладки или выпуска. - person Matteo Italia; 14.11.2013
comment
Итак, если бы я изменил бит map, чтобы он был указателем, и был выделен и освобожден в конструкторе/деструкторе, это, вероятно, было бы в порядке, потому что тогда DLL контролировала бы выделение... Я думал даже перечисление полей private в файлах заголовков было необязательным, хотя, думаю, это необходимо для распределения. - person Earlz; 14.11.2013
comment
Да, это, вероятно, сработает. Нет списка приватных полей не является обязательным :) - person jcoder; 14.11.2013

Это может не соответствовать вашим потребностям, но не забывайте, что реализация всего этого в заголовочных файлах упрощает проблему (вроде) :-)

person manuell    schedule 14.11.2013