C++动态库加载类的实现思路


  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中提取类型和值显然是不可能。

  我搜索相关问题的答案。其中有一个网页挺匹配我问题的。链接如下

  https://stackoverflow.com/questions/24641375/is-there-an-easier-way-to-do-a-macro-to-define-a-function-with-variable-amount-o

在类型和值中间插入一个分割符,传入(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支持这个选项把(苦笑)。

  暂时先这样,挖个坑,尽快出一个可用的加载动态库的类。 

  


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM