Реализация отложенной загрузки библиотек на С++
Андрей Солодовников
Вы все еще грузите библиотеки вручную?
Тогда мы идем к вам!
Краткая предыстория
По специфике моей работы мне довольно часто приходится
вручную загружать библиотеки и динамически, при помощи 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
struct CMeyersSinglton
{
static T& GetInstance()
{
static T obj;
return obj;
}
};
В связи с этим первый вариант определения шаблона
CModule мог бы выглядеть так:
template
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 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 CModule
{
public:
typedef CModule type;
typedef Name name_type;
static type &GetModule()
{
#ifdef DL_MT
static volatile LONG lMutex =
FALSE;
CLWMutex theMutex(lMutex);
CAutoLock
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
struct CGlobalProxyTable
{
static FARPROC &GetProxy()
{
static FARPROC proxy;
return proxy;
}
};
В данном примере для каждого входного типа будет
сгенерирован уникальный глобальный указатель типа FARPROC, фактически
являющийся ячейкой глобальной в терминах единиц трансляций таблицы функций.
Для того, чтобы определить интерфейс ячейки таблицы
функций, выясним, от чего зависит импортируемая функция. Очевидно, это имя
функции, модуль, из которого надо ее импортировать, и прокси, используемый для
определения адреса функции в загружаемой библиотеке. В связи с этим определим
класс CDynFunction, инкапсулирующий ячейку для хранения адреса функции в
глобальной таблице импортируемых функций:
template
class CDynFunction
Учитывая все вышесказанное, реализация класса
тривиальна и будет выглядеть так:
template
class CDynFunction
{
public:
typedef CDynFunction 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::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::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
struct FUN_PROXY_IMPL(param_count)
{
template 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::MakeReturn();
}
};
};
template
struct FUN_PROXY_IMPL(param_count)
{
template 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::MakeReturn();
}
};
};
template >
struct FUN_PROXY(param_count)
{
typedef R (WINAPI
*fun_type)(DL_REPEAT_N(param_count, P));
typedef R ret_type;
template struct Proxy:public
FUN_PROXY_IMPL(param_count)::template RetProxy
{
};
};
Ключевым в реализации является макрос
DECLARE_FUN_PROXY(param_count), который определяет шаблон класса прокси-функции
с количеством параметров импортируемой функции, указанным в param_count. В
результате применения этого макроса порождается набор шаблонных классов
прокси-функций для количества параметров от 1 до 16. Макросы DL_REPEAT_N и
DL_REPEAT_PARAM_N формируют список формальных и поименованных параметров
соответственно.
В целом, после подстановки макросов, получаемый класс
для количества параметров n выглядит так:
template >
struct CFunProxyn
{
typedef R (WINAPI
*fun_type)(P1, P2, .. , Pn));
typedef R ret_type;
template
struct Proxy:public CFunProxynImpln::template
RetProxy
{
};
};
Ключевым является вложенный шаблон Proxy, именно он
наследует прокси-функцию ProxyFun из CFunProxynImpl. Класс CFunProxynImpl
необходим из-за невозможности вернуть тип void при помощи оператора return в
Visual C++ 6.0. В качестве обходного маневра используется специализация
реализации прокси по типу возвращаемого значения – отдельно для типа void и
отдельно для всех остальных типов.
Прокси-функция ProxyFun будет использована в
CDynFunction для первоначальной инициализации адреса указателя на функцию:
static typename proxy_type::fun_type &GetProxy()
{
static typename
proxy_type::fun_type proxy = proxy_type::template
Proxy::ProxyFun;
return proxy;
}
Для обеспечения возможности реакции на ошибку
нахождения функции в модуле используется соответствующая стратегия. Стратегия
состоит из класса, вложенного в него шаблона, принимающего в качестве параметра
тип ячейки таблицы импортируемых функций и имеющего статическую функцию
MakeReturn, которая и вызывается при ошибке поиска адреса функции или при
ошибке загрузки библиотеки. На данный момент реализованы 2 стратегии. Одна
(CFunProxyThrowPolicy) – выбрасывает исключение (по умолчанию CDynFunException)
при ошибке поиска функциизагрузки библиотеки, другая (CFunProxyValuePolicy) –
возвращает определенное пользователем значение:
template
struct CFunProxyThrowRetTypeTrait
{
template
struct FunctionTraitImpl
{
static R MakeReturn()
{
F::MakeReturnImpl();
return R();
}
};
};
template
struct CFunProxyThrowRetTypeTrait
{
template
struct FunctionTraitImpl
{
static void MakeReturn()
{
F::MakeReturnImpl();
}
};
};
template
struct CFunProxyThrowPolicy
{
template
struct FunctionTrait:public
CFunProxyThrowRetTypeTrait::template
FunctionTraitImpl >
{
static void MakeReturnImpl()
{
TCHAR
szMessage[DynFunction::name_type::length + 64];
_stprintf(szMessage,
_T("Can'n resolve procedure : %d"),
DynFunction::name_type::GetStr(), GetLastError());
throw E(szMessage);
}
};
};
// we need not implement void return type value policy,
// coz void function can only throw on error
template
struct CFunProxyValuePolicy
{
template
struct FunctionTrait
{
static typename
DynFunction::proxy_type::ret_type MakeReturn()
{
return value;
}
};
};
Последние штрихи
Собственно, на этом основные элементы библиотеки
реализованы, теперь необходимо описать базовые макросы, которые позволят
использовать ее более просто. В библиотеке для объявления импортируемых функций
используется интерфейс, сильно напоминающий карту сообщений MFC. Интерфейс
состоит из 3-х типов макросов.
Макросы, определяющие модуль и открывающие секцию
импортируемых из него функций (DL_USE_xxx_BEGIN);
Макросы, определяющие импортируемые функции
(DL_DECLARE_FUN_xxx);
Макрос, закрывающий секцию импорта
(DL_USE_MODULE_END).
Таким образом, традиционное объявление динамически
импортируемых из библиотеки функций выглядит как
// объявление библиотеки и
пространства имен функций, импортируемых из нее
DL_USE_MODULE_xxx_BEGIN(name_space, “some_lib.dll”)
DL_DECLARE_FUN_xxx(ImportedFunction1Name, …
)
DL_DECLARE_FUN_xxx(ImportedFunction2Name, …
)
…
DL_USE_MODULE_END()
Исходя из описанного интерфейса, определены следующие
базовые макросы:
Макрос DL_USE_MODULE_LOAD_POLICY_BEGIN(nmspace, name,
load_policy)
#define DL_USE_MODULE_LOAD_POLICY_BEGIN(nmspace, name, load_policy)
namespace nmspace
{
DECLARE_NAME_ID(DL_CAT(_MODULE_, nmspace),
name)
typedef
delayload::CModule
module_type;
определяет в пространстве имен nmspace (тем самым
открывая секцию импорта функций для данной библиотеки) класс модуля,
используемого для загрузки библиотеки с именем name, при этом применяя политику
загрузки load_policy. Также в пространстве имен функций импортируемой
библиотеки определяется тип module_type, который представляет собой тип класса
модуля для данной библиотеки и может быть использован для управления временем
жизни библиотеки, например, для ее выгрузки при помощи статического метода
UnloadModule.
Макрос DL_DECLARE_FUN_ERR_POLICY(name_id, r, p, pl)
#define DL_DECLARE_FUN_ERR_POLICY(name_id, r, p, pl)
DECLARE_NAME_ID_A(name_id, DL_STRINGIZE(name_id))
static r (WINAPI *&name_id)(DL_SEQ_ENUM(p)) =
delayload::CDynFunction
>::GetProxy();
определяет ссылку name_id на указатель на функцию с
именем name_id, типом возвращаемого значения r, списком параметров p и
политикой реакции на ошибку загрузки библиотекипоиска функции pl. Изначально
этот указатель указывает на соответствующую прокси-функцию, однако после
первого вызова функции указатель указывает непосредственно на саму функцию.
Таким образом, использование импортируемой функции из программы тривиально –
это обычный вызов функции из пространства имен (nmspace::name_id).
Неочевидной, но интересной особенностью такой
реализации становится то, что автоматически добавляется поддержка UNICODE
версий импортируемых функций при подключении заголовков от соответствующих
статически линкуемых библиотек, где определены макросы ИмяФункцииW и
ИмяФункцииA.
Использование библиотеки
Так как при создании библиотеки одной из основных
целей было обеспечение простоты ее использования, то наиболее подходящим
интерфейсом объявления импортируемых библиотек и функций оказался интерфейс,
внешне напоминающий карты сообщений MFC. В библиотеке определено несколько
макросов, которые значительно упрощают ее использование. Это макросы:
DL_USE_MODULE_BEGIN(nmspace, name) – открывает секцию
импорта функций из библиотеки. Параметр nmspace – название пространства имен, в
которое будет помещены импортируемые функции, name – имя библиотеки, которую
необходимо загрузить. Для загрузки используется LoadLibrary;
DL_USE_MODULE_NON_LOAD_BEGIN(nmspace, name) –
аналогично предыдущему, однако для загрузки используется GetModuleHandle;
DL_DECLARE_FUN(name_id, r, p) – определяет функцию с
именем name_id, типом возвращаемого значения r, и списком типов параметров p в
виде (type1)(type2)…(typen). В случае ошибки при загрузке библиотекипоиске
функции из функции возвращается значение r(). Для функций с возвращаемым
значением void использование данного макроса не имеет смысла, поскольку
распознать ошибку возможным не представится (а в случае Visual C++ 6.0 это
просто не скомпилируется);
DL_DECLARE_FUN_ERR(name_id, r, p, e) – аналогично
предыдущему, однако в случае ошибки при загрузке библиотекипоиске функции возвращается
не r(), а значение, указанное в параметре e;
DL_DECLARE_FUN_THROW(name_id, r, p) – аналогично
предыдущему, однако в случае ошибки при загрузке библиотекипоиске функции
выбрасывается исключение CDynFunException;
DL_USE_MODULE_END() – закрывает секцию импорта функций
из модуля.
При вызове функции будет использоваться синтаксис
nmspace::name_id.
Рассмотрим пример использования библиотеки в реальной
программе:
#include "stdafx.h"
#include
#include
"../delayimphlp.h"
// объявление секции
импорта из kernel32.dll
DL_USE_MODULE_BEGIN(kernel, "kernel32.dll")
DL_DECLARE_FUN_ERR(GetProcAddress, FARPROC,
(HMODULE)(LPCTSTR), NULL)
DL_DECLARE_FUN(GetModuleHandle,
HMODULE, (LPCTSTR))
DL_DECLARE_FUN_THROW(InitializeCriticalSection,
void, (LPCRITICAL_SECTION))
DL_USE_MODULE_END()
int main(int argc, char* argv[])
{
try
{
CRITICAL_SECTION cs;
HMODULE hm =
kernel::GetModuleHandle("ntdll.dll");
kernel::InitializeCriticalSection(&cs);
FARPROC p =
kernel::GetProcAddress(hm, "NtQuerySystemInformation");
}
catch
(delayload::CDynFunException &E)
{
::MessageBox(NULL,
E.GetMessage(), NULL, MB_OK | MB_ICONERROR);
}
return 0;
}
В данном примере мы загружаем библиотеку kernel32.dll,
затем импортируем из нее функции GetProcAddress, GetModuleHandle и
InitializeCriticalSection. Как видим, все достаточно просто и тривиально. В
случае наличия стандартных заголовков к статически линкуемым библиотекам, где
при помощи макросов определены ANSI и UNICODE варианты импортируемых функций,
при подключении этих заголовков в зависимости от типа проекта (ANSI или
UNICODE), соответствующим образом будут меняться и динамически импортируемые
функции, обеспечивая импорт корректных версий функций.
Заключение
Итак, в данной статье рассмотрен инструментарий,
позволяющий удобно использовать в коде множество динамически загружаемых
библиотек и импортируемых из них функций, попутно рассмотрев несколько
интересных приемов программирования на C++ в условиях ограниченной поддержки
шаблонов. Библиотека получилась, на мой взгляд, достаточно гибкая и хорошо
расширяемая, требует достаточно мало ресурсов в плане памятикода и получаемый
при ее использовании результат в большинстве случаев по быстродействию не уступает
статически импортируемым функциям. Многое в ней реализовано так, а не иначе, из
расчета поддержки как можно большего количества компиляторов. Библиотека
проверялась на работоспособность с Visual C++ 6.0, 7.0 и 7.1, но особых проблем
при портировании на другие компиляторы (кроме, пожалуй, линейки от Borland)
быть не должно. Автор выражает благодарность всем участникам обсуждения данной
библиотеки на форуме RSDN за полезные мысли, советы и поправки. Надеюсь, что
данная библиотека поможет хотя бы частично упростить жизнь программистам WinAPI
и не только.
Список литературы
Для подготовки данной работы были использованы
материалы с сайта http://www.rsdn.ru/