Возврат не примитивного типа C ++ из функции DLL, связанной со статической средой выполнения (/ MT или / MTd) - PullRequest
8 голосов
/ 15 октября 2011

Предположим, у нас есть динамическая библиотека ( "HelloWorld.dll" ), которая скомпилирована с Microsoft Visual Studio 2010 из следующего исходного кода:

#include <string>

extern "C" __declspec(dllexport) std::string hello_world()
{
    return std::string("Hello, World!"); // or just: return "Hello, World!";
}

И у нас также есть исполняемый файл ( "LoadLibraryExample.exe" ), который динамически загружает эту DLL с помощью LoadLibrary Функция WINAPI:

#include <iostream>
#include <string>

#include <Windows.h>

typedef std::string (*HelloWorldFunc)();

int main(int argc, char* argv[])
{
    if (HMODULE library = LoadLibrary("HelloWorld.dll"))
    {
        if (HelloWorldFunc hello_world = (HelloWorldFunc)GetProcAddress(library, "hello_world"))
            std::cout << hello_world() << std::endl;
        else
            std::cout << "GetProcAddress failed!" << std::endl;

        FreeLibrary(library);
    }
    else
        std::cout << "LoadLibrary failed!" << std::endl;
    std::cin.get();
}

Это прекрасно работает при подключении к динамической библиотеке времени выполнения (переключатели / MD или / MDd ).

Проблема возникает, когда я связываю их (библиотека и исполняемый файл) с отладочной версией статической библиотеки времени выполнения (переключатель / MTd ). Программа, кажется, работает ( «Hello, World!» отображается в окне консоли), но затем вылетает со следующим выводом:

HEAP[LoadLibraryExample.exe]: Invalid address specified to RtlValidateHeap( 00680000, 00413F60 )
Windows has triggered a breakpoint in LoadLibraryExample.exe.

This may be due to a corruption of the heap, which indicates a bug in LoadLibraryExample.exe or any of the DLLs it has loaded.

This may also be due to the user pressing F12 while LoadLibraryExample.exe has focus.

The output window may have more diagnostic information.

Проблема волшебным образом не возникает с выпуском версии статической библиотеки времени выполнения (переключатель / MT ). Я предполагаю, что версия выпуска просто не видит ошибку, но она все еще там.

После небольшого исследования я нашел эту страницу в MSDN, в которой говорится следующее:

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

Таким образом, библиотека и исполняемый файл имеют свои собственные копии CRT, которые имеют свои собственные состояния. Экземпляр std :: string создается в библиотеке (с некоторыми выделениями внутренней памяти, выполняемыми CRT библиотеки) и затем возвращается в исполняемый файл. Исполняемый файл отображает его и затем вызывает его деструктор (что приводит к освобождению внутренней памяти с помощью CRT исполняемого файла). Как я понимаю, именно здесь возникает ошибка: базовая память std :: string выделяется одним CRT и пытается быть освобождена другим.

Проблема не появляется, если мы возвращаем примитивный тип (int, char, float и т. Д.) Или указатель из DLL, потому что в этих случаях нет выделения памяти или освобождения. Однако попытка удалить возвращенного указателя в исполняемом файле приводит к той же ошибке (а не удаление указателя, очевидно, приводит к утечке памяти).

Итак, вопрос: возможно ли обойти эту проблему?

P.S .: Я действительно не хочу зависеть от MSVCR100.dll и заставлять пользователей моего приложения устанавливать любые распространяемые пакеты.

P.P.S: Приведенный выше код выдает следующее предупреждение:

warning C4190: 'hello_world' has C-linkage specified, but returns UDT 'std::basic_string<_Elem,_Traits,_Ax>' which is incompatible with C

, который можно устранить, удалив extern "C" из объявления библиотечной функции:

__declspec(dllexport) std::string hello_world()

и изменение вызова GetProcAddress следующим образом:

GetProcAddress(library, "?hello_world@@YA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ")

(имя функции оформляется компилятором C ++, фактическое имя можно получить с помощью утилиты dumpbin.exe ). Затем предупреждение исчезает, но проблема остается.

P.P.P.S: я вижу возможное решение в предоставлении пары функций в библиотеке для каждой такой ситуации: одна возвращает указатель на некоторые данные, а другая - удаляет указатель на эти данные. В этом случае память выделяется и освобождается с помощью одного и того же ЭЛТ. Но это решение кажется очень уродливым и не дружественным, так как мы всегда должны работать с указателями, и, кроме того, программист должен всегда не забывать вызывать специальную библиотечную функцию для удаления указателя, а не просто использовать ключевое слово delete .

1 Ответ

11 голосов
/ 15 октября 2011

Да, это основная причина, по которой / MD существует в первую очередь. Когда вы создаете DLL с / MT, она получит свою собственную копию CRT. Который создает свою собственную кучу для выделения. Возвращаемый вами объект std :: string будет размещен в этой куче.

Когда клиентский код пытается освободить этот объект, все идет не так. Он вызывает оператор удаления, и он пытается освободить память в своей собственной куче . В Vista и Win7 диспетчер памяти Windows замечает, что его просят освободить блок кучи, который не является частью кучи, и что отладчик подключен. Он генерирует автоматическое прерывание отладчика и диагностическое сообщение, чтобы сообщить вам о проблеме. Очень хорошо между прочим.

Очевидно, что / MD решает проблему, и ваша DLL, и клиентский код будут использовать одну и ту же копию CRT и, следовательно, одну и ту же кучу. Это не верное решение, вы все равно столкнетесь с проблемами, связанными с DLL, созданной на основе другой версии CRT. Как msvcr90.dll вместо msvcr100.dll.

Единственное полное безошибочное решение - ограничить API, предоставляемый вами из DLL. Не возвращайте указатели на объекты, которые должны быть освобождены клиентским кодом. Назначьте владение объектами для модуля, который его создал. Подсчет ссылок является распространенным решением. А если вам необходимо использовать кучу, которая используется всем кодом в процессе, то подойдет либо куча процесса по умолчанию (GlobalAlloc), либо куча COM (CoTaskMemAlloc). Также не позволяйте исключениям пересекать барьер, та же проблема. COM Automation abi является хорошим примером.

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