另一个用于动态加载的 DLL 包装器






4.88/5 (11投票s)
一个DLL包装器,在切换到动态加载时无需更改代码,并提供详细的错误检查功能。
dynlink项目也托管在https://code.google.com/p/dynlink/
简介
尤其是在通过供应商提供的API控制硬件时,以及在尝试避免因某些共享库(DLL)在系统上不可用而在应用程序启动期间出现错误时,**动态加载**此类DLL是一种常用技术。
从C++链接到DLL的各种方法已在本网站的几篇文章中进行了介绍。对于那些对该主题不太熟悉的人,我推荐Hans Dietrich的文章Step by Step: Calling C++ DLLs from VC++ and VB - Part 4。不幸的是,动态加载涉及大量重复但容易出错的编码(见下文),并且在DLL的API发生变化时,在保持代码最新方面需要格外小心。为了减少必要的编码量,已经提出了**DLL包装器**,例如,请参阅LateLoad DLL Wrapper。除了避免代码重复外,这种包装器还允许进行更复杂的错误检查。
但是,函数声明仍然必须从DLL的头文件中提取,保持最新,并且访问API的代码需要更改。另一方面,所谓的**延迟加载**不需要任何代码更改或额外的代码,但它缺乏动态加载的灵活性。
本文提出了一种新型包装器,在动态加载和静态加载之间切换时仅需最少的代码更改,但仍提供现有DLL包装器的所有功能以及更多功能。
背景
为什么使用动态加载?
Hans Dietrich的文章中列出了几个动态加载可能有用甚至必要的良好理由,其中大多数在7年后仍然有效。除了那里列出的原因外,我还遇到过需要在响应用户输入或根据我访问的硬件版本链接不同版本的DLL的情况,而且,相信与否,我曾不得不将一个DLL以不同的名称加载两次,以便能够访问两个相同的硬件。
延迟加载来帮忙?
延迟加载也在此处进行了详细描述。其本质是,它通过让链接器生成必要的代码来自动化动态加载过程。如果你愿意,它是一个链接器生成的DLL包装器。事实上,它解决了其他两种方法的某些难题:无需代码更改,因为DLL的延迟加载只需通过链接器开关即可启用,而且与静态加载不同,即使应用程序链接到一个不存在的库,或者导入了一个在可用DLL版本中缺失的符号,应用程序仍然可以启动。现有符号在您的代码首次访问时在运行时加载。
缺点是,如果不进行额外的错误检查,当您尝试访问一个不在加载的DLL中(但属于导入库一部分)的符号,或者在首次访问符号时无法加载DLL时,您的应用程序就会崩溃。为了确保这种情况不会发生,您可以手动调用__HrLoadAllImportsForDll
(参见delayimp.h,但请注意:LoadLibrary
不区分大小写,但此API区分大小写),并在未加载所有符号时禁用DLL的使用,或者您可以尝试通过设置在加载符号失败时调用的钩子来实现更细粒度的错误检查。但这涉及到调用比GetProcAddress()
复杂得多的API,因此编写(不必要地)复杂的代码。
传统的方法有什么问题?
没什么。它只是不够优雅,需要大量的复制粘贴和搜索替换,而且容易出错,尤其是在API更改或供应商提供的头文件与DLL不同步时(嗯,或者,如果您混淆它们)。此外,它要求您在静态链接和动态链接之间切换时更改代码。
演示代码包含了在您的应用程序中动态加载psapi.dll所需 ancak,并对此进行了说明。理想情况下,您创建一个头文件,例如psapi_dynamic.h,其中包含必要的typedef
和函数指针声明。它还包含一个加载DLL和符号的函数的声明,load_psapi()
#ifndef _PSAPI_DYNLINK_H_
#define _PSAPI_DYNLINK_H_
#include <psapi.h>
typedef BOOL
(WINAPI *
EnumProcesses_t)(
__out_bcount(cb) DWORD * lpidProcess,
__in DWORD cb,
__out LPDWORD lpcbNeeded
);
typedef BOOL
(WINAPI *
EnumProcessModules_t)(
__in HANDLE hProcess,
__out_bcount(cb) HMODULE *lphModule,
__in DWORD cb,
__out LPDWORD lpcbNeeded
);
...
extern EnumProcesses_t pEnumProcesses;
extern EnumProcessModules_t pEnumProcessModules;
...
// Try to load the psapi.dll
// returns
// 0 if library was not found,
// 1 if all symbols were loaded and
// 2 if some symbols are NULL
int psapi_load();
如果您擅长正则表达式,这个头文件可以在几分钟内从psapi.h创建。最好保持与原始头文件中声明的格式和顺序尽可能一致。这使得您可以使用良好的文件比较工具检查和更新typedef
,并使此解决方案非常易于维护。
总而言之,每个符号在您的动态加载代码中会出现四次:一次作为typedef
,一次在函数指针声明中,两次在相应的.cpp文件中,其中定义了函数指针并在load_psapi()
函数中加载了符号。
#include "stdafx.h"
#include "psapi_dynamic.h"
EnumProcesses_t pEnumProcesses= NULL;
EnumProcessModules_t pEnumProcessModules= NULL;
...
int load_psapi()
{
HMODULE hDll = LoadLibrary(_T("psapi.dll"));
if (! hDll)
return 0;
bool b_ok = true;
b_ok &= NULL != (pEnumProcesses = (EnumProcesses_t)
GetProcAddress(hDll, "EnumProcesses"));
b_ok &= NULL != (pEnumProcessModules = (EnumProcessModules_t)
GetProcAddress(hDll, "EnumProcessModules"));
...
return b_ok ? 1 : 2;
}
因此,添加新的函数/更改函数名称或删除函数需要四个不同位置的编辑。最好先编辑typedef
列表,编译器将提醒您完成其余工作。
在您的代码中,您只需将DLL中的所有函数调用替换为相应的前缀“p”或您选择的任何其他前缀即可。
...
// Try to load the library
if (0 == psapi_load())
return;
// Check whether the symbol was loaded
if (! pGetProcessImageFileNameW)
return;
TCHAR sName1[1024];
pGetProcessImageFileNameW(GetCurrentProcess(), sName1, 1024);
...
如果我们即使在某些符号未加载的情况下继续,如上所述,我们最好不要忘记检查pGetProcessImageFileW
是否为NULL
。虽然这些额外的检查和函数(指针)名称的更改使向动态加载的过渡永久化,但该方法提供了许多优于延迟加载或静态加载的优势:您可以通过在运行时检查可用性来启用或禁用功能,而不是检查文档和读取DLL版本以找出哪些函数可以安全调用。
DLL包装器
但是,如果您想生成有意义的错误消息或为未加载的函数或变量提供默认函数(存根),您很快就会为每个动态加载的库编写相同的代码。此方案的任何增强功能都必须为每个库手动添加(或者,更可能地,不添加),并且函数和变量存根需要大量额外的编码。
这一切都要求使用宏和包装器,而这正是**DLL包装器**的作用。在前面提到的LateLoad DLL Wrapper中,您会输入类似以下内容:
#include <psapi.h>
#include "LateLoad.h"
LATELOAD_BEGIN_CLASS(CPsapiWrapper,psapi,FALSE,TRUE)
LATELOAD_FUNC_3(FALSE,
BOOL,
WINAPI,
EnumProcesses,
__out_bcount(cb) DWORD *,
__in DWORD,
__out LPDWORD
)
LATELOAD_FUNC_4(FALSE,
BOOL,
WINAPI,
EnumProcessModules,
__in HANDLE,
__out_bcount(cb) HMODULE,
__in DWORD,
__out LPDWORD
)
...
LATELOAD_END_CLASS()
在某个头文件psapi_lateload.h中。再次注意,我们尽可能保留了原始格式,以便以后能够快速检查和更新文件。现在通过包装器对象调用DLL:
// Declare the wrapper object
CPsapiWrapper psapi;
...
// Test for presence of a symbol
if (! psapi.Is_EnumProcesses())
return;
// Call into the dll
WCHAR sName1[1024];
psapi.GetProcessImageFileNameW(GetCurrentProcess(), sName1, 1024);
...
此包装器为每个函数添加了存根,并且Is_EnumProcesses()
检查原始函数或存根是否就位。
虽然这比简单的动态加载更方便使用,但它也有一些附加条件。首先,创建psapi_lateload.h比以前的psapi_dynamic.h工作量**大得多**,因为必须删除参数名称。使用的宏取决于参数数量,必须手动选择。此外,使用psapi.dll中函数的每个文件都维护自己的包装器对象,并且由于整个函数定义都存在于(许多版本的)宏中,因此向包装器添加功能需要编写大量代码。
更重要的是,再次强调,在隐式和显式加载之间切换时必须更改代码。虽然这似乎是一个小问题,但如果,例如,存在依赖于函数名称保持不变的宏,那么它可能是一个完全的障碍。例如,psapi.h中函数的TCHAR
版本不能再使用,这会导致在任何调用带有字符串参数的函数的地方都出现#ifdef UNICODE ... #else ... #endif
部分。
dynlink包装器
当然,上面提到的关于动态加载(无论是否使用现有包装器)的所有问题都不是编写新包装器的理由。
在本文代码所花费的总时间内,我本可以轻松地包装和更新我可能很快需要动态加载的所有DLL。但编写它肯定比一遍又一遍地编写相同的动态加载代码更有趣。
与现有解决方案相比,dynlink包装器具有以下优点:
- 它无需更改为静态加载DLL而编写的现有代码。因此,它鼓励您切换到动态加载,从而允许您支持,例如,不同版本的特定SDK。
- 因此,通过更改一个
#define
就可以在静态和动态链接之间切换。这使得不可用的符号在链接时出现,并有助于使您的代码和DLL保持同步。 - 它可以在大型项目中广泛使用,而不会产生任何开销。
- 对于大多数API,只需在头文件中进行一次搜索和替换即可。然后,您维护一个原始头文件的修改版本,只需进行最少的更改,使用简单的交互式文件比较工具即可轻松更新。
- 它在运行时提供丰富的错误信息。切换到静态链接时,所有错误检查都会成功,从而允许您在切换加载方法时添加错误检查代码。
使用代码
使用包装器涉及三个步骤:
准备修改后的头文件
复制原始头文件,例如psapi.h到psapix.h,添加一个宏来定义库名称,使用DYNLINK_LIBNAME
宏定义默认库名称(通常,这应该是DLL的文件名,但可以是任何名称。它仅与函数dynlink_<DYNLINK_LIBNAME>()
返回对库对象的引用以及调用库对象的load()
方法时使用的默认文件名有关),并用DYNLINK_DECLARE
或DYNLINK_DECLARE_EX
宏包装所有函数和变量声明。这需要与使用传统方法类似的搜索和替换技巧。psapix.h的开头如下:
// DYNLINK: Outside the protection through ifndef
#undef DYNLINK_LIBNAME
#define DYNLINK_LIBNAME psapi
#include <dynlink.h>
// DYNLINK: Declarations changed by
// DYNLINK: {.*}\nWINAPI\n{[^\(:b]*}:b*{\(([^;]*\n)*.*\)}; -->
// DYNLINK_DECLARE(\1, WINAPI, \2, \3)
// DYNLINK: in Visual studio regular expressions
#ifndef _PSAPI_H_
#define _PSAPI_H_
...
DYNLINK_DECLARE(
BOOL,
WINAPI,
EnumProcesses,(
__out_bcount(cb) DWORD * lpidProcess,
__in DWORD cb,
__out LPDWORD lpcbNeeded
))
DYNLINK_DECLARE(
BOOL,
WINAPI,
EnumProcessModules, (
__in HANDLE hProcess,
__out_bcount(cb) HMODULE *lphModule,
__in DWORD cb,
__out LPDWORD lpcbNeeded
))
...
注释中提供了更改声明的正则表达式。如果您想在不同名称下多次加载库,您需要付出更多努力:DYNLINK_DECLARE
宏必须放在受#ifndef _PSAPI_H_
保护的部分之外(最好不要移动声明,以便我们能够保持原始格式和头文件结构的接近)。有关示例,请参阅psapixx.h。
链接到dynlink.lib
链接到dynlink.lib。或者,您可以#define DYNLINK_NO_AUTO
(或从dynlink.h中删除#pragma comment(lib, "dynlink.lib")
)并将dynlink.cpp添加到您的项目中。
添加加载和错误检查代码
在最简单的情况下,您调整包含语句,并在一个文件中选择要定义符号的位置。在这里,您添加:
#define DYNLINK_DEFINE
在包含修改后的头文件之前。
此外,还有几个宏可以修改包装器的行为。它们在dynlink.h中有描述,并允许自定义DLL的包装方式。
DYNLINK_USE_STUBS
是否用存根替换不可用函数。使用此选项时,需要定义默认返回值(见下文)。DYNLINK_RETVAL_[some type]
返回[some type]
的存根的默认返回值。DYNLINK_DECLARE_EX
,它允许按函数定义返回值,并且该返回值是全局的。DYNLINK_LINK_IMPLICIT
如果定义了此宏,dynlink.h
头文件将回退到隐式链接。当然,库不能再以不同的名称加载,并且前缀不再有效。尝试这样做将导致编译时错误。
DYNLINK_PREFIX
用此宏的值为所有函数和变量添加前缀,例如,为了多次加载相同的库。DYNLINK_NOCHECK
如果定义了此宏,通过dynlink调用导入的函数几乎不会产生开销。但是,这也禁用了auto_load和throw功能(见下文)。
您可以使用不同的方法和前缀链接不同的库(并且您可以使用不同的前缀多次链接相同的库)。您只需确保为每个库和前缀一致地定义隐式或显式链接,并且在适用的情况下,在包含修改后的头文件以访问库的所有文件中定义前缀。对于每个前缀,必须在仅一个文件中定义DYNLINK_DEFINE
宏。这是dynlink库如何实现加载和错误检查的结果:
当不使用DYNLINK_LINK_IMPLICIT
时,所有符号都被包装到一个派生自CDynlinkSymbol
的对象中,其名称与它包装的函数或变量完全相同。该对象可以隐式转换为正确的函数指针或变量引用类型,因此只要您确保使用存根或仅调用已加载的符号,就不需要更改代码。
同时,您当然可以调用在CDynlinkBase
和CDynlinkSymbol
中定义的成员函数,它们都非常自明:
class CDynlinkBase
{
public:
//! Get error during library / symbol load.
//! @return
//! The last system error that caused a call to this object to fail
virtual DWORD get_error() const = 0;
//! Get error during library / symbol load.
//! @param s_err
//! Error string
//! @return
//! The last system error that caused a call to this object to fail
DWORD get_error(std::wstring &s_err) const;
DWORD get_error(std::string &s_err) const;
//! True if the symbol/library was successfully loaded with no stub
//! in place.
virtual bool is_loaded() const = 0;
//! True if the symbol/library was successfully loaded, possibly with
//! stubs in place
virtual bool is_loaded_or_stub() const = 0;
...
};
//! A symbol in the dll
class CDynlinkSymbol : public CDynlinkBase
{
friend class CDynlinkLib;
public:
// Constructor. Don't use this unless you know exactly what you are doing.
// This is used by the DYNLINK_DECLARE macros
CDynlinkSymbol(CDynlinkLib &lib, LPCSTR s_name);
...
//! Get the symbol name
LPCSTR get_name()
{
return m_s_name.c_str();
}
//! Get the library from which we are loaded
CDynlinkLib * get_lib()
{
return m_p_lib;
}
...
};
直接调用这些函数时,切换回静态加载时代码将停止工作(因为显而易见,函数没有成员函数)。如果您希望代码在两种方法之间保持有效,请使用CDynlinkGetError
类来访问错误和状态信息。
// Just for convenience
#define _E(symbol) CDynlinkGetError(symbol)
...
// This assumes that psapix.h has been included and that DYNLINK_LIBNAME
// was set to psapi in this file.
dynlink_psapi().load();
...
DWORD dwErr;
std::string s_error;
if (!_E(EnumProcesses).is_loaded())
_E(EnumProcesses).get_error(s_error);
...
静态加载时,所有函数都会提示加载成功。get_name()
和get_lib()
不适用于静态加载。
加载和维护库状态的代码包含在类型为CDynlinkLib
的对象中。它通过函数 dynlink_<DYNLINK_LIBNAME>()
访问,其中<DYNLINK_LIBNAME>
是在修改后的头文件中定义的库名称(见上文)。
最简单的情况下,您可以调用其load()
方法即可,但它也允许您在选择显式链接时微调库符号的行为。
//! Interface class for explicit and implicit linking and loading of dlls
//! and contained symbols.
class CDynlinkLib : public CDynlinkBase
{
friend class CDynlinkSymbol;
public:
//! Mode of loading
enum mode
{
//! Explicit loading of function addresses
ex,
//! Explicit loading, does not fail when a function symbol is
//! not found and creates a stub instead that will be called and
//! executes the code defined in DYNLINK_RETVAL_<rettype> instead
exstub,
//! Loading is assumed to be implicit. Function returns always true
imp
};
//! Create a library interface. Use this only if you know EXACTLY what
//! you are doing. Usually including the modified header will take care
//! of construction and defines a function dynlink_<name>() to access
//! it as a singleton where <name> is the library name defined in the
//! header.
//! @param sName
//! The name of the library. (e.g. psapi for psapi.dll)
//! @param d_mode
//! The mode (see above). If this is 'imp' or 'delay' it is assumed
//! that the appropriate linker flags were set.
CDynlinkLib(LPCWSTR sName, mode d_mode = ex);
CDynlinkLib(LPCSTR sName, mode d_mode = ex);
//! Get error during library load
//! @param s_err
//! The string to write a formatted error message to
//! @return
//! The error as returned by GetLastError() during the load_library call.
virtual DWORD get_error() const;
#ifndef DYNLINK_NO_CHECK
//! If set to true accessing a variable or function not loaded
//! will cause a c++ exception to be thrown. In fact, the symbol throws a
//! pointer to itself so you could wrap your code accessing the lib into
//! try
//! {
//! code;
//! }
//! catch (CDynlinkSymbol * p)
//! {
//! retrive and display error information;
//! }
//! @param d_throw
//! When to throw the exception
//! 0: never throw exceptions (default behaviour)
//! 1: throw exceptions when the function is not loaded (loadable)
//! and no stub is available
//! 2: throw exception always, even if a stub would be available
//!
//! REMARK: this will also occur if you just check the symbol against
//! NULL. If this is enabled use the is_loaded() function instead to
//! avoid throwing an exception.
void set_throw(int d_throw)
{
m_d_throw = d_throw;
}
//! Change auto load behavior. If enabled, the library will load on first
//! access to one of its symbols.
void set_auto_load(bool b_auto_load)
{
m_b_auto_load = b_auto_load;
}
#endif
//! Load the library and all symbols. For details see above.
//! @param b_retry
//! Retry the load independently of previous results. If not TRUE
//! a loaded library and its symbols will not be touched.
//! @param s_name
//! Replace the library name given above.
//! @return
//! State of the loading, see definitions of DYNLINK_ERR ... above
int load(LPCWSTR s_name = 0, bool b_retry = false);
int load(LPCSTR s_name, bool b_retry = false);
//! Fake-load the library and use only stubs. This will not call
//! LoadLibrary() and it will not call GetProcAddress() but all error
//! handling will indicate that the library was successfully loaded.
//! This function has no effect if loading is static.
int load_stubs();
//! Get loading state.
//! @return
//! State of the loading, see definitions of DYNLINK_ERR ... above
int get_state() const
{
return m_d_loaded;
}
//! Get the loading mode of the library object. See CDynlinkLib::mode for
//! details.
//! @return
//! the mode with which the library was loaded.
mode get_mode() const
{
return m_mode;
}
//! Check whether the library was loaded successfully.
//! @return
//! true if get_state() is DYNLINK_SUCCESS
virtual bool is_loaded() const
{
return (get_state() == DYNLINK_SUCCESS);
}
//! Check whether all symbols can be called in the library without NULLPTR
//! references.
//! @return
//! true if get_state() is either DYNLINK_SUCCESS or DYNLINK_ERR_STUBS
virtual bool is_loaded_or_stub() const
{
return (get_state() == DYNLINK_SUCCESS) || (get_state() == DYNLINK_ERR_STUBS);
}
//! Unload the library. This will fail for an implicitely linked library
//! but should succeed for explicite linkage and delayed loading.
bool free();
//! Get the module handle for a successfully loaded library. NULL if the
//! library was not loaded.
//! REMARK: when implicitely linking, this may return NULL even if the
//! library was successfully loaded but if the module name does not match
HMODULE get_module() const
{
return m_h_dll;
}
//! Get the filename of the library loaded. Can fail for implicitely linked
//! libs if the library name is not correct
bool get_filename(std::wstring &s_fn) const;
//! Get the filename of the library loaded.
bool get_filename(std::string &s_fn) const;
protected:
...
};
同样,其中大部分是不言自明的。首次访问时自动加载以及访问未加载符号时抛出异常(set_auto_load()
和set_throw()
)可以在运行时启用或禁用。后者在访问未加载的符号时会抛出CDynlinkSymbol*
类型的异常,并允许您在catch子句中生成诊断输出。
DynlinkTest应用程序是演示项目的一部分,它演示了当前实现的所有选项。请参阅test_dynlink.cpp。您可以通过注释掉stdafx.h中的#define DYNLINK_LINK_IMPLICIT
来切换动态和静态加载。
演示项目还包含上面介绍的另外两种方法的代码,即使您决定dynlink包装器不适合您,它也是一个很好的起点。
使用存根
关于使用存根函数的简要说明。头文件会自动定义必要的函数,您只需确保对于DYNLINK_DECLARE
包装的函数的每个返回类型(与DYNLINK_DECLARE_EX
相反,后者允许按函数定义返回类型,但与头文件的最小更改范例有些不兼容),在定义了DYNLINK_DEFINE
的包含头文件的地方定义适当的DYNLINK_RETVAL_<type>
宏。重要的是,宏可以是任何R值,包括函数,并且 wherever 宏被使用,都会定义一个类型为CDynlinkSymbol *
的变量p_symbol
。这允许您在为特定符号名称或返回类型调用存根时生成诊断输出或更改程序状态。有关如何使用此功能的示例,请参阅测试应用程序中的main.cpp文件。
工作原理
这些类很小,实现也或多或少是直观的。虽然宏定义起初看起来很复杂,但它们也相当无害。
基本上,DYNLINK_DECLARE
分别获取函数名、返回类型、调用约定和参数列表。如果启用了显式链接,它将定义(如果未定义DYNLINK_DEFINE
,则声明)一个类型为CDynlinkFunc<T>
或CDynlinkVar<T>
的对象,其中模板参数分别是函数指针或变量的类型。这两个类都有一个转换运算符,允许它们的用法与头文件中定义的原始符号非常相似。
变量类CDynlinkVar
在其构造函数中接受一个可选的存根初始值。
//! Class to simulate a variable imported from a dll.
template<class T> class CDynlinkVar : public CDynlinkSymbol
{
public:
// Create and set the stub value to be returned
CDynlinkVar(CDynlinkLib &lib, LPCSTR s_name, const T&val) :
CDynlinkSymbol(CCDynlinkLib &lib, LPCSTR s_name)
{
m_stub_value = val;
m_p_stub = (void *) &(m_stub_value);
m_p_ptr = NULL;
}
// Create without stub
CDynlinkVar() : CDynlinkSymbol(CCDynlinkLib &lib, LPCSTR s_name)
{
m_p_stub = NULL;
m_p_ptr = NULL;
}
// Use as variable of type T
operator T&()
{
#ifndef DYNLINK_NO_THROW
check_throw();
#endif
return *(T*) m_p_ptr;
}
protected:
// The thing
T m_stub_value;
};
函数类接受一个可选的函数指针作为存根。
// A function. T is the type of the function Pointer
template<class T> class CDynlinkFunc : public CDynlinkSymbol
{
public:
// Create and set the stub value to be returned
CDynlinkFunc(CDynlinkLib &lib, LPCSTR s_name,
T stub = NULL) : CDynlinkSymbol(lib, s_name)
{
m_p_stub = (void *) stub;
m_p_ptr = NULL;
}
// Use as variable of type T
operator T()
{
#ifndef DYNLINK_NO_THROW
check_throw();
#endif
return (T) m_p_ptr;
}
};
该设计比LateLoad包装器的每符号一成员函数的实现更复杂。这主要是由于我希望允许声明分散在一个或多个头文件中,这迫使我们将每个符号定义包装到对象定义中,该定义将在构造期间将其添加到CDynlinkLib
对象中。这种方法的好处是,库对象可以,例如,在运行时遍历其符号,并且可以在不改变更易出错的宏定义的情况下,向符号基类和库对象添加功能。
历史
- 2011年12月7日:初始版本。
- 2011年3月20日:添加了自动加载,清理了设计。更新了演示应用程序。