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

в 16:03, , рубрики: c++, msvcrt, visual c++, Visual Studio, windows, магия, Программирование

В последнее время я увлёкся темой зависимости от 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:

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()

Функция _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 <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

Источник

* - обязательные к заполнению поля