若要讓原生 Mac 外掛程式內含的相依項目保持私有,請以隱藏可見度編譯其實作,並在最後連結時只匯出外掛程式的公開進入點。請將這些控制套用至實際包含該相依項目的二進位檔。
本指南適用於開發 Mac 桌面應用程式的開發者,說明如何建置執行階段載入且包含 C 或 C++ 相依項目的外掛程式。具體目標是保留外掛程式自身的實作,同時只公開精簡的主程式介面。本指南以合成範例將 C 相依項目建置到 C++ 外掛程式中,並檢查其結果與匯出符號。
哪個二進位檔需要限制匯出?
首先確認相依項目的機器碼最後位於何處。靜態封存檔會將物件檔提供給正在連結的二進位檔;動態程式庫則會保留為獨立映像檔。因此,隱藏外掛程式中的符號,無法改寫獨立相依 dylib 的匯出表。Apple 在其動態程式庫使用指南中說明了這項差異。
| 相依項目配置 | 套用控制的位置 | 要驗證的內容 |
|---|---|---|
| 將相依項目原始碼編譯到外掛程式中 | 相依項目編譯階段與外掛程式最後連結階段 | 私有定義確實存在,但未匯出 |
| 將靜態封存檔連結到外掛程式中 | 可行時在封存檔編譯階段,以及外掛程式最後連結階段 | 必要的封存檔成員已納入;相依項目符號未匯出 |
| 獨立的相依 dylib | 相依 dylib 自身的建置 | 其公開介面,以及外掛程式記錄的匯入項目 |
| 從 C++ 標頭檔具現化相依項目程式碼 | 每個會編譯這些標頭檔的目標 | 已產生的內嵌函式、樣板與相關資料 |
此表是根據編譯與連結邊界整理的判斷輔助。例如,套用至包裝目標的匯出清單,不會自動隱藏在另一個目標中產生的符號。
macOS 預設也會使用雙層命名空間:對 dylib 解析的參照會記錄提供該符號的程式庫。平坦命名空間會改變這種行為。弱外部定義則帶來另一項考量,因為載入器可能會合併這些定義。Apple 在其連結器手冊中說明了這兩種機制。因此,僅憑名稱相符,無法判定外掛程式為何呼叫了非預期的實作。
將隱藏可見度編譯與明確匯出清單結合使用
編譯器與連結器控制作用於不同階段。Clang 的 -fvisibility=hidden 會設定正在編譯之原始碼檔案中全域定義的預設可見度,但不會重新編譯既有封存檔。Clang 另有 -fvisibility-inlines-hidden,用於 C++ 內嵌成員函式;這是特定用途的選項,是否適用取決於介面需求。請參閱 Clang 命令列參考。
使用 __attribute__((visibility("default"))) 標註預期的公開進入點。可見度屬性可以覆寫編譯預設值,因此請檢查相依項目標頭檔是否有明確的匯出標註。Apple 在控制符號可見度中說明了這項行為。
在最後連結時,使用 -exported_symbols_list 列出公開介面。Apple 文件所載的匯出清單格式使用連結器符號名稱,C 符號名稱前方須包含底線。請選用匯出清單或未匯出符號清單其中一種策略,不要同時使用。請參閱動態程式庫設計指南。
對於介面精簡的外掛程式,明確匯出清單能讓審查更直接:每個匯出名稱都必須有公開的理由。請讓原始碼標註與清單保持一致;標註會在程式碼中識別 API,清單則會限制最終二進位檔。
建置完整的本機範例
本範例刻意使用 C 相依項目,以示範 Mach-O 匯出控制,同時避免將不相容的 C++ 定義說成可攜式語言技巧。主程式的相依項目會回傳 10;外掛程式的私有建置則會回傳 20。這些值僅供說明。
請在可使用 Xcode 命令列工具的 Mac 上,於同一個終端機 Shell 中依序執行下列區塊。這些命令會建立全新的暫存目錄,並針對目前工具鏈的預設架構進行建置。本範例已經過檢視,但研究期間未進行編譯或執行,因為當時可用的環境是 Linux,沒有 Apple 工具鏈。
建立相依項目與外掛程式
set -eu
example_dir=$(mktemp -d)
cd "$example_dir"
cat > dependency.h <<'EOF'
#ifndef DEMO_DEPENDENCY_H
#define DEMO_DEPENDENCY_H
#ifdef __cplusplus
extern "C" {
#endif
int demo_dependency_value(void);
#ifdef __cplusplus
}
#endif
#endif
EOF
cat > dependency.c <<'EOF'
#include "dependency.h"
#ifndef DEP_VALUE
#error DEP_VALUE must be supplied at compilation
#endif
int demo_dependency_value(void) {
return DEP_VALUE;
}
EOF
cat > plugin.cpp <<'EOF'
#include "dependency.h"
extern "C" __attribute__((visibility("default")))
int demo_plugin_value(void) {
return demo_dependency_value();
}
EOF
cat > exports.txt <<'EOF'
_demo_plugin_value
EOF
相依項目標頭檔會為 C++ 包裝程式提供正確的 C 語言連結宣告。公開包裝函式也使用 C 連結,讓此進入點的連結器名稱保持簡單。C++ 名稱通常需要名稱修飾;Apple 在控制符號可見度中討論了這項差異。
建立主程式
主程式會載入外掛程式,並透過回傳的程式庫控制代碼取得其公開函式。Apple 文件指出,dlopen 同時支援動態程式庫與 Mach-O 套件。其 RTLD_LOCAL 選項會限制已載入映像檔的匯出符號可見範圍,但不能取代建置時的可見度控制。請參閱 Apple 的 dlopen 手冊。
cat > host.cpp <<'EOF'
#include "dependency.h"
#include <cstdio>
#include <dlfcn.h>
int main() {
void* handle = dlopen("./DemoPlugin.dylib", RTLD_NOW | RTLD_LOCAL);
if (!handle) {
std::fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
dlerror();
void* address = dlsym(handle, "demo_plugin_value");
const char* error = dlerror();
if (error || !address) {
std::fprintf(stderr, "dlsym: %s\n",
error ? error : "null function address");
dlclose(handle);
return 1;
}
using PluginValue = int (*)(void);
PluginValue plugin_value = reinterpret_cast<PluginValue>(address);
const int host_result = demo_dependency_value();
const int plugin_result = plugin_value();
std::printf("host=%d plugin=%d\n", host_result, plugin_result);
const bool correct = host_result == 10 && plugin_result == 20;
if (dlclose(handle) != 0) {
std::fprintf(stderr, "dlclose: %s\n", dlerror());
return 1;
}
return correct ? 0 : 2;
}
EOF
編譯與連結
# Build the plug-in's dependency with hidden definitions.
xcrun clang -fvisibility=hidden -DDEP_VALUE=20 \
-c dependency.c -o plugin_dependency.o
xcrun ar rcs libPluginDependency.a plugin_dependency.o
# Compile the public wrapper, then constrain the final exports.
xcrun clang++ -std=c++17 -fvisibility=hidden \
-c plugin.cpp -o plugin.o
xcrun clang++ -dynamiclib plugin.o libPluginDependency.a \
-Wl,-exported_symbols_list,exports.txt \
-o DemoPlugin.dylib
# Give the host a separately compiled implementation.
xcrun clang -DDEP_VALUE=10 -c dependency.c -o host_dependency.o
xcrun clang++ -std=c++17 host.cpp host_dependency.o \
-Wl,-exported_symbol,_demo_dependency_value -o host
./host
預期輸出為 host=10 plugin=20。主程式會明確匯出其相依項目符號,讓本範例包含一個具有相同 C 名稱、可從外部存取的實作。外掛程式仍須包含自己的私有實作。
這是驗證範例,並非宣稱移除控制項就一定會讓結果失敗。在許多配置中,Mach-O 的預設繫結方式已能區分不同定義。值得檢查的是選用的實作,以及最終匯出範圍。
分別驗證匯出與匯入
在範例目錄中執行下列命令:
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
在第一個命令中,-g 會選取外部符號,-U 會排除未定義符號,-j 則會列印名稱。對此精簡的外掛程式而言,預期的已定義外部名稱是 _demo_plugin_value;_demo_dependency_value 不應出現。Apple 的 nm 手冊記載了這些選項,並說明符號表資訊可能與載入器使用的資訊不同。
使用 dyld_info -exports 檢查載入器所見的匯出項目,並使用 -imports 檢查匯入的符號。相依項目函式不應出現在任何一份清單中:它由外掛程式在內部私有實作。這些檢查選項記載於 Apple 的 dyld_info 手冊。
使用 otool -L 檢查實際的 dylib 相依項目。靜態封存檔不應列為獨立的執行階段相依項目。Apple 在動態程式庫使用指南中說明了這項相依項目檢查。
對於實際的外掛程式,請分開確認三個問題:必要的實作是否已納入?它是否已匯出?是否仍有呼叫會匯入競爭的實作?建置成功本身無法回答其中任何一個問題。
在 Xcode 中套用相同控制
對於會產生最終外掛程式二進位檔的目標,請將 Exported Symbols File(EXPORTED_SYMBOLS_FILE)設為匯出清單相對於專案的路徑。請在會編譯私有實作的 C 與 C++ 目標上啟用 Symbols Hidden by Default(GCC_SYMBOLS_PRIVATE_EXTERN)。Apple 最新的建置設定參考記載了這兩項設定。
如果相依項目是獨立的靜態程式庫目標,也請設定該目標。只修改外掛程式的編譯器設定,不會影響先前已編譯的相依項目物件。請重新建置受影響的目標,並檢查每個要發佈架構所產生的產品。
對於預先建置的封存檔,最後連結時仍可使用匯出清單。Apple 公開的連結器手冊也記載了 -load_hidden,可在正常擷取封存檔成員時,將封存檔的全域符號視為隱藏符號。該手冊適用於 ld-classic;選用此額外選項前,請先查閱已安裝連結器的文件。-force_load 控制的是封存檔成員是否納入,這是另一項不同問題。請參閱 Apple 連結器手冊。
保留 C++ 與 Swift 介面需求
C++ 可見度無法修復不相容的定義
C++ 單一定義規則對跨翻譯單元的定義設下要求;在允許多個定義的情況下,也包括符合權杖與名稱查找要求。隱藏符號不代表可以合併互相衝突的 C++ 定義。若要使用可獨立版本管理的實作,請在可行時採用不同的命名空間或符號名稱。請參閱 C++ 工作草案的單一定義規則。
也請保留刻意設計的 C++ 介面所需的可見度。Apple 指出,跨程式庫 RTTI、例外、動態轉型、內嵌函式位址識別,以及共用靜態物件等情況都需要謹慎處理。實用的設計方式是將相依項目型別與所有權留在外掛程式內,並以約定好的純量值或不透明控制代碼提供精簡介面。請參閱控制符號可見度。
Swift 匯入存取控制管理不同的邊界
Swift 的 internal import 會限制匯入宣告可被參照的範圍。SE-0409 已在 Swift 6.0 實作,建議將 @_implementationOnly import 的用法改為 internal import 或更低的存取層級。它並未定義 Mach-O 相依項目重新命名或匯出篩選機制。請繼續分別設定與檢查原生二進位檔。請參閱匯入宣告的存取層級修飾詞。
若使用 Swift/C++ 直接互通,請保留其工具鏈限制:Swift 5.9 起才支援,混合程式碼必須使用相同的 C++ 標準程式庫,而 Apple 平台使用 libc++。匯入的 C++ 參考型別也會將 macOS 13.3 設為最低部署版本。匯出篩選不會改變這些需求。請參閱 C++ 互通的支援功能與限制。
當預期的進入點仍可使用、私有相依項目定義未出現在載入器匯出項目中,且匯入項目符合預期使用的程式庫時,外掛程式便已準備好進行整合測試。請將這些檢查與合成範例一類的行為測試一併保留,讓後續相依項目更新能揭露介面變更與實作選擇變更。
來源
- 建置設定參考 — Apple。
- 動態程式庫設計指南 — Apple。
- 動態程式庫使用指南 — Apple。
- 控制符號可見度 — Apple。
- Clang 命令列引數參考 — LLVM Project。
- ld-classic(1) 手冊 — Apple。
- dlopen(3) 手冊 — Apple。
- nm(1) 手冊 — Apple。
- dyld_info(1) 手冊 — Apple。
- 單一定義規則 — C++ 工作草案,由 Tim Song 排版呈現。
- 匯入宣告的存取層級修飾詞 — Swift Project。
- C++ 互通的支援功能與限制 — Swift.org。