Компонування з 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 <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 ++.
 
Проте, слід враховувати, що дане рішення далеко від гнучкого, і тому може зажадати від вас змін в коді, в тому числі значних, тому не факт, що у вашому конкретному випадку воно вам підійде.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.