В последнее время я увлёкся темой зависимости от C Runtime в проектах, написанных на Visual C++. Вернее, темой избавления от зависимости от Visual C++ Redistributable, ведь если проект представляет собой небольшую библиотеку интеграции или простейшую утилиту, таскать за собой целый распространяемый пакет не очень удобно.
На Хабре уже была статья на данную тему, однако я в процессе своих экспериментов столкнулся с некоторыми проблемами. Об этих проблемах и о способе их решения и пойдёт речь.
Сразу оговорюсь, что я изначально ожидаю, в целом, справедливой критики на счёт правильности подхода к проблемам и вообще в целом линковки с msvcrt.dll — да, это не поддерживаемое Microsoft решение, да, это решение больше подходит для новых проектов, и да, возможно, придётся отказаться от многих плюшек, но ведь это используют, да и кто не рискует… В общем, все, кому интересна эта тема, как и мне, — прошу под кат.
Заранее прошу прощения за заголовок: я старался перевести фразу «Linking to msvcrt.dll in Visual C++». Статья моя, это не перевод, но название всё-таки проще сформулировать на английском.
Проблематика
Любое решение когда-то было проблемой. В том смысле, что поиск некоего решения всегда начинается с появления некой проблемы. Я уже упомянул, что одной из причин, почему я занялся этим вопросом, было нежелание зависеть от Visual C++ Redistributable. Но ведь есть известные и, кроме того, официально поддерживаемые способы решения этой проблемы, почему же не воспользоваться ими и не морочить себе голову?
Альтернативных решения, на самом деле, два. Нет, можно, конечно, сменить компилятор, но это может создать другие зависимости и прочие неудобства.
Второе решение — это избавиться от C Runtime вообще. То есть, совсем. О том, почему это неудобно, даже говорить нечего: Вы просто теряете здоровую часть удобного функционала, и Вам, скорее всего, придётся изобретать велосипеды. Флаг в руки, конечно, но я решил, что не надо так делать.
В сухом остатке я пришёл к выводу, что оба альтернативных варианта мне не подходят. К тому же, существует ещё одна причина неприемлемости первого решения, которая заключается в том, что при динамической подгрузке библиотеки с собственным рантаймом в исполняемый процесс, в случае, если рантайм, с которым была скомпилирована подгружаемая библиотека, отличается от рантайма, используемого исполняемым процессом, могут возникнуть разнообразные проблемы. А именно так и используется библиотека, из-за которой и возник весь сыр-бор.
Третье решение
Совершенно ясно, что нужно искать какое-то третье решение, которое решит сразу все описанные проблемы. Здесь-то и вспоминается, что ОС Windows, начиная с некоторой версии Windows 2000 (если не ошибаюсь, с Service Pack 4) и выше, включает в себя msvcrt.dll — тот самый C Runtime, используемый ныне внутри Microsoft, а некогда использовавшийся в Visual C++ 6 и в Windows Driver Kit (WDK).
Наша задача — выполнить компоновку (можно я буду говорить «прилинковать»?) проекта с этой самой версией 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 и т.д.
WinDDK7600.16385.1libCrti386
WinDDK7600.16385.1libwxpi386
В таком случае не придётся задавать параметр /NODEFAULTLIB для всех или каждой отдельной стандартной библиотеки и вручную указывать компоновщику зависимость от msvcrt.lib из WDK.
Поскольку проект, о котором неявно идёт речь, должен работать на всех версиях Windows, начиная с XP, объектный файл msvcrt_winxp.obj становится незаменимым помощником и… первой проблемой.
Проблема первая: а был ли мальчик?
Рассмотрим простейший код:
int main(int argc, _TCHAR* argv[])
{
_TCHAR szStr[13] = { '' };
_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 <exception>
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 <exception>
Благодаря объявлению _STATIC_CPPLIB
директива _CRTIMP_PURE
будет объявлена как пустая строка. Ну, а объявление _DISABLE_DEPRECATE_STATIC_CPPLIB
говорит само за себя.
Рассмотрим ещё один кусок кода:
#include <list>
int main(int argc, wchar_t* argv[])
{
std::list<std::string> 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
возвращает указатель на структуру _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. Код из файла — под спойлером.
#include <exception>
#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++.
Тем не менее, следует учитывать, что данное решение далеко от гибкого, и поэтому может потребовать от вас изменений в коде, в том числе значительных, поэтому не факт, что в вашем конкретном случае оно вам подойдёт.
Автор: SgtRiggs91