Xreferat.com » Рефераты по информатике и программированию » Реализация отложенной загрузки библиотек на С++

Реализация отложенной загрузки библиотек на С++

Андрей Солодовников

Вы все еще грузите библиотеки вручную?

Тогда мы идем к вам!

Краткая предыстория

По специфике моей работы мне довольно часто приходится вручную загружать библиотеки и динамически, при помощи 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 мы будем использовать технику создания прокси-функций. Фактически, при первом вызове импортируемой функции мы будем попадать в сформированную компилятором прокси-функцию, в которой будет производиться поиск адреса функции по ее имени в библиотеке и в зависимости от успешности поиска производится либо вызов функции, либо выполнение операции, заданной в стратегии реакции на ошибки поиска. Для того, чтобы в дальнейшем вызывалась непосредственно импортируемая функция, а не прокси, адрес, полученный в прокси, запоминается в глобальной для всех единиц трансляции таблице указателей на функции. Для создания таблицы используется техника, подобная применяемой в синглтоне Мейерса. В сильно упрощенном виде это выглядит так:

template <class Proxy>

struct CGlobalProxyTable

{

 static FARPROC &GetProxy()

 {

 static FARPROC proxy;

 return proxy;

 }

};

В данном примере для каждого входного типа будет сгенерирован уникальный глобальный указатель типа FARPROC, фактически являющийся ячейкой глобальной в терминах единиц трансляций таблицы функций.

Для того, чтобы определить интерфейс ячейки таблицы функций, выясним, от чего зависит импортируемая функция. Очевидно, это имя функции, модуль, из которого надо ее импортировать, и прокси, используемый для определения адреса функции в загружаемой библиотеке. В связи с этим определим класс CDynFunction, инкапсулирующий ячейку для хранения адреса функции в глобальной таблице импортируемых функций:

template <class Module, class Name, class Proxy>

class CDynFunction

Учитывая все вышесказанное, реализация класса тривиальна и будет выглядеть так:

template <class Module, class Name, class Proxy>

class CDynFunction

{

public:

 typedef CDynFunction<Module, Name, Proxy> type;

 typedef Proxy proxy_type;

 typedef Module module_type;

 typedef Name name_type;

 

 static typename proxy_type::fun_type &GetProxy()

 {

 static typename proxy_type::fun_type proxy = proxy_type::template Proxy<type>::ProxyFun;

 return proxy;

 }

 static BOOL InitFunction()

 {

#ifdef DL_MT

 static volatile LONG lMutex = FALSE;

#endif // DL_MT

 const module_type &theModule = module_type::GetModule();

 if (theModule.IsLoaded())

 return DL_GetProcAddressImpl(

#ifdef DL_MT

 lMutex,

 (const FARPROC)proxy_type::template Proxy<type>::ProxyFun,

#endif //DL_MT

 (volatile FARPROC &)GetProxy(),

 theModule.GetModuleHandle(),

 name_type::GetStr()

 );

 return FALSE;

 }

};

Функция DL_GetProcAddressImpl представляет собой обертку GetProcAddress, и вынесена в отдельный функциональный элемент для уменьшения размера кода при поддержке многопоточности. Статический метод GetProxy() вернет глобальный в смысле единиц трансляции адрес в таблице функций, причем изначально по этому адресу находится адрес прокси функции. Таким образом, вызывая функцию по указателю, полученному при помощи GetProxy(), мы первоначально вызываем прокси, а в дальнейшем будем вызывать импортируемую функцию напрямую.

Реализация прокси функций

До этого момента все было достаточно очевидно и довольно просто. Однако при попытке реализации класса, определяющего функционал прокси-функции, мы сталкиваемся с проблемами. Чтобы понять, в чем они заключаются, рассмотрим параметры, необходимые для генерации прокси функции. Это:

тип возвращаемого значения импортируемой функции;

список типов параметров импортируемой функции;

стратегия реакции на ошибку поиска функции в модуле;

тип ячейки глобальной таблицы указателей на импортируемые функции (CDynFunction), который будет использован при создании прокси.

Как известно, С++ не поддерживает шаблоны с переменным количеством параметров. В связи с этим придется использовать генерацию экземпляров шаблона при помощи макросов а-ля boost::preprocessor. Объяснять подробно здесь, как это работает, я не буду – это тема для отдельной статьи. Кроме того, все это удовольствие осложняется тем, что Visual C 6.0 не может возвращать из void функции тип void. Для обхода этой проблемы приходится создавать отдельные классы для «нормальных» типов и для void, а затем использовать специализацию шаблона по возвращаемому значению с последующим наследованием.

Рассмотрим реализацию, предлагаемую в библиотеке:

#define FUN_PROXY(n) DL_CAT(CFunProxy,n)

#define FUN_PROXY_IMPL(n) DL_CAT(FUN_PROXY(n),Impl)

#define DECLARE_FUN_PROXY(param_count)

template <typename R>

struct FUN_PROXY_IMPL(param_count)

{

 template <class DynFunction, DL_REPEAT_N(param_count, typename P), class Policy> struct RetProxy

 {

 static R WINAPI ProxyFun(DL_REPEAT_PARAM_N(param_count, P, v))

 {

 if (DynFunction::InitFunction())

 return DynFunction::GetProxy()(DL_REPEAT_N(param_count, v));

 return Policy::template FunctionTrait<DynFunction>::MakeReturn();

 }

 };

};

template <>

struct FUN_PROXY_IMPL(param_count) <void>

{

 template <class DynFunction, DL_REPEAT_N(param_count, typename P), class Policy> struct RetProxy

 {

 static void WINAPI ProxyFun(DL_REPEAT_PARAM_N(param_count, P, v))

 {

 if (DynFunction::InitFunction())

 DynFunction::GetProxy()(DL_REPEAT_N(param_count, v));

 else

 Policy::template FunctionTrait<DynFunction>::MakeReturn();

 }

 };

Если Вам нужна помощь с академической работой (курсовая, контрольная, диплом, реферат и т.д.), обратитесь к нашим специалистам. Более 90000 специалистов готовы Вам помочь.
Бесплатные корректировки и доработки. Бесплатная оценка стоимости работы.

Поможем написать работу на аналогичную тему

Получить выполненную работу или консультацию специалиста по вашему учебному проекту
Нужна помощь в написании работы?
Мы - биржа профессиональных авторов (преподавателей и доцентов вузов). Пишем статьи РИНЦ, ВАК, Scopus. Помогаем в публикации. Правки вносим бесплатно.

Похожие рефераты: