DLL动态链接库分包引用及延迟加载
1.为什么要分包
最近项目中有应用到比较多的项目dll和第三方dll,之前是都放在exe的平级目录下的,当dll多到一定程度时,会非常的乱。有一些库已经没有用到了,但由于第三方库直接没有归类,也不知道那些库是必须引用的,因此也没有清理。
时间久了,整个目录下会显得杂乱无章,命名规范互相不同。
由于项目用到了C#和C++两种语言,因此dll还存在不同的类型。
虽然DLL乱不影响整体软件使用,但对于强迫症来说还是相当难受的。
为了方便管理,研究出了一种dll分包依赖的管理技术,解决依赖的dll存在杂乱的问题。
2.如何分包
1.确定编译位置
首先将项目中各模块的编译位置指定到需要分包的结构,例如本项目中的分包结构,就是在libs/x64下的分包结构,将core暴露在最外层。
2.添加引用库目录
指定dll搜索目录,即运行目录下的所有子目录,并使用windows中的API将这些子目录都加到dll搜索路径列表中。
将使用到kernel32.dll中的SetDllDirectory 、AddDllDirectory 和SetDefaultDllDirectories
SetDllDirectory 用于指定设置系统搜索路径 AddDllDirectory 用于提交用户定义的系统所有路径 关键在于SetDefaultDllDirectories ,用于指定需要所有路径的类型,这里填
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR
| LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
| LOAD_LIBRARY_SEARCH_SYSTEM32
| LOAD_LIBRARY_SEARCH_USER_DIRS);
我们将这些指定的代码封装到公共类中,也就是在core.dll里面,这也是为什么要将core.dll暴露在最外层。
在程序运行时,还没有调用这些设置依赖路径的函数之前,是只能找到根目录下的dll的,除非把设置依赖的代码写在main函数入口,否则必须要将core.dll放到根目录下。
3.延迟加载dll
在此之前,需要介绍一下dll的加载方式
1. dll隐式加载
一个dll的使用需要依赖头文件和Lib文件,直接以普通的接口的方式来调用dll的函数,这种方式就是隐式加载。dll的隐式加载默认会在 mian 之前进行dll的加载工作。通过 depends 工具可以查看到dll的依赖. 隐式加载的dll的路径搜索一般是的顺序是: 1.可执行的文件目录 2.系统目录(c:/windows/system32) 3.Path环境变量的目录 4.LoadLibrary 函数参数中指定的 dll文件所在目录
2. dll显示加载
对于很多的插件化加载方式使用的是显示加载的方式,即dll的加载是通过 loadLibrary 实现,函数调用通过 GetProcAddress 来实现
项目采用Qt搭建界面,因此原本的设计中必不可免的会在启动项目中引用的Qt依赖库。
如果将Qt库放到分包的位置(不在根目录下),程序启动不起来,找不到需要依赖的Qt库。(隐式加载要求在启动前就先加载依赖项)
因此,需要使用延迟加载。
● 延迟加载的优点
- 一般的大型平台依赖的dll可能会有几百个之多,如果都在程序启动的时候去加载这些dll,将导致启动程序非常慢。使用延迟加载可以在第一次调用dll函数的时候去加载这些库,这样可以大大提高程序的启动效率,最合理的利用内存。
- 隐式加载的DLL一般情况下只能在当前exe所在的目录下(其他目录会污染系统环境),所有的dll在同一个目录是非常不利于管理大型程序的。
● 延迟加载技术
- 延迟载入是针对隐式链接DLL的
- 一个导出了字段(如全局变量)的DLL是无法延迟载入的
- 系统的DLL一般都是无法延迟载入的,如Kernel32.dll
- main 函数或者dllmain中的函数的第一句不要直接使用延迟载入函数(因为没有设置延迟载入的dll路径,也有可能进入死循环)
使用了延迟加载,则程序只有在运行到需要依赖的dll时,才会去加载dll,这样就可以在程序还没运行到依赖之前,把依赖路径添加进搜索列表中,从而达到启动页的依赖项也能够放到分包位置的目的。
对于Qt而言,还有另一个问题。就是Qt的Qt5Core.dll是有静态类的,因此无法延迟加载。
对此,我们只需要新建一个项目,项目中依赖Qt库。而启动程序中,延迟加载这个项目,就可以达到同一目的。
如何使用延迟加载
1.在VS项目中使用延迟加载
- 建立常规的C++库和可执行程序
- 添加延迟加载链接 如VS中为了延迟加载Dll,还需要在解决方案的该项目“属性”->“配置属性”->“链接器”->“输入”->“延迟加载的Dll”,需要注意的是扩展名是 dll 不是 lib
2.在CMake工程中使用延迟加载
1.在需要被延迟加载的dll中,引用delayimp.lib依赖
## 延迟加载宏
MACRO(ADD_DELAYLOAD_FLAGS flagsVar)
SET(dlls "${ARGN}")
FOREACH(dll ${dlls})
SET(${flagsVar} "${${flagsVar}} /DELAYLOAD:${dll}.dll")
ENDFOREACH()
ENDMACRO()
## 延迟加载方案使用,需要注意的是,使用延迟加载需要依赖静态库delayimp.lib
## 即在被延迟加载的库的CMakeLists文件中,target_link_libraries(${lib_name} delayimp.lib)
2.在需要延迟加载的项目的CMakeList.txt中
##然后在需要延迟加载的入口库的CMakeList文件中声明
ADD_DELAYLOAD_FLAGS(CMAKE_EXE_LINKER_FLAGS sample) ##sample需要延迟加载的dll名,不带后缀
3.示例 gdal库延迟加载fileGDBAPI.dll(在某些环境下,明确知道不需要使用某个库的时候可以使用延迟加载)
## 在最外层的makefile.vc中找到类似的下面这行,可以支持多个延迟加载的dll
$(GDAL_DLL): $(LIB_DEPENDS)
link /nologo /dll $(OGR_INCLUDE) $(BASE_INCLUDE) $(LIBOBJ) \
$(EXTERNAL_LIBS) gcore\Version.res \
/out:$(GDAL_DLL) /implib:gdal_i.lib $(LINKER_FLAGS) /delayload:FileGDBAPI.dll /delayload:expat.dll
4.C#项目中使用分包依赖
C#项目中对dll的依赖也可以使用分包的结构,主要分几种情况
1.纯C#项目
C#中同样可以调用Windows API,达到添加搜索路径的目的
public static class DllLoader
{
#region C Interface
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool AddDllDirectory(string lpPathName);
[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetDefaultDllDirectories(int flags);
#endregion
#region private Interface
static private void _addDirectory(string lpPathName)
{
var root = new DirectoryInfo(lpPathName);
if (root.Exists)
{
bool isOk = AddDllDirectory(lpPathName);
var subDirs = root.GetDirectories();
foreach (var dir in subDirs)
{
_addDirectory(dir.FullName);
}
}
}
static private bool _setDefaultDllDirectories(int flags)
{
return SetDefaultDllDirectories(flags);
}
private static System.Reflection.Assembly _CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var dll_path = args.Name.Split(',')[0];
string workPath = AppDomain.CurrentDomain.BaseDirectory;
var paths = Directory.GetFiles(workPath, dll_path, SearchOption.AllDirectories);
if (paths.Length == 0)
{
return null;
}
AddDllDirectory(paths[0]);
return System.Reflection.Assembly.LoadFrom(paths[0]);
}
#endregion
static public void AddDirectory(string lpPathName)
{
AppDomain.CurrentDomain.AssemblyResolve -= _CurrentDomain_AssemblyResolve;
AppDomain.CurrentDomain.AssemblyResolve += _CurrentDomain_AssemblyResolve;
bool isOk = _setDefaultDllDirectories(7680);
if (!Directory.Exists(lpPathName))
{
lpPathName = Directory.GetCurrentDirectory();
}
_addDirectory(lpPathName);
}
}
2.C++与C#混合项目
C#中写和直接调用C++接口,即调用上文中提到的SGLibrary.AddDllSearchPath ,效果一样。 并且需要把core_csharp.dll(c++转换成C#的dll),放到根目录下,程序才能启动。
或者在程序启动项的App.config下,加上搜索目录"swig",能把exe运行目录下swig目录包含进去
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="swig"/>
</assemblyBinding>
</runtime>
</configuration>
如果子目录的深度不高,子目录不多,也可以将所有分包的结构都写到privatePath中,这样不需要依赖任何代码,都能实现分包。
|