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

#c #dll

#c #dll

Вопрос:

Рассмотрим, что у нас есть динамическая библиотека («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). Кажется, что программа работает («Привет, мир!» отображается в окне консоли), но затем происходит сбой со следующим выводом:

 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, созданная путем ссылки на статический CRT, будет иметь свое собственное состояние CRT, не рекомендуется статически связываться с CRT в DLL, если только последствия этого не являются желательными и понятными.

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

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

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

PS: я действительно не хочу зависеть от 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
  

который может быть решен путем удаления внешнего «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: Я вижу возможное решение в предоставлении пары функций в библиотеке для каждой такой ситуации: одна, которая возвращает указатель на некоторые данные, а другая, которая удаляет указатель на эти данные. В этом случае память выделяется и освобождается с помощью одного и того же CRT. Но это решение кажется очень уродливым и недружелюбным, поскольку мы всегда должны работать с указателями, и, кроме того, программист должен всегда помнить о вызове специальной библиотечной функции для удаления указателя вместо простого использования ключевого слова delete .

Ответ №1:

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

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

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

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

Комментарии:

1. Обратите внимание, что Visual C 2012 CRT не создает частную кучу; он использует кучу процесса (это изменение по сравнению с предыдущими версиями).

2. Вау, я вижу это в heapinit.c, потрясающе. Это совершенно новая игра с пользовательским кодом, которая таинственным образом приводит к сбою кода Windows 😉 Спасибо @James.