Компоновка с msvcrt.dll в Visual C++: проблемы и решения

В последнее время я увлёкся темой зависимости от C Runtime в проектах, написанных на Visual C++. Вернее, темой избавления от зависимости от Visual C++ Redistributable, ведь если проект представляет собой небольшую библиотеку интеграции или простейшую утилиту, таскать за собой целый распространяемый пакет не очень удобно.На Хабре уже была статья на данную тему, однако я в процессе своих экспериментов столкнулся с некоторыми проблемами. Об этих проблемах и о способе их решения и пойдёт речь.

Сразу оговорюсь, что я изначально ожидаю, в целом, справедливой критики на счёт правильности подхода к проблемам и вообще в целом линковки с msvcrt.dll — да, это не поддерживаемое Microsoft решение, да, это решение больше подходит для новых проектов, и да, возможно, придётся отказаться от многих плюшек, но ведь это используют, да и кто не рискует… В общем, все, кому интересна эта тема, как и мне, — прошу под кат.

Заранее прошу прощения за заголовок: я старался перевести фразу «Linking to msvcrt.dll in Visual C++». Статья моя, это не перевод, но название всё-таки проще сформулировать на английском.

ПроблематикаЛюбое решение когда-то было проблемой. В том смысле, что поиск некоего решения всегда начинается с появления некой проблемы. Я уже упомянул, что одной из причин, почему я занялся этим вопросом, было нежелание зависеть от Visual C++ Redistributable. Но ведь есть известные и, кроме того, официально поддерживаемые способы решения этой проблемы, почему же не воспользоваться ими и не морочить себе голову? Альтернативных решения, на самом деле, два. Нет, можно, конечно, сменить компилятор, но это может создать другие зависимости и прочие неудобства.

Альтернативные решения Первое — это статическая компоновка. То есть, вместо ключа компоновщика /MD использовать ключ /MT, тем самым вшив в себя необходимую часть C Runtime. Это плохо сразу по нескольким причинам. Во-первых, это означает, что проект сразу вырастает в размерах, и чем бóльшую часть рантайма он использует, тем больше он становится. Во-вторых, в случае, если в решении сразу несколько зависящих друг от друга проектов, то каждый из них будет содержать копию рантайма, а значит, размер конечного проекта станет ещё больше. В-третьих, если вдруг Microsoft выпустит обновление на рантайм, это обновление проект не затронет. А вдруг там что-то критично важное? Придётся пересобирать.Второе решение — это избавиться от C Runtime вообще. То есть, совсем. О том, почему это неудобно, даже говорить нечего: Вы просто теряете здоровую часть удобного функционала, и Вам, скорее всего, придётся изобретать велосипеды. Флаг в руки, конечно, но я решил, что не надо так делать.

В сухом остатке я пришёл к выводу, что оба альтернативных варианта мне не подходят. К тому же, существует ещё одна причина неприемлемости первого решения, которая заключается в том, что при динамической подгрузке библиотеки с собственным рантаймом в исполняемый процесс, в случае, если рантайм, с которым была скомпилирована подгружаемая библиотека, отличается от рантайма, используемого исполняемым процессом, могут возникнуть разнообразные проблемы. А именно так и используется библиотека, из-за которой и возник весь сыр-бор.Третье решение Совершенно ясно, что нужно искать какое-то третье решение, которое решит сразу все описанные проблемы. Здесь-то и вспоминается, что ОС Windows, начиная с некоторой версии Windows 2000 (если не ошибаюсь, с Service Pack 4) и выше, включает в себя msvcrt.dll — тот самый C Runtime, используемый ныне внутри Microsoft, а некогда использовавшийся в Visual C++ 6 и в Windows Driver Kit (WDK).Немного про msvcrt.dll Microsoft в своё время отказалась — перестала поддерживать и стала не рекомендовать — от использования msvcrt.dll при компиляции проектов Visual C++, а всё из-за потенциального DLL hell: если каждое приложение будет устанавливать в системе свою копию msvcrt.dll, то будет нехорошо. Тем не менее, если не пытаться подменить системные файлы, а пользоваться имеющейся версией с учётом её особенностей, проблем не возникнет. Кроме того, в WDK вплоть до (включая) версии для Windows 7 проекты компоновались именно с системной версией msvcrt.dll.

Наша задача — выполнить компоновку (можно я буду говорить «прилинковать»?) проекта с этой самой версией C Runtime. Подробнее об этом можно прочитать в статье, упомянутой в начале, я же перейду сразу к делу: берём последнюю версию WDK (7.1.0).WDK 7.1.0 помимо прочего включает в себя стандартную статическую библиотеку msvcrt.lib, соответствующую версии msvcrt.dll в Windows 7, а также набор объектных файлов, содержащих, по сути, все различия между текущей (Windows 7) версией C Runtime, и теми, которые были в предыдущих версиях Windows. Собственно, объектные файлы и называются соответственно: msvcrt_win2000.obj, msvcrt_winxp.obj и т.д.

Как прилинковать msvcrt.dll к проекту Я предполагаю, что читатель немного подкован в том, как изменить свойства проекта Visual C++, поэтому не буду особо заострять внимание на том, как непосредственно прилинковать msvcrt.dll к проекту. Я предпочитаю указывать в параметре Additional Library Directories компоновщика пути к следующим папкам WDK:\WinDDK\7600.16385.1\lib\Crt\i386\WinDDK\7600.16385.1\lib\wxp\i386

В таком случае не придётся задавать параметр /NODEFAULTLIB для всех или каждой отдельной стандартной библиотеки и вручную указывать компоновщику зависимость от msvcrt.lib из WDK.

Поскольку проект, о котором неявно идёт речь, должен работать на всех версиях Windows, начиная с XP, объектный файл msvcrt_winxp.obj становится незаменимым помощником и… первой проблемой.Проблема первая:, а был ли мальчик? Рассмотрим простейший код: int main (int argc, _TCHAR* argv[]) { _TCHAR szStr[13] = { '\0' }; _stprintf_s (szStr, 13, _T («Hello world!»)); return 0; } Этот код использует макрос _stprintf_s, который в Юникод-проекте разворачивается в swprintf_s. В свою очередь, функция swprintf_s присутствует в msvcrt.dll образца Windows Vista и выше, но отсутствует в msvcrt.dll в Windows XP. Для такого случая и существует объектный файл msvcrt_winxp.obj: он в числе прочих содержит реализацию и этой функции, совместимую с Windows XP.Добавляем в Input компоновщика msvcrt_winxp.obj, компилируем, смотрим в Dependency Walker и… снова видим там зависимость от swprintf_s в msvcrt.dll. Как же так? Размер исполняемого файла немного подрос, значит, что-то из объектного файла всё же «пришло» в наш код. Но был ли мальчик, то есть, функция swprintf_s?

На самом деле, поскольку функция была благополучно найдена в msvcrt.lib, а в заголовочных файлах Visual C++, которые мы используем, эта функция оказывается обозначена как _CRTIMP_ALTERNATIVE, разворачивающееся в __declspec (dllimport), берётся именно объявление функции из msvcrt.lib, а не из объектного файла.

Решение подсказывает тот самый _CRTIMP_ALTERNATIVE, а вернее следующий кусок кода в crtdefs.h:

#ifdef _CRT_ALTERNATIVE_INLINES #define _CRTIMP_ALTERNATIVE … Именно объявление _CRT_ALTERNATIVE_INLINES позволит считать, что _CRTIMP_ALTERNATIVE объявлен как пустая строка, а не __declspec (dllimport), и в таком случае объявление функции swprintf_s будет взято из объектного файла, содержащего её полную реализацию. Добавляем в Preprocessor Definitions в свойствах проекта объявление _CRT_ALTERNATIVE_INLINES, компилируем, и видим, что зависимость от функции swprintf_s исчезла, правда, принеся с собой дополнительные килобайты к размеру файла.В действительности, если посмотреть в заголовочные файлы Visual C++, объявление _CRTIMP_ALTERNATIVE содержится при многих функциях так называемого «безопасного CRT» (safe CRT), так что трюк с _CRT_ALTERNATIVE_INLINES сработает для всех подобных функций.

Подводный камень — куда ж без него — заключается в том, что не все функции safe CRT объявлены как _CRTIMP_ALTERNATIVE — например, к ним не относится функция wprintf_s. Более того, такие функции могут отсутствовать в msvcrt_winxp.obj, так что придётся искать им замену. Хорошая новость: если вы разрабатываете без оглядки на версии Windows ниже Windows 7, эта проблема вас вообще не коснётся.

Проблема вторая: исключения Ваш код на Visual C++ наверняка содержит обработку исключений. Если даже этого не делаете вы, возможно, исключения обрабатываются в классах стандартной библиотеки, которые вы используете. Рассмотрим такой код: #include int main (int argc, _TCHAR* argv[]) { try { throw std: exception («Hello Exception»); } catch (std: exception) { } return 0; } Немного наигранно, конечно, но суть проблемы раскрывает. Если вы попробуете скомпилировать этот код с линковкой к msvcrt.dll, вы получите пачку ошибок компиляции: error LNK2001: unresolved external symbol »__declspec (dllimport) public: __thiscall std: exception: exception (char const * const &)» (__imp_?0exception@std@@QAE@ABQBD@Z)error LNK2001: unresolved external symbol »__declspec (dllimport) public: virtual __thiscall std: exception::~exception (void)» (__imp_?1exception@std@@UAE@XZ)error LNK2001: unresolved external symbol »__declspec (dllimport) public: __thiscall std: exception: exception (class std: exception const &)» (__imp_?0exception@std@@QAE@ABV01@@Z)

Дело в том, что по какой-то причине msvcrt.dll не экспортирует некоторые функции класса std: exception. Поэтому придётся придумать что-то, чтобы реализация этих функций «находилась» в нашем коде. К счастью, такая возможность предусмотрена.

Если взглянуть на std: exception, то можно увидеть, что при объявленной директиве _DEFINE_EXCEPTION_MEMBER_FUNCTIONS реализация функций std: exception не импортируется, а описывается как есть. Значит, можно просто объявить эту директиву перед включением файла exception, и всё? Не совсем. Сам класс std: exception объявлен как _CRTIMP_PURE, и если просто объявить указанную выше директиву, компиляция упадёт со следующей ошибкой:

error LNK2001: unresolved external symbol »__declspec (dllimport) const std: exception::`vftable'» (__imp_?_7exception@std@@6B@)

При этом в выводе компилятора будет несколько предупреждений вида warning C4273: 'std: exception: exception' : inconsistent dll linkage. `vftable' — это таблица виртуальных функций, так что придётся сделать так, чтобы класс std: exception не импортировался, а реализовывался.

Решение есть: нужно сделать так, чтобы _CRTIMP_PURE был объявлен как пустая строка. Самый простой способ сделать это — создать в проекте новый файл, например, msvcrt_link.cpp, и отключить для него использование Precompiled Header. Дело в том, что _CRTIMP_PURE объявляется ещё при включении Windows.h, так что скорее всего, ваш Precompiled Header вам помешает. Тогда содержимое файла msvcrt_link.cpp должно выглядеть примерно так:

#define _DISABLE_DEPRECATE_STATIC_CPPLIB #define _STATIC_CPPLIB

#define _DEFINE_EXCEPTION_MEMBER_FUNCTIONS #include Благодаря объявлению _STATIC_CPPLIB директива _CRTIMP_PURE будет объявлена как пустая строка. Ну, а объявление _DISABLE_DEPRECATE_STATIC_CPPLIB говорит само за себя.Рассмотрим ещё один кусок кода:

#include int main (int argc, wchar_t* argv[]) { std: list strList; strList.push_back («Hello World!»); return 0; } Здесь обработка исключений выполняется неявно в классе std: list, и если исключения мы уже «поправили», то здесь нас поджидают ещё две ошибки: error LNK2001: unresolved external symbol »__declspec (dllimport) void __cdecl std::_Xlength_error (char const *)» (__imp_?_Xlength_error@std@@YAXPBD@Z)error LNK2001: unresolved external symbol »__declspec (dllimport) void __cdecl std::_Xout_of_range (char const *)» (__imp_?_Xout_of_range@std@@YAXPBD@Z)

Эти две функции можно легко найти в исходниках CRT, поставляемых вместе с Visual C++, так что решение этой проблемы довольно прагматично — в созданный ранее файл msvcrt_link.cpp добавляем:

#include <../crt/src/xthrow.cpp> Подводные камни: если обработка исключений вам пригодится наверняка, то с классом std: list это по сути стрельба из пушки по воробьям. Не факт, что этого будет достаточно, и не факт, что снаряд в воробья попадёт, поэтому не исключено, что вам придётся самостоятельно искать решение. Возможно, что-то придётся заменить: например, regex проще поменять, чем стрелять по нему из этой пушки. О том, что это решение может потребовать отказа от многих плюшек, я предупредил ещё в начале статьи.Бонус. Проблема третья: C++/CLI На самом деле, мне так понравился подход с линковкой к msvcrt.dll, что я решил попробовать использовать этот подход в библиотеке, написанной на C++/CLI. И вы знаете? Получилось.Мне не пришлось использовать практически никаких функций и классов стандартной библиотеки C++, поскольку для моих целей вполне хватало .NET Framework. Поэтому единственной функцией, отсутствовавшей в msvcrt.dll при компиляции С++/CLI библиотеки, оказалась:

error LNK2001: unresolved external symbol «extern «C» int __cdecl __FrameUnwindFilter (struct _EXCEPTION_POINTERS *)» (?__FrameUnwindFilter@@$$J0YAHPAU_EXCEPTION_POINTERS@@@Z)

Функция __FrameUnwindFilter является частью процесса обработки исключений, так что её реализация была бы очень кстати ;)

Реализацию этой функции можно легко найти в исходниках CRT, поставляемых с Visual Studio 2013. Кроме вызовов EHTRACE_ENTER и EHTRACE_EXIT, объявления которых я найти не смог и за сим их опустил (в конце концов, трейс — не катастрофа, правда?), основную проблему представлял вызов функции _getptd, которая из msvcrt.dll не экспортируется, хоть и реализована там.

Что я узнал о _getptd () Функция _getptd возвращает указатель на структуру _tiddata, содержащую разнообразную информацию текущего потока. Эта структура создаётся для потока в недрах CRT, так что так просто получить на неё указатель вряд ли получится. А нам из этой структуры нужно, в частности, поле _ProcessingThrow, задающее количество обрабатываемых в текущий момент исключений. Прошу знатоков поправить, если что-то не так.

Решение снова подсказали исходные коды CRT. Есть такая функция, _errno, которая возвращает указатель на значение, содержащее код ошибки. А значение это — не что иное, как третье по счёту поле в структуре _tiddata, которая нам и нужна! Учитывая объявление _tiddata, найденное всё в тех же исходниках CRT, получаем следующую функцию на замену _getptd: #define ENOMEM 12 #define _RT_THREAD 16 extern «C» void __cdecl _amsg_exit (int); _ptiddata __cdecl _my_getptd (void) { int *pErrno = _errno (); if (ENOMEM == *pErrno) { _amsg_exit (_RT_THREAD); } intptr_t ptdAddr = (intptr_t)pErrno — sizeof (uintptr_t) — sizeof (unsigned long); return (_ptiddata)ptdAddr; } Стоит обратить внимание на функцию _amsg_exit. Дело в том, что в коде _errno используется функция _getptd_noexit. Функция же _getptd сначала вызывает _getptd_noexit, и затем, если этот вызов вернул NULL, выполняет _amsg_exit (_RT_THREAD). Функция же _errno в случае, если вызов _getptd_noexit вернул NULL, возвращает указатель на целочисленное значение ENOMEM.Это единственная сложность, с которой я столкнулся при реализации __FrameUnwindFilter, остальную часть функции восполнить было гораздо проще. Эта реализация также отправляется в отдельный .cpp-файл, поскольку её необходимо компилировать в обязательном порядке без ключей /clr. Код из файла — под спойлером.

msvcrt_link_for_clr.cpp #include #include <../crt/src/mtdll.h>

#define ENOMEM 12 #define _RT_THREAD 16 /* not enough space for thread data */

// The NT Exception # that we use #define EH_EXCEPTION_NUMBER ('msc' | 0xE0000000) // Pre-V4 managed exception code #define MANAGED_EXCEPTION_CODE 0XE0434F4D // V4 and later managed exception code #define MANAGED_EXCEPTION_CODE_V4 0XE0434352

extern «C» int __cdecl __FrameUnwindFilter (EXCEPTION_POINTERS *pExPtrs); extern «C» void __cdecl _amsg_exit (int); /* crt0.c */

_ptiddata __cdecl _my_getptd (void) { int *pErrno = _errno (); if (ENOMEM == *pErrno) { _amsg_exit (_RT_THREAD); } intptr_t ptdAddr = (intptr_t)pErrno — sizeof (uintptr_t) — sizeof (unsigned long); return (_ptiddata)ptdAddr; }

extern «C» int __cdecl __FrameUnwindFilter (EXCEPTION_POINTERS *pExPtrs) { EXCEPTION_RECORD *pExcept = pExPtrs→ExceptionRecord;

switch (pExcept→ExceptionCode) { case EH_EXCEPTION_NUMBER: _my_getptd ()→_ProcessingThrow = 0; terminate ();

case MANAGED_EXCEPTION_CODE: case MANAGED_EXCEPTION_CODE_V4: if (_my_getptd ()→_ProcessingThrow > 0) { --_my_getptd ()→_ProcessingThrow; } std: uncaught_exception (); return EXCEPTION_CONTINUE_SEARCH;

default: return EXCEPTION_CONTINUE_SEARCH; } } Заключение Я хочу ещё раз заметить: то, о чём я пишу в этой статье, совершенно необязательно подходит именно лично Вам, уважаемый читатель, однако это не значит, что оно не подходит кому-то другому. Я лишь делюсь с Вами, уважаемый читатель, опытом решения моей конкретной проблемы.Моя теоретическая основа, возможно, не слишком богата, однако она вкупе с опытом использования такого решения подсказывает, что проблем в процессе работы возникнуть должно не больше, чем при стандартной компоновке с библиотеками, соответствующими версии Visual C++.

Тем не менее, следует учитывать, что данное решение далеко от гибкого, и поэтому может потребовать от вас изменений в коде, в том числе значительных, поэтому не факт, что в вашем конкретном случае оно вам подойдёт.

© Habrahabr.ru