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支持這個選項把(苦笑)。
暫時先這樣,挖個坑,盡快出一個可用的加載動態庫的類。