1 前言
windows 平台使用动态加载库来扩展功能。
动态库的使用包含静态和动态两种方式。静态不在今天讨论范围内。动态加载,通过是通过LoadLibary 或者 LoadLibaryEx,以及其他API获取模板基址。然后获取地址,转化成函数指针,然后调用。
这里面有很多工作量都是重复的。比如加载函数,获取地址,定义函数指针。因为可以抽象出来,做一个类对象,来做这部分内容。
2 目标
实现一个类,用来加载动态库。使用动态库的函数像使用类的成员函数一样简单。
要实现这个目标,最困难的是,动态类成员函数。在这个动态获取成员函数的实现里面,获取动态库函数的地址,然后传参调用。
3 动态成员函数
实现动态成员函数,名字可变,参数可变,返回值不确定,调用方式不确定。几乎就没有确定的东西。我唯一想到的就是用宏来实现。比如:
1 #define LIB_FUNCTION(FunctionName, retType, CallMethod, args)\ 2 retType CallMethod FunctionName##args\ 3 {\ 4 typedef retType (CallMethod * PFN_#FunctionName)##args;\ 5 PFN_##FunctionName pfn##FunctionName = (PFN_##FunctionName)GetProcAddress(hMoudle, #FunctionName);\ 6 return pfn##FunctionName(???);\ 7 } 8 9 LIB_FUNCTION(test,int,_stdcall,(int,int)) 10 LIB_FUNCTION(test, int, _stdcall, (int a, int b))
很快你将遇到无法在这个新定义的函数中调用其他函数。原因是英文声明函数需要类型,和调用函数需要值。二者需求不一样。怎么解决呢。
一个丑一点的实现方式。代码如下
1 #define LIB_FUNCTION(FunctionName, retType, CallMethod, argsType, argsValue)\ 2 retType CallMethod FunctionName##argsType\ 3 {\ 4 typedef retType (CallMethod * PFN_#FunctionName)##argsType;\ 5 PFN_##FunctionName pfn##FunctionName = (PFN_##FunctionName)GetProcAddress(hMoudle, #FunctionName);\ 6 return pfn##FunctionName##argsValue;\ 7 } 8 9 LIB_FUNCTION(test, int, _stdcall, (int a, int b),(a,b))
就是在宏中添加一个参数argsValue,使用这个来传值。宏(a,b)必须是argsType定义的。不然编译会报错。
虽然有一点瑕疵,但确实可以这个实现。使用的时候确实有一点点不方便。有没有更好的办法呢。能不能只传(int a, int b)呢。
这里需要一点点宏知识。宏是无类型的。编译期间会替换。使用","分隔符。每一部分都是一个token。如果传入(int a, int b),那么int a,是一个token,int b是一个token。
想要在一个token中提取类型和值显然是不可能。
我搜索相关问题的答案。其中有一个网页挺匹配我问题的。链接如下
在类型和值中间插入一个分割符,传入(int,a,int,b),再定义一些基础宏,就可以递归解析,获取类型和值。这个思路我觉得没有问题。stackoverflow我没法验证。
原因是缺少一些宏定义,也无法调试,因为你知道的,宏晦涩难懂。但是思路肯定没错。即便是这样,传入(int,a,int,b),我仍然觉得有瑕疵。有没有更好的办法呢。
我确信肯定有办法实现。因为之前在江民,就有一个类,就是用来加载动态库,获取地址后调用的。只需要在类中用宏来声明调用函数的原型就行了,当初简单看了下,记得用宏来实现的。一看到宏,我就放弃了。
因为我觉得宏是丑陋的,甚至是邪恶的。后来看到现在的项目中,也是通过loadlibary,getprocaddr获取地址,然后调用。结合以前,我也写过很多加载动态库,获取地址。
觉得有必要封装了一个类。干这些杂活。说远了。总之就是我确信有办法实现。
如果只传入类型的话,比如(int,int),在宏里面判断出来有几个类型,然后根据类型,定义变量,传递给调用函数。这样的想法不错。
怎么判断宏中可变参的个数。网上有这样的定义,如下:
1 #define NARGS(...) NARGS_(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 2 #define NARGS_(_10, _9, _8, _7, _6, _5, _4, _3, _2, _1, N, ...) N
使用宏NARGS(int,int) ,宏展开后确实位2.遗憾的是,最多能判断10个参数。另外一个严重的问题是当可变参为0时候,就是 NARGS(),宏展开后值是1,是错误的。
即使__VA_ARGS__前加两个#,也不行。因为##__VA_ARGS__,智能吃掉前面的分隔符,不能吃掉后面的分隔符。不过还有其他的方式,如下:
#define FL_INTERNAL_ARG_COUNT_PRIVATE(\ _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, \ _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, \ _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, \ _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, \ _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, \ _60, _61, _62, _63, _64, N, ...) N #define FL_ARG_COUNT(...) FL_INTERNAL_ARG_COUNT_PRIVATE(0, ##__VA_ARGS__,\ 64, 63, 62, 61, 60, \ 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, \ 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, \ 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, \ 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, \ 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, \ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
使用 FL_ARG_COUNT(int,int) 以及 FL_ARG_COUNT()验证计算可变参的个数是对的。FL_ARG_COUNT 来自这里 https://blog.csdn.net/10km/article/details/80760533
接下来就是定义变量了。 定义变量也就一个列表比如 (_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10),在可变参类型列表取出来一个类型,然后再变量列表取出来一个变量,组成类似(int,_0)对。
使用如下的宏定义。
1 #define EMIT_DECL(T, N) T N 2 #define EMIT_CALL(T, N) N
使用 EMIT_DECL 宏可以将(int, _0) 展开成int _0而EMIT_CALL 用来获取值,如_0。使用了以后可以将值传递给调用函数。实现完美传参。
EMIT_DECL 的定义来自网站 https://stackoverflow.com/questions/24643183/how-to-do-a-runtime-subclassing-system。 比较完整的代码如下:
1 #define NARGS_(\ 2 _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, \ 3 _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, \ 4 _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, \ 5 _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, \ 6 _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, \ 7 _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, \ 8 _60, _61, _62, _63, _64, N, ...) N 9 10 #define NARGS(...) NARGS_(0, ##__VA_ARGS__,\ 11 64, 63, 62, 61, 60, \ 12 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, \ 13 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, \ 14 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, \ 15 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, \ 16 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, \ 17 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 18 19 #define CAT(A, B) CAT_(A, B) 20 #define CAT_(A, B) A##B 21 #define ID(...) __VA_ARGS__ 22 #define APPEND(L, E) ID(L),E 23 #define FIRST(A, ...) A 24 #define REST(A, ...) __VA_ARGS__ 25 26 #define ZIP(F, L1, L2) CAT(ZIP_, ID(NARGS L1))(F, L1, L2) 27 28 #define ZIP_4(F, L1, L2) F(ID(FIRST L1), ID(FIRST L2)), ZIP_3(F, (ID(REST L1)), (ID(REST L2))) 29 #define ZIP_3(F, L1, L2) F(ID(FIRST L1), ID(FIRST L2)), ZIP_2(F, (ID(REST L1)), (ID(REST L2))) 30 #define ZIP_2(F, L1, L2) F(ID(FIRST L1), ID(FIRST L2)), ZIP_1(F, (ID(REST L1)), (ID(REST L2))) 31 #define ZIP_1(F, L1, L2) F(ID(FIRST L1), ID(FIRST L2)) 32 #define ZIP_0(F, L1, L2) 33 34 35 #define GENSYMS (_0, _1, _2, _3, _4, _5, _6, _7, _8, _9) 36 37 #define EMIT_DECL(T, N) T N 38 #define EMIT_CALL(T, N) N 39 40 #define runtime_subclass_method(rtype,method,args) \ 41 rtype method(ZIP(EMIT_DECL, args, GENSYMS)) { \ 42 return NARGS##args;\ 43 } 44 45 46 class CTest 47 { 48 public: 49 CTest() {}; 50 ~CTest() {}; 51 52 public: 53 bool load() { return false; }; 54 55 runtime_subclass_method(int, doSomething, (int, int)) 56 runtime_subclass_method(int, dovoid,()) 57 runtime_subclass_method(int, domore, (int,int,int,int)) 58 59 //private:60 //HMODULE m_hMoudle; 61 };
对了,上面的宏仍然有点问题,最大的问题就是传入的参数做多就是4个。多与四个的,就没有对应的宏处理。
可以想办法使用参数N,一级一级处理。多于四个参数的。思路确实不错。挺佩服的。
PS:我用vs 2019编译器。runtime_subclass_method(int, dovoid,()) 会显示红色波浪线,不知道怎么回事。编译没有问题。如果采用vs默认设置,那么你很有可能得不到预期的结果。
原因是vs 展开宏的方式不是标准的。网上说msvc展开宏会有很多问题。有一个解决办法是在vs工程属性页,左边C/C++,预处理器,右边选项 “使用标准符合性预处理器”, 在下拉列表中选择,“是 (/Zc:preprocessor)”。
如果你找不到 “使用标准符合性预处理器” 设置项,那么你确认下,在vs工程属性页,常规选项卡中,平台工作集中是否是“Visual Studio 2019 (v142)”。我只有vs2019编译器。只有选择 Visual Studio 2019 (v142),才有相关设置。
平台集是 "Visual Studio 2017 - Windows XP (v141_xp)" 或者 "Visual Studio 2015 (v140)"都不行。不会只有vs2019支持这个选项把(苦笑)。
暂时先这样,挖个坑,尽快出一个可用的加载动态库的类。