Реализация отложенной загрузки библиотек на С++
Андрей Солодовников
Вы все еще грузите библиотеки вручную?
Тогда мы идем к вам!
По специфике моей работы мне довольно часто приходится вручную загружать библиотеки и динамически, при помощи GetProcAddress, импортировать множество функций. Это происходит отчасти потому, что требуется обеспечить совместимость с различными версиями Windows, в которых целевые функции могут отсутствовать, отчасти потому, что так бывает удобнее (например, при реализации механизма плагинов). Конечно, всегда хочется это автоматизировать, особенно если функций и библиотек много. С одной стороны, в линейке Visual C++ для этого есть поддержка компилятора\линкера в виде механизма Delay Load, с другой стороны, бытует мнение, что использовать этот метод является дурным тоном, и, наверное, это так. Одна из основных причин, которую хочется отметить особенно – этот механизм является microsoft-specific, то есть никаких гарантий, что написанный Вами код будет работать и на других компиляторах или платформах, нет. Более того, несколько раз «попав» на странное поведение этого механизма (например, см.
Q218613), мы от его использования в своих проектах отказались.Следующим шагом был поиск готового подходящего функционала. Как ни странно, такого не находилось, несмотря на то, что проблема действительно имеет место быть. Многие решения были слишком просты и неоптимальны (например,
это решение). Они не позволяли определять импорт сразу нескольких функций из одной библиотеки, либо для этого нужно было написать приличное количество кода. Они вызывали GetProcAddress и LoadLibrary в любое время, когда им вздумается, а на самом деле – чуть ли не при каждом обращении к импортируемой функции. Другие (например, такое решение) было достаточно сложно и неудобно использовать.ПРИМЕЧАНИЕНа самом деле, указанные варианты вполне могут быть использованы в небольших проектах, когда не требуется импортировать большое количество функций. Однако их использование в любом случае требует достаточно много усидчивости и терпения, по крайней мере, меня это не устраивало. |
И общий недостаток всех этих решений – они были и есть неоптимальны. Особенно это касается количества кода, генерируемого компилятором (да и программистом) на одну импортируемую функцию и быстродействия полученного кода.
Все это, вкупе с потраченным временем, сподвигло меня к необходимости написания очередного велосипеда в виде библиотеки эмуляции Delay Load, а также и этой статьи.
Требования к библиотеке, реализующей механизм Delay load
В данном параграфе мы рассмотрим более подробно, каким базовым требованиям должен удовлетворять механизм динамической загрузки библиотек.
Исходя из описанного выше, можно сформулировать следующие требования к механизму поддержки динамической загрузки библиотек:
Как можно большая независимость от компилятора С++ (в пределах ANSI C++). Минимальные требования к компилятору – библиотека должна быть полностью функциональна на всех Visual C++ компиляторах, начиная с Visual C++ 6.0;
Минимальное количество кода, генерируемого компилятором, которое приходится на одну импортируемую функцию;
Удобство определения в проекте импортируемых библиотек\функций;
Возможность задания своих стратегий (реакций) на ошибки загрузки библиотеки\нахождения функции;
Минимизация вызовов LoadLibrary. Для одной библиотеки (модуля) вызов LoadLibrary должен производится один раз вне зависимости от количества импортируемых из нее функций. Данный механизм должен работать не только в пределах одной единицы трансляции, но и проекта в целом. Таким образом, должна создаваться единая для приложения таблица используемых модулей;
Минимизация вызовов GetProcAddress. GetProcAddress должен вызываться только при первом обращении к импортируемой функции, в дальнейшем все вызовы импортируемой функции должны производиться напрямую;
Библиотека должна обеспечивать привычный синтаксис вызова – не должно быть никаких внешних отличий от обычного вызова функции из С/С++;
Должны поддерживаться UNICODE версии импортируемых функций, причем, желательно, на основе заголовков от соответствующих статически линкуемых библиотек, в которых определены соответствующие макросы.
Из описанных выше требований наиболее важными и интересными представляются пункты 3,5,6 и 7. Особенности их реализации будут рассмотрены более подробно далее вместе с программной реализацией библиотеки. Для тех, кому детали реализации не интересны, а интересны методы использования библиотеки, предлагается приступать сразу к разделу Использование библиотеки.
Предлагаемая реализация библиотеки
Класс, инкапсулирующий работу с модулями
Для начала рассмотрим требование (5) к реализации загрузки библиотек\модулей. Очевидно, что обеспечить выполнение данного требования с условием уникальности экземпляра библиотеки в пределах всех единиц трансляции проекта можно использованием паттерна Singlton для загружаемого модуля. При этом для каждого различного загружаемого модуля должен создаваться собственный экземпляр синглтона, который и будет обеспечивать «одноразовую» загрузку в конструкторе и в дальнейшем выгрузку библиотеки в деструкторе. Эта задача решается определением шаблонного класса CModule. Имя библиотеки должно служить значением, относительно которого производится инстанцирование объекта, инкапсулирующего загрузку библиотеки. Поскольку в качестве паттерна Singlton используется синглтон Мейерса, то в качестве бонуса мы получаем отложенную загрузку библиотек (поскольку создание экземпляра синглтона производится при первом обращении к порождающей функции).
СОВЕТНапомню, что простейшая реализация синглтона Мейерса выглядит следующим образом: | ||
template <class T>struct CMeyersSinglton{ static T& GetInstance() { static T obj;return obj; }}; |
В связи с этим первый вариант определения шаблона CModule мог бы выглядеть так:
template <LPCTSTR Name>class CModule; |
Тут следует сделать небольшое отступление. Как было бы прекрасно, если бы любой абстрактный язык программирования, используемый нами, обеспечивал бы любую востребованную нами возможность. Но, очевидно, по соображениям здравого смысла, это невыполнимо, поэтому приходится пользоваться тем, что есть. А есть такая неприятная вещь – в С++ напрямую инстанцировать шаблон строковым литералом не получится. Шаблон может быть инстанцирован только константой с external linkage, а строковый литерал имеет internal linkage. На первый взгляд, все достаточно печально. Однако, как обычно, решение лежит на поверхности. Оно очень простое и очевидное. Мы будем инстанцировать шаблон модуля уникальным классом, инкапсулирующим строковый литерал. Сам же класс будет формироваться при помощи макросов:
#define DECLARE_NAME_ID_IMPL(id, name, ret, text)\struct NAME_ID(id)\{\ enum {length = sizeof(name)};\ static ret GetStr(){return text(name);}\};#define DECLARE_NAME_ID_A(id, name) DECLARE_NAME_ID_IMPL(id, name, LPCSTR, DL_EMPTY())#define DECLARE_NAME_ID(id, name) DECLARE_NAME_ID_IMPL(id, name, LPCTSTR,_T) |
Данный класс является универсальным и будет использован в дальнейшем и для представления имен импортируемых функций. Но и тут есть один маленький нюанс – поскольку функция GetProcAddress использует только ANSI строки, то мы вынуждены это предусмотреть, объявив дополнительный макрос DECLARE_NAME_ID_A.
Итак, в связи со всем вышеизложенным, определение шаблона CModule без учета стратегий будет выглядеть так:
template <class Name>class CModule; |
Теперь добавим стратегии загрузки\выгрузки модуля. Поскольку стратегия контролирует процессы, связанные с загрузкой и выгрузкой, у нее должно быть как минимум 2 функции. Одна отвечает за загрузку модуля, вторая за его выгрузку:
struct CModulePolicy{ static HMODULE Load(LPCTSTR szFileName); static BOOL Free(HMODULE hModule);}; |
Теперь у нас есть все, что необходимо для полного написания класса CModule. Реализация его в предлагаемой библиотеке приведена в листинге ниже:
struct CModuleLoadLibraryPolicy{ static HMODULE Load(LPCTSTR szFileName) { return ::LoadLibrary(szFileName); } static BOOL Free(HMODULE hModule) { return ::FreeLibrary(hModule); }};struct CModuleGetModuleHandlePolicy{ static HMODULE Load(LPCTSTR szFileName) { return ::GetModuleHandle(szFileName); } static BOOL Free(HMODULE hModule) { return TRUE; }};template <class Name, class LoadPolicy = CModuleLoadLibraryPolicy>class CModule{public: typedef CModule<Name, LoadPolicy> type; typedef Name name_type; static type &GetModule() {#ifdef DL_MT static volatile LONG lMutex = FALSE; CLWMutex theMutex(lMutex); CAutoLock<CLWMutex> autoLock(theMutex);#endif //DL_MT static type Module; return Module; } HMODULE GetModuleHandle() const { return m_hModule; } BOOL IsLoaded() const { return m_hModule != NULL; }// Caution - use with care. Not thread-safe BOOL UnloadModule() { HMODULE hModule = m_hModule; m_hModule = NULL; return LoadPolicy::Free(hModule); } ~CModule() { if (m_hModule) UnloadModule(); }private: CModule() { m_hModule = LoadPolicy::Load(name_type::GetStr());} HMODULE m_hModule;}; |
Класс модуля позволяет явно выгружать библиотеку (модуль) при помощи функции UnloadModule, однако пользоваться этой возможностью надо с большой осторожностью.
Реализация динамического поиска функций и глобальной таблицы импорта
Теперь рассмотрим детали реализации пунктов 6 и 7 (поиск адресов импортируемых функций и их вызов). Это наиболее нетривиальная и интересная в плане программирования часть библиотеки, поскольку функции могут иметь различное число параметров, а также различные типы возвращаемых значений. И напомню основное требование – естественный синтаксис вызова функций и минимизация обращений к GetProcAddress.
В данном случае для обеспечения требования минимизации вызовов GetProcAddress мы будем использовать технику создания прокси-функций. Фактически, при первом вызове импортируемой функции мы будем попадать в сформированную компилятором прокси-функцию, в которой будет производиться поиск адреса функции по ее имени в библиотеке и в зависимости от успешности поиска производится либо вызов функции, либо выполнение операции, заданной в стратегии реакции на ошибки поиска. Для того, чтобы в дальнейшем вызывалась непосредственно импортируемая функция, а не прокси, адрес, полученный в прокси, запоминается в глобальной для всех единиц трансляции таблице указателей на функции. Для создания таблицы используется техника, подобная применяемой в синглтоне Мейерса. В сильно упрощенном виде это выглядит так: