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