Wednesday, July 12, 2006

BCB - VC compatibility : 探訪動態連結函式庫

探訪動態連結函式庫


探 訪 動 態 連 結 函 式 庫 (Dynamic Linking Libraries,DLLs)


打Windows 從 娘 胎 出 生 後 , 動 態 連 結 函 式 庫(Dynamic Linking Libraries,DLLs) 宛 如 這 個 新 生 兒 的 血 液 一 般 的 重 要 ,DLLs 一 直 扮 演 著Windows 的 基 石 這 個 角 色 。 到 了Win32 雄 霸 一 方 的世 紀 ,DLLs 的 威 風 更 是 不 減 當 年 , 幾 乎 所 有 的Win32 API 都 以隱 身 在DLLs 中 的 形 式 存 在 ,Windows 血 液 裡 流 的 盡 乎 都 是DLLs 。 在Windows 下DLL 通 常 是 以 『 副 檔 名 為DLL 的 檔 案 』 存 在 ,DLL 內 可 以 包 含 提 供 外 部 呼 叫 的 函 式 、 資 源 (Resource ) 以 及各 種 的 變 數 , 當DLLs 被 應 用 程 式 載 入 時 這 些DLLs 內 含 的 資料 都 會 變 為 應 用 程 式 所 屬 行 程 的 一 部 份 。 我 們 可 以 把DLL 當 成 一 個 函 式 庫 (Library ) , 但 因 為 還 要 能 夠 被 動 態 載 入, 這 個 函 式 庫 自 然 比 傳 統 的 函 式 庫 複 雜 些 。

DLL 一 二 說

DLLs 的 出 現 提 供 了 程 式 設 計 師 一 個 將 程 式 模 組 化 的 方法 , 別 於C++ 類 別 的 建 構 時 期 模 組 化 ,DLL 乃 是 執 行 時 期 模組 化 , 因 此 程 式 設 計 師 可 以 在 建 構 程 式 時 將 所 需 要 用 到的 函 式 分 門 別 類 的 製 造 成DLLs 的 形 式 。 但 為 什 麼 我 們 要 使用DLLs 呢 ?DLLs 充 其 量 不 就 你 把 程 式 碼 、 資 源 等 獨 立 到 另一 個 檔 案 裡 頭 去 , 到 底 有 什 麼 好 處 值 得 我 們 大 費 周 章 地把 部 份 程 式 寫 成DLLs 的 形 式 呢 ? 使 用DLL 的 好 處 大 致 可 以 如下 歸 類 :

1 、 有 效 率 的 重 複 使 用 程 式 碼

當 程 式 設 計 師 所 撰 寫 的 程 式 碼 一 多 起 來 , 必 然 地 會 發現 有 很 多 程 式 碼 是 在 做 相 同 的 事 情 , 通 常 當 程 式 設 計 師遇 到 這 些 重 複 的 程 式 碼 , 最 平 常 不 過 的 方 法 就 是 把 這 些重 複 的 程 式 碼 大 則 獨 立 成 『 函 式 』 (Function ) 小 則 獨 立成 『 巨 集 』 (Marco ) 。 但 是 , 當 這 些 函 式 不 單 單 只 在 單一 應 用 程 式 內 會 用 到 , 而 是 在 撰 寫 許 多 應 用 程 式 時 都 得用 到 時 , 這 些 常 用 的 函 式 通 常 就 會 被 製 作 成 函 式 庫 來 使用 , 例 如C 語 言 的Runtime Library (RTL ) ; 但 編 譯 器 在 編 譯 應用 程 式 遇 到 函 式 庫 時 , 會 把 這 些 隱 身 在 函 式 庫 裡 頭 的 函式 實 體 內 容 如 同 我 們 在 程 式 裡 頭 撰 寫 這 些 函 式 的 原 始 碼般 地 加 進 應 用 程 式 的 執 行 檔 中 , 也 就 是 說 當 你 所 用 的 函式 庫 越 多 時 , 你 的 執 行 檔 也 就 相 對 的 會 越 來 越 龐 大 , 這個 做 法 也 就 是 我 們 所 謂 的 靜 態 連 結 (Static Linking ) 。 因此 為 了 避 免 應 用 程 式 的 過 分 龐 大 , 有 人 提 出 了 動 態 連 結(Dynamic Linking ) 的 做 法 , 所 謂 動 態 連 結 就 是 提 供 了 一 個做 法 讓 我 們 不 需 要 把 應 用 程 式 的 執 行 檔 變 得 如 此 龐 大 ,但 一 樣 可 以 享 用 這 些 使 用 頻 率 高 的 函 式 。 也 就 是 說 這 些函 式 會 在 程 式 執 行 時 才 被 載 入 , 而 不 是 直 接 編 譯 在 執 行檔 中 。 這 樣 一 來 可 以 讓 我 們 更 有 效 率 使 用 這 些 函 式 。 但相 對 的 , 當 你 所 撰 寫 的 程 式 得 交 給 他 人 使 用 時 , 你 除 了得 把 你 所 編 譯 好 的EXE 檔 交 給 他 之 外 , 還 得 一 併 把 編 譯 好的DLLs 檔 交 給 他 , 否 則 執 行 起 來 一 定 會 產 生 不 可 預 期 的 錯誤 。

2 、 區 分 程 式 碼

依 據 先 前 的 第 一 點 , 若 有 朝 一 日 發 現 這 些 被 包 裝 在DLLs 之 中 的 函 式 的 實 作 方 法 有 點 錯 誤 或 是 發 現 有 更 好 的 做 法時 , 需 要 更 動 僅 只 有 部 份 的DLLs 原 始 碼 , 重 新 將 修 改 過 的DLLs 給 編 譯 後 就 可 以 達 成 更 新 程 式 的 目 的 , 至 於 應 用 程 式 端連 動 都 不 需 要 動 一 下 ; 當 然 了 , 前 提 是 這 些 個 修 改 過 的函 式 名 稱 與 傳 入 的 參 數 型 別 宣 告 都 不 能 夠 更 動 。 根 據 這些 個 特 性 , 咱 們 可 以 把 整 個 應 用 程 式 中 的 函 式 依 照 功 能或 目 的 分 類 , 並 將 這 些 分 類 好 的 函 式 組 合 成 許 多 個DLLs 模組 , 將 執 行 檔 給 分 割 城 數 個 小 檔 案 , 讓 這 些DLLs 模 組 分 工合 作 來 完 成 應 用 程 式 所 要 達 到 的 目 的 。 這 樣 一 來 , 對 於應 用 程 式 的 維 護 以 及 更 新 就 不 需 要 大 費 周 章 地 從 頭 再 編譯 一 次 了 , 僅 需 把 要 有 修 改 到 的DLLs 從 新 編 譯 就 可 以 達 成程 式 的 更 新 。

3 、 節 省 記 憶 體 的 使 用 量

先 前 提 到 :DLLs 的 載 入 是 在 應 用 程 式 執 行 時 才 被 載 入 ,甚 至 還 可 以 是 可 以 在 應 用 程 式 所 需 用 到 函 式 時 才 被 載 入。 此 外DLLs 還 有 一 個 很 重 要 的 特 性 : 若 不 同 的 應 用 程 式 但需 要 相 同 的DLL 中 的 函 式 時 ,DLL 僅 在 第 一 個 使 用 到DLL 的 應用 程 式 執 行 時 載 入 , 只 要 這 個 應 用 程 式 尚 未 結 束 而 其 他的 應 用 程 式 又 正 好 需 要 使 用 到 同 一DLL 中 的 函 式 時 ,DLL 不需 要 再 重 新 被 載 入 到 記 憶 體 中 就 可 以 供 第 一 個 應 用 程 式以 外 需 要 用 到DLL 的 應 用 程 式 使 用 , 一 直 到 沒 有 任 何 應 用程 式 使 用 這 個DLL 時 ,DLL 才 會 跟 著 最 後 一 個 使 用DLL 的 應 用程 式 一 起 從 記 憶 體 裡 頭 消 失 , 因 此 使 用DLLs 來 包 裝 常 用 的函 式 是 個 不 錯 能 夠 節 省 記 憶 體 與 系 統 資 源 的 做 法 。

4 、 將 程 式 推 向 國 際 舞 台

DLL 在 設 計 時 , 就 已 經 被 設 計 不 只 是 能 夠 放 入 函 式 而 已, 還 能 夠 被 放 入 許 多 的 資 源 (Resource ) , 如 : 可 以 放 入選 單 資 料 、 字 串 資 料 、 圖 形 資 料 等 等 … 。 也 因 此DLL 很 常被 拿 來 作 為 應 用 程 式 邁 向 國 際 舞 台 的 一 個 墊 腳 石 。 你 可以 在 應 用 程 式 被 執 行 時 檢 查 執 行 應 用 程 式 的 作 業 系 統 語言 版 本 , 之 後 把 當 地 語 言 版 本 的Resource DLL 給 載 入 , 讓 所有 的 文 字 及 畫 面 都 達 到 當 地 語 言 化 的 目 的 。 而 這 個 功 能在C++Builder 3 當 中 經 由Borland C++Builder 部 門 工 程 師 的 努 力 ,已 經 幫 我 們 把 這 些 煩 人 的 步 驟 給 簡 化 了 許 多 , 我 們 僅 須按 下 選 單 上 的New 並 選 擇Resource DLL Wizard , 並 將 將 需 要 更 改的 文 字 給 更 改 成 不 同 的 語 言 , 並 不 須 在 執 行 時 檢 查 執 行平 台 的 語 言 為 何 。 其 餘 煩 瑣 的 工 作 都 已 經 被Borland 工 程 師給 完 成 了 。 我 們 最 後 只 需 要 重 新 編 譯 這 個Resource DLL 並 附在 應 用 程 式 中 就 可 以 完 成 一 個 國 際 化 的 應 用 程 式 了 。

既 然DLLs 在Windows 上 頭 是 那 麼 的 重 要 且 使 用DLLs 還 有 那 麼多 的 好 處 , 當 然 值 得 咱 們 來 好 好 的 了 解 一 下 。 先 來 討 論DLLs 的 基 本 架 構 。

Import 還 是Export ?

我 們 已 經 知 道 當DLL 被 應 用 程 式 載 入 時 ,DLL 內 所 含 有 的資 料 都 會 成 為 應 用 程 式 所 屬 行 程 中 的 一 部 份 , 這 到 底 怎麼 辦 到 的 ? 其 實 在DLL 被 應 用 程 式 載 入 時 ,DLL 會 被 先 設 定一 個 基 底 位 址 (Base Address ) , 若 這 個 基 底 位 址 並 沒 有 和應 用 程 式 中 的 其 他 資 源 互 相 衝 突 , 則 這 個DLL 檔 會 被 映 射到 載 入 端 行 程 內 的 相 同 位 址 上 讓 載 入 端 應 用 。 那DLL 中 到底 有 多 少 資 訊 可 以 被 載 入 呢 ? 先 看 看 圖 一 :

圖 一 是 使 用C++Builder 內 附 上 的tdump.exe 工 具 列 出 一 個DLL 檔內 所 有 的Section ,tdump 是 個 非 常 好 用 的 工 具 , 之 後 使 用 次會 不 少 , 先 說 明 用 法 , 用 法 很 簡 單 :

tdump inputfile outputfile

這 樣 就 可 以 把inputfile 裡 頭 的 資 料 給 格 式 化 輸 出 到outputfile 裡 頭 , 當 然 了 , 你 的inputfile 得 是tdump 認 得 的 檔 案 :DOS 下的 執 行 檔 、PE 格 式 執 行 檔 ( 註1 ) 、.OBJ 檔 、.LIB 檔 , 其 餘的 檔 案 若 為 文 字 格 式 就 直 接 輸 出 , 而Binary 格 式 的 檔 案 就以HEX Dump 格 式 輸 出 。 此 外 , 在Microsoft Visual C++ 裡 頭 的dumpbin.exe 也 提 供 相 同 功 能 來 分 析 這 些 檔 案 。

圖 一 中 的 這 些Section 表 示 著 :

Section
包 含
意 義
.text 應 用 程 式 或DLL 的 程 式 碼 這 個section 包 含 了 一 般 性 的 程 式 碼 ,就 是 先 前 提 到 的 除 了 自 己 所 撰 寫 的 程 式 碼 外 還 有Runtime Library 的 程 式 碼 。
.data 具 有 初 始 值 的 資 料 這 個section 存 放 了 在 編 譯 時 期 就 已 經具 有 初 始 值 的 資 料 ; 包 括 了 全 域 變 數 (global variable ) 、靜 態 變 數 (static variable ) 以 及 ”Hello World ” 這 一 類 字 串等 等 … 。
.tls 執 行 緒 內 部 儲 存 空 間 thread local storage
.idata 輸 入 名 稱 表 這 個section 包 含 了 有 『 從 其 他DLLs 中輸 入 過 來 的 函 式 與 資 料 』 的 相 關 資 訊 。
.edata 輸 出 名 稱 表 這 個section 恰 好 與.idate 相 反 , 是 存 放了 由 此EXE 或DLL 輸 出 給 外 頭 使 用 的 函 式 與 資 料 的 相 關 資 訊。
.rsrc 資 源 若 你 使 用 過Microsoft Visual C++ 或 是Borland Resoure Workshop 來 觀 察 過EXE 或DLL , 你 所 看 到 的 那 些resource date 就 是 儲 存 在 這 個section 裡 。 也 就 是 編 譯 器 將 應 用 程 式 所 需要 用 到 的resource date 都 整 理 好 一 起 放 到 這 個section 裡 頭 。
.reloc 修 正 表 資 訊 這 個section 裡 頭 含 有 一 個base relocationsg 是 一 個 調 整 值 , 先 前 說 過 當 …. 會 , 但 若 無 法 載 入 到 預 設的 位 址 , 就 會 依 據 這 個 調 整 值 來 作 調 整 。

除 了 了 解 這 些section 之 外 , 你 還 必 須 知 道 的 另 一 個 觀 念是 所 謂 的 相 對 虛 擬 位 址 (Relative Virtual Address ,RVA) 。PE 格式 執 行 檔 中 有 許 多 資 料 的 位 址 都 是 以RVA 表 示 。 簡 單 的 來說RVA 是 某 一 項 資 料 從 檔 案 被 映 射 進 來 的 起 點 算 起 的 偏 移值 (offset ) 。 舉 個 例 子 , 我 們 說Windows 載 入 器 把 一 個PE 格式 執 行 檔 檔 映 射 到 虛 擬 位 址 空 間0x400000 處 , 如 果 在 此 執行 檔 中 有 一 個 函 式 的 函 式 指 標 起 始 於0x40C000 , 那 麼 這 個函 式 指 標 的RVA 就 是0x C000 :

虛 擬 位 址 (0x40C000 ) ─ 基 底 位 址 (0x400000 ) = RVA (0xC000 )

只 要 把 相 對 虛 擬 位 址 加 上 基 底 位 址 , 相 對 虛 擬 位 址 就可 以 被 轉 換 為 一 個 有 用 的 指 標 。 『 基 底 位 址 』(Base Address) 也 是 另 一 個 重 要 名 詞 , 通 常 基 底 位 址 是 用 來 描 述 被 映 射到 記 憶 體 中 的EXE 或DLL 的 起 始 位 址 。

另 外 圖 一 中 『Key to section flags 』 是 這 些section 的 屬 性 旗標 種 類 , 如 : 唯 讀 、 共 享 或 可 寫 入 等 等 … 每 個section 的 屬相 可 以 從 『Object table 』 最 後 一 欄 的 『Flag 』 中 看 出 。

了 解 這 些 基 本 知 識 後 , 再 回 頭 詳 細 看 一 看 在 圖 一 中 的輸 出 輸 入section , 我 們 已 經 知 道 , 製 作DLL 的 主 要 目 的 是 製造 一 個 模 組 化 的 函 式 或 資 料 供 其 他 的 程 式 應 用 , 而 這 種提 供 給 其 他EXE 和DLL 使 用 的 方 式 就 稱 為 輸 出 (export ) , 反之 若 取 用 其 他 的EXE 或DLL 中 的 函 式 , 就 稱 為 輸 入 。 在DLL 中, 你 可 以 輸 出 任 何 想 要 輸 出 的 資 料 , 如 函 式 、 類 別 (class ) 或 是 資 源 等 等 … , 我 們 把 重 點 放 在 輸 出 輸 入 函 式 的 部份 , 我 們 先 來 觀 察 一 般 的 輸 出 函 式 :

一 樣 可 以 使 用 先 前 提 到 的tdump 來 觀 察DLL 中 的 輸 出 表 格, 由 圖 二 中 可 以 看 到 輸 出 函 式 表 格 包 含 了Ordinal 、RVA 以 及Name 三 個 欄 位 表 示 。Name 就 是 輸 出 的 函 式 名 稱 而RVA ─ 相 對 虛 擬位 址 在 前 頭 已 經 介 紹 過 了 , 至 於Ordinal 則 是 輸 出 表 格 中 輸出 函 式 的 序 號 。 這 些 輸 出 函 式 透 過 這 個 表 格 上 的 函 式 名稱 與 函 式 序 號 讓 外 界 認 得 。 當 載 入 端 最 初 在 載 入DLL 時 並不 知 道DLL 內 的 輸 出 函 式 的 正 確 位 址 只 知 道 函 式 的 序 號 與名 稱 , 但 在 動 態 連 結 的 過 程 中 會 建 立 起 一 個 連 連 看 表 格將 載 入 端 的 函 式 呼 叫 與 被 載 入 端 內 的 函 式 正 確 位 址 給 連結 起 來 。

那 我 們 要 怎 樣 才 能 夠 達 成 輸 出 的 動 作 , 其 實 很 簡 單 ,你 只 要 在 你 的 應 用 程 式 中 需 要 輸 出 的 函 式 前 頭 加 上 :__declspec(dllexport) 即 可 , 如 :

__declspec(dllexport) void Function(void);

這 樣 一 來 就 會 把Function(void) 這 個 函 式 給 放 到 輸 出 表 格上 頭 了 。

先 前 還 有 提 到 還 可 以 將C++ 類 別 透 過DLL 來 輸 出 , 可 以 的一 樣 是 加 上__declspec(dllexport) , 如 :

class __declspec(dllexport) __stdcall MyClass : public TObject{ … …};

當 我 們 在 看 輸 出 表 格 時 , 會 發 現 函 式 的 輸 出 表 格 前 頭好 像 有 個 如 圖 三 一 般 的 資 料 :

這 就 是 函 式 的 輸 入 表 格 , 這 裡 列 出 來 的 是USER32.DLL 裡 頭被 我 們 使 用 到 的 函 式 。 一 樣 的 怎 樣 建 立 輸 入 表 格 呢 ? 一般 的Win32 API 都 已 經 在 其 所 屬 的Header File 裡 頭 定 義 好 了 ,我 們 只 需 加 上#include 就 可 以 安 心 使 用 了 , 而對 於 自 行 打 造 的DLL 中 的 輸 出 函 式 我 們 在 載 用 此DLL 的 載 入端 就 得 在 函 式 的 宣 告 前 面 加 上__declspec(dllimport) , 如 :

__declspec(dllimport) void Function(void);

目 的 是 告 訴 編 譯 器Function(void) 這 個 函 式 是 由 外 部 輸 入的 。

Name Mangling

嗯 ! 經 過 了 先 前 的 說 明 , 讀 者 們 應 該 知 道 若 要 在DLLs 中將 函 式 輸 出 , 僅 需 要 在 函 式 的 宣 告 中 加 上__declspec(dllexport) 即 可 , 若 要 載 入 別 的DLLs 中 的 函 式 則 須 在 函 式 的 宣 告 中 加上__declspec(dllimport) , 但 這 僅 止 於C 編 譯 器 , 在C++ 編 譯 器 裡頭 是 行 不 通 的 , 怎 麼 說 呢 ? 就 拿 一 個 多 載 (overloading )的 例 子 來 說 , 如 果 函 式 的 名 稱 都 相 同 ( 當 然 所 傳 的 參 數型 別 不 同 ) , 編 譯 器 應 該 會 如 何 處 理 ? 到 底 那 個 才 是 我們 真 正 使 用 到 的 函 式 呢 ? 其 實 在 編 譯 器 做 編 譯 動 作 時 ,對 這 些 同 名 的 函 式 都 動 了 點 手 腳 讓 同 名 的 函 式 偷 偷 地 變成 不 同 名 稱 , 以 下 面 同 名 的 三 個 函 式 為 例 :

int Func(int X);

int Func(float X);

void Func(double *d);

使 用C++Builder 3.0 所 編 譯 出 來 的 函 式 名 稱 為 : ( 註2 )

@Func$qf

@Func$qi

@Func$qpd

而 使 用Visual C++ 6.0 所 編 譯 出 來 的 函 式 名 稱 為 : ( 註3 )

?Func@@YAHH@Z

?Func@@YAHM@Z

?Func@@YAXPAN@Z

編 譯 器 這 個 偷 偷 修 改 函 式 名 稱 的 行 為 稱 為 『name mangling 』 , 但 除 了 函 式 名 稱 被 改 變 外 你 是 否 發 現 還 現 另 一 件 很嚴 重 的 事 情 , 就 是 同 的 編 譯 器 竟 然 有 著 不 同 的name mangling 做 法 , 這 表 示 著 若 咱 們 若 使 用Borland C++Builder 編 譯 器 來 開發 應 用 程 式 時 將 無 法 使 用 一 個 經 由Microsoft Visual C++ 所 編譯 器 完 成 的 函 式 庫 。 此 外 ,Naming Mangling 的 作 用 不 止 於 多載 的 函 式 上 ,C++ 程 式 中 所 有 的global 函 式 以 及class 中 所 有的 成 員 (members ) 都 會 被name mangling 這 個 動 作 給 整 型 一 下。 那 這 樣 不 就 沒 戲 唱 了 ! 若 要 在C++Builder 下 使 用Visual C++ 所 編 譯 的DLLs 豈 不 都 得 擁 有DLLs 的 原 始 碼 才 能 囉 ? 其 實 不然 , 有 方 法 可 抑 制name mangling 的 作 用 , 就 是 在 函 式 的 宣 告錢 加 上extern “C ” 這 個 修 飾 詞 , 強 制 將 函 式 以C 語 言 的 行台 重 現 , 而 非 以C++ 語 言 的 形 態 出 現 。 但 是 要 注 意 , 多 載函 式 可 不 能 加 上extern ”C ” 這 個 修 飾 詞 , 因 為 這 個 會 造 成一 堆 名 稱 相 同 的 函 式 , 若 你 硬 是 要 使 用extern “C ” 在 多 載含 上 , 編 譯 器 一 定 會 送 你 一 個ERROR ( 如 下 ) 做 獎 品 。

in C++Builder :

[C++Error]Project1.cpp(13): Only one of s set of overloaded functions can be “C ”.

in Visual C++

error C2733 : Second C linkage of overloaded function ‘Func ’ not allowed

因 此 若 拿 原 先 多 載 的 例 子 給 加 上extern “C ” :

extern “C ” int Func(int X);

則 會 被 編 譯 器 給 編 譯 成 :

in Visual C++ :

_Func

in C++Builder :

_Func

似 乎 兩 個 編 譯 器 已 經 達 成 了 一 致 的 輸 出 。 嗯 ! 這 樣 一來 就 可 以 把DLLs 互 相 使 用 了 , 不 不 不 ! 還 沒 有 那 麼 簡 單 ,還 有 一 個 重 要 的 議 題 『Call Conventions 』 , 不 過 這 個 問 題 在此 先 不 提 , 放 到 後 頭 在 提 , 先 談 談 怎 樣 使 用C++Builder 來 建立DLLs 吧 !

 

建 立DLL

使 用C++Builder 來 建 立DLL 並 不 是 什 麼 難 事 , 只 需 要 按 下 幾下 滑 鼠 的 左 鍵 即 可 。 在C++Builder 下 建 立DLLs 大 致 分 成 兩 種方 法 , 先 按 選 單 上 的File|New 後 會 出 現New Items 對 話 盒 (Dialog ) ( 圖 四 ) :

  1. 選 擇New Items 對 話 盒 中 的Console Wizard 選 項 按 下OK 後 再 選 擇DLL 即 可 。 ( 圖 五 )
  2. 選 擇New Items 對 話 盒 中 的DLL 那 個 選 項 按 下OK 即 可 。
方 法 一 是 建 立 一 個 標 準 的DLL Project , 不 允 許 使 用 任 何VCL 類 別 , 而 方 法 二 所 打 造 出 來 的DLL 則 是 可 建 立 包 含 了VCL 類別 的DLL ( 當 然 你 也 可 以 不 使 用VCL 類 別 ) , 而 這 兩 個 功 能在Visual C++ 中 差 可 比 擬 的 是Win32 Dynamic-Link Library 與MFC AppWizard(dll) 。

按 完OK 或Finish 後 你 會 看 到DLL Project 與 些 許 程 式 碼 的 產 生, 這 段 由C++Builder 自 動 產 生 的 程 式 碼 中 分 兩 大 部 分 , 第 一部 份 是 個 很 長 一 串 的 註 解 , 最 後 就 是 所 謂 的DLL 進 入 點 。先 來 了 解 這 一 長 串 的 註 解 , 由 方 法 一 與 方 法 二 製 造 出 來的DLL Project 註 解 有 點 不 相 同 , 不 過 內 容 大 致 上 差 不 多 ,內 容 如 是 說 : 如 果 我 們 的DLL 內 使 用 到 了 字 串 物 件 如 :AnsiString , 或 是 在 輸 出 函 式 的 參 數 或 回 傳 值 使 用 到 長 字 串 的 話 ,就 必 須 加 入MEMMGR.LIB 這 個 函 式 庫 。 另 外 , 若 我 們 在 另 一 個模 組 ( 如DLL ) 中 使 用 了 例 如new 或GetMem 等 方 法 來 配 置 記 憶體 , 而 在 不 同 的 模 組 ( 如EXE 應 用 程 式 ) 中 使 用 了 這 塊 記憶 體 或 呼 叫FreeMem 等 方 法 來 釋 放 記 憶 體 , 則MEMMGR.LIB 也 是必 須 被 加 入 的 。 此 外 還 有 一 個 值 得 注 意 的 , 就 是MEMMGR.LIB 必 須 加 在 所 有 要 用 到 函 式 庫 的 最 前 頭 , 以 便 在 其 他 函 式庫 之 前 優 先 載 入 並 接 手 相 關 的 記 憶 體 維 護 。 同 時 要 記 住的 是 若 你 使 用 了MEMMGR.LIB 這 個 函 式 庫 , 那 麼 當 你 移 交DLL 或是 應 用 程 式 時 , 你 必 須 連 同BORLNDMM.DLL 一 併 移 交 給 使 用 者。 不 過 在 這 段 聲 明 的 倒 數 第 二 段 中 有 提 到 , 若 要 避 免 額外 的 檔 案 付 給 使 用 者 ( 越 多 的 檔 案 對 使 用 這 來 說 是 一 種負 擔 ) , 你 可 以 將 有 關 字 串 的 資 料 改 由char * 或 是shortstring 來 傳 送 , 這 樣 可 以 不 動 用 到BORLNDMM.DLL 與MEMMGR.LIB 來 作 記 憶體 的 配 置 。 另 外 , 聲 明 的 最 後 一 段 中 有 提 到 , 若 你 在 Project\Options 裡 頭 的Link 一 頁 勾 選 了Use Dynamic RTL 一 項 時 , 就 不 須 額 外 手動 將MEMMGR.LIB 給 加 到Project 裡 頭 了 , 因 為C++Builder 會 自 動 幫你 做 這 個 動 作 。

緊 接 著 咱 們 來 看 一 下DLL 的 進 入 點

DLL 的 進 入 點 :DLLMain

以 下 是 由 方 法 二 生 產 出 來 的DLL 原 始 碼 :

  1. //---------------------------------------------------------------------------
  2. #include
  3. #pragma hdrstop
  4. //---------------------------------------------------------------------------
  5. int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
  6. {
  7. return 1;
  8. }
  9. //---------------------------------------------------------------------------
在Win32 平 台 的 程 式 設 計 中 , 明 言 規 定DLL 的 進 入 點 函 式 定義 為DllMain , 可 是 我 們 怎 麼 看 到 的 是DllEntryPoint 呢 ? 這 是Borland 對 於 標 準 的DLL 所 動 的 一 點 點 手 腳 , 當 然 你 若 執 意 把DllEntryPoint 給 改 成DllMain 也 是 可 以 的 , 但 是 你 要 注 意 , 當 你 更 動DllEntryPoint 時 ,C++Builder 工 具 列 上 頭 的Run 會 被Disable 喔 ! 這 是 就 是 擺明 了 不 給 你 編 譯 , 即 使 你 使 用Project\Build All 也 會 送 給 你錯 誤 訊 息 , 不 過 你 可 以 在DllMain 函 式 之 後 加 上#define WinMain 就 可 以 編 譯 了 , 不 過 筆 者 並 不 建 議 在C++Builder 下 這 麼 用 。

早 在 討 論 輸 入 輸 出 表 時 就 已 經 知 道 若 不 考 慮 到 各 個 編譯 器 間DLL 的 互 相 引 用 , 我 們 大 可 把 輸 出 函 式 這 麼 寫 :

__declspec(dllexport) int MyFunc (void);

若 要 輸 出 整 個 類 別 則 可 以 這 樣 寫 :

class __declspec(dllexport) MyClass : public TObject{...};

但 是 注 意 , 這 若 這 樣 寫 僅 可 以 在 自 己 寫 作DLL 的 編 譯 器中 來 使 用 這 個DLL 了 , 並 沒 辦 法 達 到 其 他 編 譯 器 也 可 以 使用 的 目 的 , 大 大 的 抹 殺 了 軟 體 元 件 的 構 想 , 更 何 況DLL 是採 用 模 組 化 的 設 計 。 當 然 了 ! 類 別 的 輸 出 當 然 不 在 考 慮範 圍 內 , 但 若 對 函 式 的 輸 出 咱 們 還 是 乖 一 點 , 加 上extern “C ” 來 遏 止name mangling 對 我 們 函 式 名 稱 所 動 的 手 腳 :

extern “C ” __declspec(dllexport) int MyFunc (void);

有 了 這 些 基 礎 知 識 後 , 咱 們 來 來 真 正 撰 寫 一 個 有 用 的DLL 試 試 看 吧 ! 那 要 做 些 什 麼 呢 ? 咱 們 就 做 個 簡 單 的 訊 息 視窗 即 可 , 怎 麼 做 , 其 實 說 穿 了 就 是 把Windows API MessageBox 給稍 微 包 裝 一 下 , 不 過 還 是 用MessageBox 這 個 函 式 來 實 作 內 容, 讀 者 們 可 能 覺 得 , 這 麼 無 聊 還 在 包 裝 一 次MessageBox 函 式幹 嘛 ? 這 個 嘛 ! 不 過 做 個 測 試 嘛 !

先 使 用Consol Wizard 來 產 生DLL Project , 並 將 此Project 儲 存 為ShowMsg.bpr , 在DLL 的 進 入 點DllEntryPoint 之 後 加 上 以 下 的 程 式 碼 :

  1. extern “C ” __declspec(dllexport) int ShowMsg(char *pText,HWND hWnd)
  2. {
  3. return MessageBox(hWnd,pText,"Information",MB_OK);
  4. }
嗯 ! 這 樣 一 來 咱 們 只 要 呼 叫ShowMsg() 函 式 , 並 將 要 秀 出 來的 文 字 與 視 窗 代 碼 當 作 參 數 來 傳 即 可 。 緊 接 著 只 需 按 下Ctrl+F9 就 開 始 編 譯 了 並 產 生 出ShowMsg.dll 了 。

使 用DLLs 中 的 函 式 ─DLLs 的 載 入

知 道 了 怎 樣 建 立 起 一 個DLL 後 , 接 著 就 是 了 解 如 何 使 用DLL 裡 頭 所 提 供 的 函 式 的 時 候 了 , 在 使 用 這 些 函 式 之 前 還 必須 做 個 更 重 要 的 動 作 , 就 是 將DLL 給 載 入 。 載 入DLLs 的 方 法大 致 上 可 以 分 成 兩 個 :

Implicit Linking 與Explicit Linking 。

Implicit Linking

Implicitly Link ( 隱 式 聯 結 ) 又 稱 靜 態 載 入 , 所 謂 靜 態 載入 是 指 程 式 在 聯 結 時 期 即 與DLLs 所 對 應 的import libraries 做靜 態 鏈 結 , 於 是 可 執 行 檔 中 便 對 所 有 的DLL 函 式 都 有 一 份重 定 位 表 格(relocation table) 和 待 修 正 記 錄(fixup record) 。 當程 式 被Windows 載 入 器 載 入 記 憶 體 中 , 載 入 器 會 自 動 修 正 所有 的fixup records , 而 這 個fixup records 就 是 記 錄 由DLL 中 所 有輸 出 資 源 的 正 確 位 址 , 也 就 是 先 前 提 到 的RVA 加 上DLL 被 載入 的 基 底 位 址 , 經 過 這 樣 的 程 序 動 態 聯 結 便 順 利 產 生 。也 就 是 說 , 程 式 開 始 執 行 時 , 會 用 靜 態 載 入 方 式 所 使 用到 的DLLs 都 載 入 到 行 程 的 記 憶 體 裡 。 先 來 看 看 靜 態 載 入 放是 的 優 點 :

1 、 靜 態 載 入 方 式 所 使 用 到 的 這 個DLL 會 在 應 用 程 式 執行 時 載 入 , 然 後 就 可 以 呼 叫 出 所 有 由DLL 中 匯 出 的 函 式 ,就 好 像 是 包 含 在 程 式 中 一 般 。

2 、 動 作 較 為 簡 單 , 載 入 的 方 法 由 編 譯 器 負 責 處 理 ,咱 們 不 須 動 腦 筋 。
而 缺 點 是 :

1 、 當 這 個 程 式 靜 態 載 入 方 式 所 使 用 到 的 這 個DLL 不 存在 時 , 這 個 程 式 在 開 始 時 就 出 現 無 法 找 到DLL 的 訊 息 而 導致 應 用 程 式 無 執 行 。

2 、 編 譯 時 需 要 加 入 額 外 的import library 。

3 、 若 是 要 載 入 的DLLs 一 多 , 載 入 應 用 程 式 的 速 度 會 便慢 。

4 、 若 遇 到 不 同 品 牌 的C++ 編 譯 器 時 , 靜 態 載 入 可 就 沒有 這 麼 簡 單 處 理 了 , 因 為 當 函 式 經 過Calling Conventions 的 處理 後 , 若 要 使 用 其 他 品 牌 編 譯 器 所 致 造 出 的DLL 須 得 大 動干 戈 才 行 。
Implicit Linking 範 例 :

以 先 前 建 立 的ShowMsg.DLL 為 例 子 , 我 們 已 知 這 個DLL 僅 輸出 一 個 函 式 :ShowMsg , 且 知 道 這 個 函 式 的 原 始 定 義 :

extern “C ” __declspec(dllexport) int ShowMsg(char *pText,HWND hWnd);

因 此 , 若 我 們 要 載 入 這 個 函 式 則 必 須 在 應 用 程 式 中 加入 此 輸 入 函 式 的 宣 告 :

extern “C ” __declspec(dllimport) int ShowMsg(char *pText,HWND hWnd);

此 外 , 還 要 加 上 這 個DLL 的import library File , 要 產 生import library 的 方 法 有 兩 個 :

1 、Project\Options 的Linker 中 的Geretate import library 勾 選 , 在正 常 情 況 下 , 預 設 值 是 勾 選 起 來 的 。

2 、 若 不 小 心 把lib 檔 案 給 刪 除 掉 了 , 也 可 以 利 用implib.exe 這 個C++Builder 所 附 上 的 工 具 來 產 生lib 檔 ,implib 是 文 字 模 式下 的 程 式 , 因 此 必 須 到 文 字 模 式 下 使 用 , 以 我 們 現 在 的例 子 來 說 , 使 用 方 式 為 :implib ShowMsg.lib ShowMsg.dll , 這 樣就 會 產 生ShowMsg.lib 檔 了 。
緊 接 著 就 是 加 入 這 個lib 檔 到 咱 們 的Project 裡 頭 , 可 以使 用 【Project\Add to Project … 】 來 加 入lib 檔 。 如 此 一 來 就 可以 在 應 用 程 式 的 任 何 地 方 使 用ShowMsg 函 式 了 。

不 過 在 此 我 們 發 現 一 個 小 問 題 , 若 每 次 要 匯 入DLL 裡 頭的 函 式 , 還 必 須 把 函 式 的 原 始 定 義 給 抄 過 來 ( 雖 然 說 複製 ─ 貼 上 這 個 動 作 很 簡 單 ) , 但 有 沒 有 更 好 的 辦 法 呢 ?有 的 , 咱 們 可 以 在DLL 原 始 碼 的Header File 裡 動 點 手 腳 , 讓要 使 用DLL 的 應 用 程 式 只 需include 這 個Header File 就 可 以 了 ,怎 麼 做 呢 ? 就 是 使 用 前 置 處 理 符 號 , 若 是 使 用Borland C++ 或 是Borland C++Builder 來 編 譯DLL 都 必 須 加 上#define __DLL__ 這 個宣 告 , 但 是 我 們 從C++Builder 所 幫 我 建 立 的Project 裡 面 看 不到 這 個 , 原 因 是C++Builder 已 經 在Make file 裡 幫 我 們 加 上-WD 這個 編 譯 參 數 來 達 成#define __DLL__ 所 要 的 目 的 了 。 所 以 我 們可 以 把ShowMsg.h 給 改 成 :

程 式 列 表 一 、ShowMsg.h

  1. //---------------------------------------------------------------------------
  2. #ifndef ShowMsgH
  3. #define ShowMsgH
  4. //---------------------------------------------------------------------------
  5. #ifdef __DLL__
  6. #define DLLAPI extern "C" __declspec(dllexport)
  7. #else
  8. #define DLLAPI extern "C" __declspec(dllimport)
  9. #endif
  10. DLLAPI int ShowMsg(char *pText,HWND hWnd);
  11. #endif
程 式 列 表 二 、ShowMsg.cpp
  1. #include
  2. #pragma hdrstop
  3. #include
  4. USEFILE("ShowMsg.h");
  5. //---------------------------------------------------------------------------
  6. #pragma argsused
  7. int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
  8. {
  9. return 1;
  10. }
  11. //---------------------------------------------------------------------------
  12. int ShowMsg(char *pText,HWND hWnd)
  13. {
  14. return MessageBox(hWnd,pText,"Information",MB_OK);
  15. }
這 樣 一 來 , 只 需 要 在 應 用 程 式 裡 加 入 這 個Header File 並 將ShowMsg.lib 檔 給 加 入 這 個Project 裡 頭 , 就 大 功 告 成 了

Explicit Linking

而 所 謂Explicitly link ( 顯 式 聯 結 ) 又 稱 動 態 載 入 , 若 是使 用 動 態 載 入 就 是 需 要 時 才 載 入DLL , 然 後 在 使 用 過 後 即釋 放DLL , 嗯 ! 似 乎 是 很 不 錯 的 選 擇 , 這 種 方 法 的 優 點 有:

1 、DLL 只 要 需 要 時 才 會 載 入 到 記 憶 體 中 , 可 以 更 有 效的 使 用 記 憶 體 。

2 、 應 用 程 式 載 入 的 速 度 較 使 用 隱 式 鏈 結 時 快 , 因 為當 程 式 開 始 載 入 時 並 不 需 要 把DLL 給 載 入 到 行 程 中 。

3 、 編 譯 時 不 須 額 外 的import library 檔 。

4 、 讓 我 們 可 以 更 清 楚DLL 的 載 入 流 程 。
但 不 光 只 有 優 點 也 是 點 缺 點 的 , 缺 點 就 是 必 須 寫 多 一點 程 式 碼 。 首 先 , 必 須 使 用LoadLibrary 這 個Windows API 來 手 動載 入DLL , 並 使 用GetProcessAddress 來 取 得 所 要 使 用 的 函 式 的函 式 指 標 , 最 後 不 需 要 用 到 此DLL 時 使 用FreeLibrary 將DLL 釋放 。 所 以 , 在 學 會 動 態 載 入DLL 時 , 必 須 先 知 道 函 式 指 標的 用 法 。

Explicit Linking 範 例 :

咱 們 就 拿 一 個 常 見 的DLL ─ 控 制TWAIN32 界 面 的DLL : 『Eztw32.dll 』 來 作 動 態 載 入 的 示 範 :

eztw32.dll 裡 頭 有 四 十 多 個 輸 出 函 式 可 以 使 用 , 不 過 只有 以 下 這 四 個 函 式 是 我 們 所 要 用 到 的 :

1 、void __stdcall TWAIN_SelectImageSource(HWND hwnd);

功 能 : 用 來 選 擇 所 要 使 用 的TWAIN 介 面 裝 置

2 、int __stdcall TWAIN_AcquireToClipboard(HWND hwnd, unsigned int pixmask);

功 能 : 經 由TWAIN 介 面 將 資 料 置 放 到 剪 貼 簿 中

3 、int __stdcall TWAIN_LoadSourceManager(void);

功 能 : 呼 叫TWAIN 介 面 裝 置 程 式

4 、int __stdcall TWAIN_UnloadSourceManager(void);

功 能 : 關 閉TWAIN 介 面 裝 置 程 式

再 度 使 用tdump 來 觀 察eztw32.dll 裡 頭 的 輸 出 函 式 表 :

Exports from EZTW32.dll

50 exported name(s), 50 export addresse(s). Ordinal base is 1.

Ordinal RVA Name

------- -------- ----

0000 00001000 DllMain

...

0003 000012c0 TWAIN_AcquireToClipboard

...

0029 00001650 TWAIN_LoadSourceManager

...

0038 000010a0 TWAIN_SelectImageSource

...

0046 00001980 TWAIN_UnloadSourceManager

...

先 確 認 函 式 的 名 稱 以 及 函 式 的 序 號 , 等 一 下 會 用 到 。

接 著 就 是 開 始 將DLL 載 入 了 ,Win32 API 有 兩 個 函 式 提 供 了將DLL 載 入 的 功 能 , 分 別 是LoadLibrary 與LoadLibraryEx 。 通 常 都使 用LoadLibrary , 先 看 看LoadLibrary 的 原 始 定 義 :

HINSTANCE LoadLibrary(

LPCTSTR lpLibFileName // address of filename of executable module

);

嗯 ! 只 需 要 傳 入 檔 案 名 稱 即 可 , 若 以 我 們 所 要 使 用 的Eztw32.dll 為 例 :

HINSTANCE hDLL;

hDll = LoadLibrary( “Eztw32.dll ”);

若 我 們 沒 有 指 定 副 檔 名 , 則 自 動 會 以 『.DLL 』 為 副 檔 名, 或 許 會 感 到 納 悶 , 那 我 還 沒 有 指 定DLL 的 路 徑 啊 ! 正 確的 做 法 應 該 是 要 指 定 路 徑 的 , 但 是 天 曉 得 使 用 者 會 把DLL 給 放 到 哪 裡 去 呢 ? 所 以 使 用LoadLibrary 這 個 函 式 時 , 若 參數 中 沒 有 指 明 路 徑 , 系 統 會 依 特 定 的 次 序 來 找 尋DLL 的 存在 與 否 , 若 不 存 在 則LoadLibrary 函 式 則 會 回 傳NULL , 以 下 就是 搜 尋 次 序 :

1 、 被 執 行 的 應 用 程 式 所 存 在 的 路 徑 。

2 、 目 前 的 目 錄 。

3 、Windows 系 統 目 錄 , 對Windows 95/98 說 是Windows\System , 而Windows NT 則 是Winnt\System32 。 目 錄 名 稱 可 以 使 用GetSystemDirectory 這個API 來 取 得 。

4 、Windows 目 錄 。 目 錄 名 稱 可 以 使 用GetWindowsDirectory 這 個API 來 取 得 。

5 、 最 後 由 設 定 的PATH 環 境 變 數 來 尋 找 。
這 也 就 是 為 什 麼 我 們 的Windows\System 目 錄 下 有 這 麼 多DLL 的 原 因 之 一 了 。

把DLL 載 入 記 憶 體 後 最 重 要 的 工 作 就 是 把 函 式 指 標 指 向函 式 在 記 憶 體 中 正 確 的 位 址 , 要 做 到 這 個 動 作 得 透 過GetProcAddress 這 個API 來 幫 忙 :

FARPROC GetProcAddress(

HMODULE hModule, // handle to DLL module

LPCSTR lpProcName // name of function

);

GetProcAddres 函 式 的 第 一 個 參 數 是 經 由LoadLibrary 所 取 得 的DLL 的Handle , 而 第 二 個 參 數 是 函 式 的 名 稱 或 是 函 式 的 輸 出 序號 經 由 函 式 名 稱 取 得 函 式 的 指 標 , 以eztw32.dll 中 的TWAIN_SelectImageSource 函 式 為 例 , 應 由 函 式 名 稱 取 得 函 式 的 位 址 的 方 法 為 : :

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);

若 經 由 函 式 輸 出 序 號 取 得 函 式 的 位 址 則 為 :

GetProcAddress(hDLL, MAKEINTRESOURCE (39));

在 此 要 注 意 序 號 的 起 始 值 是1 不 是0 , 經 由 tdump 所 列 出來 的Ordinal 是 由 起 始 值 開 始 的 位 移 植 , 而Ordinal Base 為1 ,因 此TWAIN_SelectImageSource 的 序 號 是39 而 不 是 38 。

接 著 在 此 先 複 習 一 下 函 式 指 標 的 使 用 方 式 , 一 樣TWAIN_SelectImageSource 函 式 為 例 ,TWAIN_SelectImageSource 函 式 的 原 始 定 義 為 :

void __stdcall TWAIN_SelectImageSource(HWND hwnd);

那 就 可 以 用 :

void (__stdcall *TWAIN_SelectImageSource)(HWND hwnd);

來 宣 告TWAIN_SelectImageSource 為 一 個 函 式 指 標 , 之 後 再 用:

TWAIN_SelectImageSource = (void (__stdcall *)(HWND hwnd))

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);
來 取 得 函 式 的 位 址 , 之 後 就 在 程 式 中 若 要 使 用TWAIN_SelectImageSource 這 個 函 式 就 可 以 像 是 靜 態 載 入 般 使 用 了 。 但 這 樣 似 乎 得打 蠻 多 字 的 , 偷 懶 的 我 通 常 都 使 用 另 一 個 方 法 , 就 是 使用typedef 來 自 訂 型 別 。 方 法 如 下 :

typedef void (__stdcall *_TWAIN_SelectImageSource)(HWND hwnd);

_TWA IN_SelectImageSource TWAIN_SelectImageSource;

TWAIN_SelectImageSource = (_TWAIN_SelectImageSource)

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);

先 使 用typedef 把_TWAIN_SelectImageSource 給 定 義 成 一 個 特 殊 的型 別 , 之 後 就 可 以 直 接 引 用 , 的 確 是 可 以 少 打 點 字 。

最 後 當DLL 裡 頭 的 函 式 不 再 需 要 使 用 時 , 咱 們 就 得 使 用FreeLibrary 將DLL 從 記 憶 體 裡 頭 卸 下 來 :

BOOL FreeLibrary(

HMODULE hLibModule // handle to loaded library module

);

使 用 方 法 很 簡 單 只 需 將LoadLibrary 所 傳 回 來 的DLL Handle 當參 數 傳 給FreeLibrary 傳 入 即 可 。

懂 得 這 些 動 態 載 入DLL 的 流 程 後 , 就 可 以 實 際 動 手 來 做做 看 。 筆 者 發 現TWAIN32 這 些 功 能 實 在 很 適 合 包 裝 成 一 個 物件 , 當 物 件 誕 生 時 , 立 即 自 動 去LoadLibrary 並 將 函 式 指 標的 位 址 給 連 結 起 來 , 當 這 個 物 件 被 摧 毀 時 , 就 自 動 去FreeLibrary , 嗯 ! 似 乎 不 錯 , 不 過 詳 細 的 做 法 就 不 多 做 解 釋 了 , 相信 讀 者 看 了 下 面 的 程 式 列 表 應 該 就 懂 了 。

程 式 列 表 三 、CTWAIN.h :

  1. //---------------------------------------------------------------------------
  2. #ifndef CTWAINH
  3. #define CTWAINH
  4. #include
  5. //---------------------------------------------------------------------------
  6. // 用typedef 自 訂 函 式 指 標 的 型 別
  7. typedef int (__stdcall *_TWAIN_AcquireToClipboard)
  8. (HWND hwnd, unsigned int pixmask);
  9. typedef int (__stdcall *_TWAIN_LoadSourceManager)
  10. (void);
  11. typedef void (__stdcall *_TWAIN_SelectImageSource)
  12. (HWND hwnd);
  13. typedef int (__stdcall *_TWAIN_UnloadSourceManager)
  14. (void);
  15. class CTWAIN
  16. {
  17. public:
  18. __fastcall CTWAIN(void);
  19. __fastcall ~CTWAIN(void);
  20. void __fastcall SelectImageSource(HWND hWnd);// 選 擇TWAIN32 設 備
  21. void __fastcall Acqure(HWND hWnd);// 經 由TWAIN32 設 備 取 得 資 料
  22. Graphics::TPicture *Picture;
  23. protected:
  24. private:
  25. // 宣 告 函 式 指 標
  26. _TWAIN_AcquireToClipboard TWAIN_AcquireToClipboard;
  27. _TWAIN_LoadSourceManager TWAIN_LoadSourceManager;
  28. _TWAIN_SelectImageSource TWAIN_SelectImageSource;
  29. _TWAIN_UnloadSourceManager TWAIN_UnloadSourceManager;
  30. HINSTANCE hDLL;
  31. };
  32. //---------------------------------------------------------------------------
  33. extern bool __stdcall CheckTWAINDLL();// 檢 驗DLL 檔 案 是 否 存 在 的函 式
  34. #endif
程 式 列 表 四 、CTWAIN.cpp :
  1. //---------------------------------------------------------------------------
  2. #include
  3. #pragma hdrstop
  4. #include "CTWAIN.h"
  5. //---------------------------------------------------------------------------
  6. #pragma package(smart_init)
  7. __fastcall CTWAIN::CTWAIN()
  8. {
  9. hDLL = ::LoadLibrary("Eztw32.dll");// 載 入DLL 到 行 程 的 記 憶 體 內
  10. // 取 得 所 需 函 式 的 函 式 指 標
  11. TWAIN_AcquireToClipboard = (_TWAIN_AcquireToClipboard)
  12. ::GetProcAddress(hDLL,"TWAIN_AcquireToClipboard");
  13. TWAIN_LoadSourceManager = (_TWAIN_LoadSourceManager)
  14. ::GetProcAddress(hDLL,"TWAIN_LoadSourceManager");
  15. TWAIN_SelectImageSource = (_TWAIN_SelectImageSource)
  16. ::GetProcAddress(hDLL,"TWAIN_SelectImageSource");
  17. TWAIN_UnloadSourceManager = (_TWAIN_UnloadSourceManager)
  18. ::GetProcAddress(hDLL,"TWAIN_UnloadSourceManager");
  19. Picture = new Graphics::TPicture;
  20. }
  21. //---------------------------------------------------------------------------
  22. __fastcall CTWAIN::~CTWAIN()
  23. {
  24. delete Picture;
  25. ::FreeLibrary(hDLL);// 從 記 憶 體 中 釋 放DLL
  26. }
  27. //---------------------------------------------------------------------------
  28. void __fastcall CTWAIN::SelectImageSource(HWND hWnd)// 選 擇TWAIN32 設備
  29. {
  30. TWAIN_SelectImageSource(hWnd);
  31. // 使 用DLL 裡 頭 的TWAIN_SelectImageSource 函 式
  32. }
  33. //---------------------------------------------------------------------------
  34. void __fastcall CTWAIN::Acqure(HWND hWnd)// 經 由TWAIN32 設 備 取 得 資料
  35. {
  36. if (TWAIN_LoadSourceManager() > 0)
  37. // 使 用DLL 裡 頭 的TWAIN_LoadSourceManager 函 式
  38. {
  39. if (TWAIN_AcquireToClipboard(hWnd,0)>0)
  40. // 使 用DLL 裡 頭 的TWAIN_AcquireToClipboard 函 式
  41. {
  42. Clipboard()->Open();
  43. if (Clipboard()->HasFormat(CF_PICTURE))
  44. Picture->Assign(Clipboard());
  45. Clipboard()->Clear();
  46. Clipboard()->Close();
  47. }
  48. TWAIN_UnloadSourceManager();
  49. // 使 用DLL 裡 頭 的TWAIN_UnloadSourceManager 函 式
  50. }
  51. }
  52. //---------------------------------------------------------------------------
  53. bool __stdcall CheckTWAINDLL()// 檢 驗DLL 檔 案 是 否 存 在 的 函 式
  54. {
  55. HANDLE hDll = ::LoadLibrary("EZTW32.DLL");
  56. if(hDll == NULL)
  57. {
  58. ::MessageDlg("Error! Could not found EZTWtw32.dll",
  59. mtInformation,TMsgDlgButtons()<
  60. return false;
  61. }
  62. ::FreeLibrary(hDll);
  63. return true;
  64. }
  65. //---------------------------------------------------------------------------
當 建 構 好 這 個CTWAIN 類 別 後 , 若 我 們 要 使 用 這 剛 剛 建 好 的類 別 , 我 們 可 以 寫 成 兩 個 函 式 :

1 、void SelectSource(HWND hWnd);

選 擇TWAIN32 設 備 的 來 源

2 、void Acquire(HWND hWnd);

經 由 選 定 的TWAIN32 設 備 來 源 將 資 料 取 得

  1. //---------------------------------------------------------------------------
  2. void SelectSource(HWND hWnd)// 選 擇TWAIN32 設 備 的 來 源
  3. {
  4. if (CheckTWAINDLL())
  5. {
  6. CTWAIN TWAIN;
  7. TWAIN.SelectImageSource(hWnd);
  8. }
  9. }
  10. //---------------------------------------------------------------------------
  11. void Acquire(HWND hWnd)// 經 由 選 定 的TWAIN32 設 備 來 源 將 資 料 取得
  12. {
  13. if (CheckTWAINDLL())
  14. {
  15. CTWAIN TWAIN;
  16. TWAIN.AcqureScanner(hWnd);
  17. if (TWAIN.Picture->Graphic != NULL)
  18. {
  19. //Do the processing that you want to do with the TWAIN.Picture
  20. TWAIN.Picture->Graphic = NULL;
  21. }
  22. }
  23. }
  24. //---------------------------------------------------------------------------
相 形 之 下 , 似 乎 靜 態 載 入 的 方 式 方 便 多 了 , 不 用 下 這 麼多 工 夫 在 手 動 載 入DLL 並 手 動 分 派 各 函 式 的 實 際 位 址 , 若遇 到 一 大 堆 輸 出 函 式 豈 不 光Keyin GetProcAddress 那 一 段 就 得花 上 許 多 時 間 。 但 若 用 到 不 同 品 牌 的C++ 編 譯 器 所 產 生 的DLL 時 , 採 用 動 態 載 入 加 上 以 函 式 輸 出 序 號 來 取 得 函 式 指 標的 方 式 , 僅 需 利 用TDUMP 製 造 初 一 張 序 號 表 再 加 上 勤 勞 的打 字 即 可 無 須 再 動 什 麼 腦 筋 。 稍 後 會 提 到 怎 麼 樣 在C++Builder 下 使 用 靜 態 載 入 來 載 入Visual C++ 所 編 譯 出 來 的DLL 。 這 樣 相形 之 下 , 反 而 又 會 覺 得 用 動 態 載 入 方 便 些 。

Calling Conventions

因 為 不 同 的 語 言 間 有 不 同 的 傳 遞 參 數 的 方 法 , 而C/C++ 編 譯 器 為 了 能 夠 使 用 由 其 他 語 言 開 發 出 來 的 函 式 庫 加 上了 這 些 參 數 傳 遞 方 式 不 同 的 方 式 稱 為calling conventions ( 呼叫 慣 例 ) , 如 在C++Builder 裡 頭 常 常 看 到 的__fastcall ; 一 般常 用 的 呼 叫 慣 例 有 以 下 四 種 :

呼 叫 慣 例
說 明
參 數 傳 遞 方 式
__cdecl 傳 遞 的 參 數 由 呼 叫 函式 清 除 。 由 右 往 左 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。
__stdcall 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 。 一 般 使 用 於Win32 的 標 準 呼 叫 , 在DLLs 間 的 呼 叫大 部 分 都 使 用__stdcall 。 由 右 往 左 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。
__fastcall 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 。 在C++Builder 中__fastcall 為VCL 元 件 使 用 的 內 定 呼叫 慣 例 。 in C++Builder :

由 左 往 右 的 次 序 傳 遞 參 數 , 第 一 個 參 數 由EAX 傳 遞 , 第二 個 由EDX 傳 遞 , 第 二 個 由ECX 傳 遞 , 其 餘 操 超 過 的 參 數 再交 由 堆 疊 傳 遞 。

in Visual C++ :

由 左 往 右 的 次 序 傳 遞 參 數 , 第 一 個 參 數 由ECX 傳 遞 , 第二 個 由EDX 傳 遞 , 其 餘 操 超 過 的 參 數 再 交 由 堆 疊 傳 遞 。

__pascal 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 , 在Windows 3.1 時 期__pascall 為 標 準 用 法 , 但 到 了Windows 95/NT 後 以 鮮 少 使 用 。 由 左 往 右 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。Visual C++ 已 經 不 支 援__pascal 此 呼 叫 方式 。 因 此 在 此 不 做__pascal 的 討 論 。

但 光 是 說 說 很 難 了 解 到 這 些 參 數 傳 遞 方 式 有 何 異 同 ,以 下 函 式 分 別 使 用__cdecl 、__stdcall 與__fastcall 三 種 呼 叫 慣例 當 做 範 例 :

void calltype MyFunc(char c, shorty s, int i , double f);

當 我 使 用 這 個 函 式 :

MyFunc( “x ”,12,8192,2.7183);

時 會 被 編 譯 器 編 譯 成 如 以 下 四 圖 :

 

由 圖 八 與 圖 九 中 可 以 看 出__fastcall 在 兩 個 編 譯 器 中 有 顯著 的 不 同 ,C++Builder 使 用 了 三 個 暫 存 器 來 存 放 參 數 , 讓 傳遞 的 速 度 更 為 加 快 , 這 也 就 是VCL 類 別 中 的 預 設 呼 叫 慣 例為__fastcall 的 原 因 。 暫 且 撇 開__fastcall 的 不 同 , 呼 叫 慣 例造 成 的 還 不 只 這 一 樣 差 異 , 還 有 著 與name mangling 有 點 類 似的 麻 煩 , 就 是 函 式 的 名 稱 更 動 問 題 。 當 你 使 用 不 同 的 呼叫 慣 例 時 , 編 譯 器 還 是 對 動 點 手 腳 , 動 什 麼 手 腳 , 筆 者用 以 下 的 範 例 做 觀 察 , 先 定 義 四 個 函 式 分 別 使 用__fastcall 、__stdcall 與__cdecl 與 不 指 定 四 種 呼 叫 慣 例 :

  1. #define DLLEXP extern "C" __declspec(dllexport)
  2. DLLEXP int MyFunc_Default(char *c,int X)
  3. {
  4. return X;
  5. }
  6. DLLEXP int __fastcall MyFunc_Fast(char *c,int X)
  7. {
  8. return X;
  9. }
  10. DLLEXP int __stdcall MyFunc_Std(char *c,int X)
  11. {
  12. return X;
  13. }
  14. DLLEXP int __cdecl MyFunc_Cdecl(char *c,int X)
  15. {
  16. return X;
  17. }
定 義 好 後 分 別 使 用C++Builder 與Visual C++ 將 這 四 個 函 式 編 譯成DLL 檔 , 編 譯 完 成 後 使 用tdump 與impdef 來 觀 察 這 些 函 式 的輸 出 結 果 : (IMPDEF 的 用 法 為 IMPDEF def_file dll_file )

 

經 由 上 述 簡 單 的 實 驗 可 以 將 兩 者 的 差 異 列 出 , 並 且 可以 找 出 編 譯 器 預 設 的 呼 叫 慣 例 :

呼 叫 慣 例
原 始 函 式
Borland C++Builder
Microsoft Visual C++
__cdecl MyFunc_cdcel _MyFunc_cdcel MyFunc_cdcel
__stdcall MyFunc_std MyFunc_std _MyFunc_std@8
__fastcall MyFunc_fast @MyFunc_fast @MyFunc_fast@8
MyFunc_default _MyFunc_default MyFunc_default
預 設 呼 叫 慣 例
__cdecl __cdecl

由 表 上 可 以 看 出 兩 者 間 的 差 異 還 不 少 呢 ! 而 為 什 麼 要討 論 到 這 一 點 呢 ? 因 為 接 著 就 要 討 論 到 如 何 拿Visual C++ 所編 譯 的DLL 到C++Builder 裡 頭 使 用 。

在Borland C++Builder 下 使 用Microsoft Visual C++ 所 編 譯 的DLLs

若 已 經 解 決 了name mangling 與calling convention 的 理 想 狀 況 下, 由C++Builder 下 來 呼 叫Visual C++ 所 編 譯 出 來 的DLLs 應 該 不 是難 事 才 對 。 但 不 幸 的 , 只 對 了 一 半 , 怎 麼 說 , 幸 運 的 那一 半 是 , 咱 們 可 以 利 用 先 前 提 過 的 『 動 態 載 入 』 方 式 來載 入DLLs 中 的 函 式 , 即 使 因 為calling convention 的 問 題 導 致 函式 名 稱 在 編 譯 後 會 被 更 動 , 但 是 只 要 知 道 函 式 的 輸 出 序號 就 可 以 照 樣 載 入 ; 不 幸 的 那 一 半 是 若 採 用 『 靜 態 載 入』 的 方 法 就 又 會 遇 上 了 個 大 難 題 :Borland 與Microsoft 所 使 用的OBJs 檔 案 格 式 不 相 同 ;Borland 採 用Intel 所 訂 定 的OMF (Object Module Format ) 格 式 而Microsoft 採 用COFF (Common Object File Format ) 格 式 , 因 此 若 要 拿Visual C++ 所 編 譯 的DLLs 與LIBs 來 使 用 ,僅 有DLLs 能 夠 用 而 已 ,LIBs 毫 無 用 武 之 地 , 但 謝 天 謝 地 ,Borland 提 供 了 一 個 工 具IMPLIB.EXE , 可 以 直 接 從 任 何 編 譯 器 所 編 譯出 的DLLs 裡 頭 將OMF 格 式 的LIBs 給 製 造 出 來 。 因 此 製 造C++Builder 相 容 的OMF 格 式LIBs 不 算 是 個 大 問 題 了 。 但 對 於Calling Convention 所 產 生 的 問 題 就 比 較 麻 煩 些 , 接 下 來 咱 們 就 來 討 論 如 何使 用 靜 態 載 入 來 載 入Visual C++ 所 製 造 出 來 的DLLs 。

要 將Visual C++ 所 製 造 出 了DLLs 搬 到C++Builder 來 用 大 致 上 分三 個 步 驟 :

A 、 檢 驗 輸 出 函 式 的 設 呼 叫 慣 例

由 先 前 呼 叫 慣 例 的 實 驗 裡 看 得 出 來 , 兩 種 編 譯 器 的 預設 呼 叫 慣 例 皆 為__cdecl , 而__cdecl 與__stdcall 的 參 數 傳 遞 方式 兩 個 編 譯 器 也 相 同 , 但 是 在C++Builder 下__fastcall 為VCL 的標 準 呼 叫 慣 例 , 而 且 為 了 加 速 參 數 的 傳 遞 , 參 數 傳 遞 的方 法 與Visual C++ 不 同 , 若 是 硬 搬 到C++Builder 來 使 用 , 恐 有問 題 出 現 ; 因 此 若 要 拿Visual C++ 所 編 譯 的DLL 來 使 用 , 切 記只 能 使 用__cdecl 與__stdcall 這 兩 種 呼 叫 慣 例 。

B 、 查 驗 經 過 編 譯 器 編 譯 後 的 函 式 名 稱

由 呼 叫 慣 例 的 實 驗 結 果 裡 看 出 ,__cdecl 與__stdcall 在 兩 個編 譯 器 下 所 編 譯 出 的 正 式 名 稱 稍 有 不 同 , 以 一 個 簡 單 的 void MyFunction(void); 為 例 :

呼 叫 慣 例 Borland C++Builder Microsoft Visual C++
__cdecl _MyFunction MyFunction
__stdcall MyFunction _MyFunction@4

我 們 必 須 檢 查 看 看 哪 些 函 式 是 使 用__stdcall 而 哪 些 函 式是 使 用__cdecl , 接 下 來 下 一 個 步 驟 就 是 轉 換 這 些 名 稱 , 將名 稱 由C++Builder 不 認 得 變 成 認 得 。

C 、 製 作OMF 格 式 的LIBs

由 於 經 過 編 譯 器 處 理 過 後 的 函 式 名 稱 已 經 與 原 先 函 式名 稱 不 相 同 了 , 因 此 若 直 接 轉 換 成LIBs 檔 也 無 啥 效 用 , 必須 動 點 手 腳 。 先 前 已 經 學 過IMPDEF 的 使 用 方 法 , 先 前 是 用在 觀 察 輸 出 函 式 , 現 在 也 是 , 但 還 多 了 動 手 腳 的 部 份 。

利 用 先 前 測 試 的 那 個VC6TEST.DLL 來 製 造DEF 檔 案 : (__fastcall 的 呼 叫 慣 例 部 份 記 得 要 先 除 去 )

IMPDEF VC6TEST.DEF VC6TEST.DLL

製 造 出 來 的DEF 檔 :

LIBRARY VC6TEST.DLL

EXPORTS

MyFunc_Cdecl @1

MyFunc_Default @2

_MyFunc_Std@8 =_MyFunc_Std @3

這 時 候 我 們 就 來 動 手 腳 , 把C++Builder 不 認 得 給 改 承 認 得的 。 改 成 如 下 :

EXPORTS

;use this type of aliasing

;(Borland name) = (Name exported by Visual C++)

MyFunc_Std = _MyFunc_Std@8

_MyFunc_Cdecl = MyFunc_Cdecl

_MyFunc_Default = MyFunc_Default

最 後 再 將DEF 檔 給 還 原 成LIB 檔 , 什 麼 ? 你 有 沒 有 說 錯 把DEF 這 個 文 字 檔 變 回LIB 檔 ? 是 的 , 其 實 靜 態 載 入 所 需 要 用 到的LIB 檔 案 只 是 個 函 式 表 格 罷 了 , 並 沒 有 真 正 函 式 內 容 在裡 頭 , 因 此 我 們 可 以 經 由 動 過 手 腳 的DEF 檔 給 還 原 成LIB 檔:

IMPLIB VC6TEST.lib VC6TEST.def

動 完 手 腳 後 不 忘 還 要 去 檢 查 一 下 是 否 函 式 輸 出 , 可 以使 用TLIB 來 觀 察 最 後 的test.lib 是 否 正 確 輸 出 :

TLIB VC6TEST.lib,VC6TEST.txt

Publics by module

_MyFunc_cdecl size = 0

_MyFunc_cdecl

_MyFunc_default size = 0

_MyFunc_default

MyFunc_std size = 0

MyFunc_std

可 以 由 觀 察test.txt 來 檢 驗 輸 出 的 正 確 與 否 , 在 確 定 輸出 名 稱 無 誤 後 就 可 以 直 接 拿 進C++Builder 做 先 前 已 經 說 明 過的 靜 態 載 入 測 試 了 。 我 的 測 試 方 法 為 建 立 一 個Console Application 來 載 入 這 個DLL 做 測 試 。

  1. #pragma hdrstop
  2. #include
  3. //---------------------------------------------------------------------------
  4. USELIB("VC6test.lib");
  5. //---------------------------------------------------------------------------
  6. #pragma argsused
  7. #define DLLIMP extern "C" __declspec(dllimport)
  8. DLLIMP int MyFunc_default(char *c,int X);
  9. DLLIMP int __stdcall MyFunc_std(char *c,int X);
  10. DLLIMP int __cdecl MyFunc_cdecl(char *c,int X);
  11. #include
  12. #include
  13. int main(int argc, char **argv)
  14. {
  15. printf("int x = MyFunc_std(\"x\",123);\n");
  16. int x = MyFunc_std("x",123);
  17. printf("Result : x = %d\n",x);
  18. printf("int y = MyFunc_cdecl(\"x\",2323);\n");
  19. int y = MyFunc_cdecl("x",2323);
  20. printf("Result : y = %d\n",y);
  21. getch();
  22. return 0;
  23. }
 

輸 出 結 果 :

int x = MyFunc_std("x",123);

Result : x = 123

int y = MyFunc_cdecl("x",2323);

Result : y = 2323

這 樣 一 來 算 大 功 告 成 , 但 在 筆 者 的 經 驗 中 使 用Visual C++ 來 寫 作DLL 供Borland C++Builder 使 用 , 有 以 下 幾 點 是 值 得 注 意的 :

  1. 抑 制name mangling 的 行 為
    一 定 要 記 住 為 輸 出 函 式 加 上 extern “C ” 來 抑 制 name mangling 的 動 手 腳 。
  2. 禁 止 輸 出 類 別
為 什 麼 要 禁 止 類 別 的 輸 出 呢 ? 因 為Visual C++ 處 理 類 別 的 方式 與C++Builder 有 著 很 大 的 不 同 。 所 以 少 用 為 妙 。
結 語

這 次 筆 者 大 多 著 墨 於DLLs 的 使 用 , 怎 麼 建 立DLLs 的 方 法卻 只 大 略 一 題 , 因 為 製 造DLLs 的 方 法 有 很 多 , 也 有 很 多 的技 巧 , 若 把 這 些 通 通 都 給 寫 出 來 , 可 能 這 個 主 題 就 要 變成 連 載 小 說 般 分 成 好 幾 期 來 刊 載 了 , 一 期 為 時 一 個 月 ,討 論 這 個 主 題 的 時 間 會 拖 得 太 久 , 且 相 差 時 間 會 過 久 ,造 成 讀 者 的 無 法 連 貫 , 筆 者 只 好 忍 痛 割 捨 , 不 過 筆 者 在此 還 是 列 出 數 本 對 於 製 作DLLs 一 題 著 墨 相 當 詳 細 的 書 籍 :

1 、Adcanced Windows 3/E, Microsoft Press, Jeffrey Richter Chapter 12

2 、Programming Windows 95, Microsoft Press, Charles Petzold Chapter 19

3 、Multithreading Application in Win32, Addison Wesley, JimBeveridge, etc . Chapter 14

4 、Borland C++Builder 3 Unleashed, Sams Publishing, Charlie Calvert, etc. Chapter 35

註1 、PE 格 式 的 執 行 檔 :PE ,Portable Executable , 是Microsoft 設 計 用 於 其 所 有Win32 作 業 系 統 (Win32s 、Windows NT 及Windows 95/98 ) 的 可 執 行 檔 格 式 。

註2 、 在C++Builder 下 觀 看 編 譯 器 產 生 的 真 正 名 稱 方 法 :


選 單 上 『Project/Options 』 裡 的 『Linker 』 一 頁 中 的Map File 選 項 中 勾 選derailed :

註3 、 在Visual C++ 下 觀 看 編 譯 器 產 生 的 真 正 名 稱 方 法 :


選 單 上 『Project/Settings … 』 中 『Link 』 一 頁 且Category 為General 時 將Generate mapfile 選 項 勾 選 。

參 考 資 料 、

  1. Windows 95 System Programmung SECRETS, IDG BOOKS, Matt Pietrek
  2. Adcanced Windows 3/E, Microsoft Press, Jeffrey Richter
  3. Programming Windows 95, Microsoft Press, Charles Petzold
  4. Borland C++Builder 3 Unleashed, Sams Publishing, Charlie Calvert
  5. High Preformance Borland C++Builder, Coriolis Group Books, Matt Telles

No comments:

Post a Comment