應用程式/解決方案‎ > ‎雜記‎ > ‎專題‎ > ‎WinAPI‎ > ‎

使用dll的一些基礎見識


不是談理論, 也不是談規範, 更不談原則, 就只是見招拆招但又不想太費工夫...

引用dll中的api, 第一步就是要先知道函式名稱, 可以透過工具抓dll的函式列表出來觀察

Windows下最常見就是 WINAPI , 在C/C++裡面就是 __stdcall
Windows是MS Windows, MS的VC如無特別定義, 預設是 __cdecl

以VC來觀察, 3個一樣的函數在dll會有什麼不同
extern "C" __declspec(dllexport) __stdcall int DllApiTest1(int x)
{return x;}

extern "C" __declspec(dllexport) __cdecl int DllApiTest2(int x)
{return x;}

extern "C" __declspec(dllexport) int DllApiTest3(int x)
{return x;}


於dll裏所見的函數名稱分別為
_DllApiTest1@4
DllApiTest2
DllApiTest3

也就是只有 DllApiTest1 的樣子不一樣, 會被修飾成以底線開頭,
同是末尾銜接@加上傳遞參數的長度(每個參數需4bytes, 因為是32位元程式)

而於Borland C/C++加底線的規則剛好相反, 會是
DllApiTest1
_DllApiTest2
_DllApiTest3

如果改編譯成x64的版本, 則就不會有底線, VC/BC都一樣是
DllApiTest1
DllApiTest2
DllApiTest3

dllexport方式 原函式名稱 Borland C++ Microsoft Visual C++
(缺省) DllApiTest _DllApiTest DllApiTest
__cdecl DllApiTest _DllApiTest DllApiTest
__stdcall DllApiTest DllApiTest _DllApiTest@4
__fastcall DllApiTest @DllApiTest @DllApiTest@4
缺省時預設 __cdecl __cdecl

而既然談dll, 當然是Windows的dll, 如果 WINAPI 就是 __stdcall
那麼當然 __stdcall 最能廣為通用, 而要解決的問題就只有函式名稱不同而已~

於這篇文 使用HFOCX資訊示範AmiBroker上用ADK設計Realtime Quote
提到 ADK 使用 __cdecl , 官方都是用VC6和DevC作說明, 因為DevC命名的方式和VC相同,
而好處就是不論 x86 or x64 平台, 函式名稱都會一樣, 缺點就是其它語言(如VB)就無法使用了~
另一缺點則是, 選擇Borland C/C++來開發的話,
因為命名不一樣, 所以AmiBroker不能正確抓到進入點所以就不能用了,
這個並非真的不能用, 只要技巧編修dll內的函式名稱過後就還是能正確使用的

所以不同於AmiBroker的ADK作法, 平台產出的dll都是用 __stdcall
由於大致上還是以VC為主平台, 常可看到針對 Win32 和 x64 上的變通整合作法是這樣的,
以 DbfTCdll2.dll 應用在上面所提的ADK範例作說明

Win32平台(x86,x32)

#include "DbfTCdll2.h"    // for 使用 DbfTCdll2 連接 DTS

//視環境配合調整 DbfTCdll2.dll 的位置
#define DbfTCdllLibSource "DbfTCdll2.dll"    //x86的版
//#define DbfTCdllLibSource "DbfTCdll2_x64.dll"    //x64的版

//啟用指定函數, 使用 WWXfunc_DllFuncAdd 的方式美化程式 (產生函式指標預設為NULL)
#define WWXfunc_DllFuncAdd(fnName) pWWXfunc_##fnName fnName=NULL;

//取得指定函數DLL進入點, 使用 WWXfunc_DllImport 的方式美化程式
#define WWXfunc_DllImport(hmDll,fnName) fnName=(pWWXfunc_##fnName)(GetProcAddress(hmDll, WWXfunc_##fnName));

//啟用指定函數
WWXfunc_DllFuncAdd(fnDbfTCdll_SelectDataSet)
WWXfunc_DllFuncAdd(fnDbfTCdll_InitUser)
WWXfunc_DllFuncAdd(fnDbfTCdll_Start)
WWXfunc_DllFuncAdd(fnDbfTCdll_Stop)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetConnectionStatus)
WWXfunc_DllFuncAdd(fnDbfTCdll_CallBack_Register)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetCurrTagValue)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetPrevTagValue)
WWXfunc_DllFuncAdd(fnDbfTCdll_CreateStringIdMapping)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetStringId)
WWXfunc_DllFuncAdd(fnDbfTCdll_CreateItemQuoteMemory)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetItemQuoteMemory)
WWXfunc_DllFuncAdd(fnDbfTCdll_GetQueueCount)
//啟用指定函數(Tick-Manager)
WWXfunc_DllFuncAdd(fnDbfTCdll_STManCreateGroup)
WWXfunc_DllFuncAdd(fnDbfTCdll_STManCreateProduct)
WWXfunc_DllFuncAdd(fnDbfTCdll_STManGetTickHandle)
WWXfunc_DllFuncAdd(fnDbfTCdll_STManGetTickMemory)

HMODULE hmDbfTCdll = NULL;


而 WWXfunc_DllImport 這個 macro 會於 x64 平台上作一些調整
#define WWXfunc_DllImport(hmDll,fnName) fnName=(pWWXfunc_##fnName)(GetProcAddress(hmDll, CheckApiFunctionNameForX64(WWXfunc_##fnName)));
char * CheckApiFunctionNameForX64(char *cpName)
{
    if (*cpName == '_')
    {
        char *p = (char *)memccpy(cpName, cpName + 1, '@', strlen(cpName));
        if (p)
            *(--p) = 0;
    }
    return cpName;
}


如果想要切換平台不用更動程式, 可以這樣改寫

x86,x64平台共用程式碼

//取得指定函數DLL進入點, 使用 WWXfunc_DllImport 的方式美化程式
#ifndef WWXPx64
#define WWXfunc_DllImport(hmDll,fnName) fnName=(pWWXfunc_##fnName)(GetProcAddress(hmDll, WWXfunc_##fnName));
#else
//於VC的x64專案上,可把專案屬性裏的 預定義(Preprocessor Definitions) 加入 WWXPx64 就會改跑這段
#define WWXfunc_DllImport(hmDll,fnName) fnName=(pWWXfunc_##fnName)(GetProcAddress(hmDll, CheckApiFunctionNameForX64(WWXfunc_##fnName)));
char * CheckApiFunctionNameForX64(char *cpName)
{
    if (*cpName == '_')
    {
        char *p = (char *)memccpy(cpName, cpName + 1, '@', strlen(cpName));
        if (p)
            *(--p) = 0;
    }
    return cpName;
}
#endif   

其實 CheckApiFunctionNameForX64 就是把原本.h檔中刻好的x86版本函式名稱去掉前面的 _ 和刪除後面 @ 的延伸
也就是不管什麼平台都用同一個.h檔, 而且內容很單純, 由x86攜至x64直接使用, 不需作任何的因應或調整

所以在那ADK的示範程式中掛接dll函式是這樣寫的, 這樣就看明瞭了吧!
PLUGINAPI int Init(void)
{
    if (!hmDbfTCdll)
        hmDbfTCdll = LoadLibrary(DbfTCdllLibSource);
    if (hmDbfTCdll)
    {
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_SelectDataSet);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_InitUser);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_Start);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_Stop);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetConnectionStatus);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_CallBack_Register);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetCurrTagValue);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetPrevTagValue);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_CreateStringIdMapping);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetStringId);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_CreateItemQuoteMemory);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetItemQuoteMemory);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_GetQueueCount);

        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_STManCreateGroup);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_STManCreateProduct);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_STManGetTickHandle);
        WWXfunc_DllImport(hmDbfTCdll, fnDbfTCdll_STManGetTickMemory);

        if (fnDbfTCdll_CallBack_Register)
        {
            //CallBack 模式
            fnDbfTCdll_CallBack_Register(TagDataProcessFunction, 1);// 1 - By Tag, 每個商品的每個Tag更新都會呼叫
            fnDbfTCdll_CallBack_Register(ItemProcessFunction, 2);// 2 - By Item, 有更新的商品會呼叫 (傳入之cpVarName,cpNewValue,cpOldValue等參數為NULL)
            if (fnDbfTCdll_STManCreateGroup)
            {
                unsigned long uGroupIndex = fnDbfTCdll_STManCreateGroup("RecentInfoDataItem", sizeof(RecentInfoDataItem), NULL, 64, 256);
                unsigned long uProductIndex = fnDbfTCdll_STManCreateProduct(uGroupIndex);
                vpQuoteIndexHandle = fnDbfTCdll_STManGetTickHandle(uGroupIndex, uProductIndex);
                if (!vpQuoteIndexHandle)
                    MessageBox(0, "Dll file "DbfTCdllLibSource" STMan Err! Memory crash!!", PLUGIN_NAME" Init Error!", MB_OK);
            }
            else
                MessageBox(0, "Dll file "DbfTCdllLibSource" not support STMan", PLUGIN_NAME" Init Error!", MB_OK);
        }
        else
        {
            FreeLibrary(hmDbfTCdll);
            hmDbfTCdll = NULL;
            MessageBox(0, "Dll file "DbfTCdllLibSource" load faild!!", PLUGIN_NAME" Init Error!", MB_OK);
        }
    }
    else
    {
        MessageBox(0,
            "Dll file "DbfTCdllLibSource" not found!!\n"
            "請將檔案置於AmiBroker的工作環境資料夾內"
            , PLUGIN_NAME" Init Error!"
            , MB_OK
        );
    }

    if (hmDbfTCdll)
    {
        fnDbfTCdll_InitUser(PLUGIN_NAME);    // 直接用PLUGIN_NAME當User,同一資訊源相同User只能一個在線; 可改成用參數提供設定
        fnDbfTCdll_SelectDataSet(caDB_list);
        if (fnDbfTCdll_Start(caDTS_host))
            g_nStatus = STATUS_WAIT;
    }

    return 1; // default implementation does nothing

};     


於這篇使用HFOCX資訊示範AmiBroker上用ADK設計Realtime Quote文中所介紹的示範專案,
包含 VC6 , VC10(x32,x64) , DevC , DevC5(x32,x64) , BCB5 , BCB6 都是只需安裝好最基本的IDE,
甚至用免安裝的版本也可以, 就能直接成功編譯出dll, 如預料只有BCB的版本不能被AmiBroker正確使用,
其它版本包含 x32 , x64 全都能正常運行, 如此程式修改同一份就能重新使用各種IDE釋出新版本的dll,
在切換x32或x86的平台專案過程, 也是完全不用更動程式碼, 是個非常理想完美的範例,

雖然此範例專案有用到本平台上的 DbfTCdll2.dll , 但因為是動態掛接的模式,
即使手邊沒有這個dll仍能使用此專案作為雛型來開發AmiBroker的ADK應用,
往往很多情況是專案環境弄不好, 或是不知怎麼從零開始,
現在只要拿這份程式碼作基礎, 依樣畫葫蘆很容易就能上手了~
加上AmiBroker可以免費下載使用, 軟體也不大, 是個不錯的練習項目

沒有 DbfTCdll2.dll 的狀況, 也能知道ADK是否正確工作

關掉於ADK中所設計彈出的訊息窗後, 右下角的狀態也會反映在ADK裏設計好的狀態回應

可供收藏的壓縮檔內容架構如下
有來問說最新的VS版本是否也可以用, 答案當然是可以, 完全不會有任何奇奇怪怪的問題
於VS2008的VC9可以直接開VC6的專案轉換後就能直接使用(原本的.dsw 轉過以後則改用.sln),
而VS2015的VC14和VS2017的VC14.1都是直接開VC10的專案轉換後就直接可以用了(轉換其實只有更新VC版本號而已)

由於不管是什麼VC版本還是任何其他IDE產品, 要使用本程式碼都很簡單, 只需產生一個dll的空專案,
然後作一個動作把程式碼include就可以用了, 因此壓縮檔內不需要放太多具體的範例, 看看就懂了

真的想用BCB來開發ADK也是可以的~ 壓縮檔內有提供修正程式


如有興趣, 可以透過街口轉帳任意金額, 備註留信箱說明索取ADKdllSample, 待作業完成就會寄送至所留信箱

註解