ネイティブ Mac プラグインに同梱された依存ライブラリを非公開に保つには、その実装を非表示の可視性でコンパイルし、最終リンク時にプラグインの公開エントリポイントだけをエクスポートします。これらの制御は、依存ライブラリを実際に含むバイナリに適用してください。
このガイドは、C または C++ の依存ライブラリを含む、実行時に読み込まれるプラグインを構築する Mac デスクトップ開発者向けです。具体的な作業は、プラグイン独自の実装を保持しながら、ホスト向けの小さなインターフェースを公開することです。例では C の依存ライブラリを C++ プラグインに組み込み、その結果とエクスポートされたシンボルの両方を確認します。
エクスポートを制限する必要があるバイナリはどれですか?
まず、依存ライブラリの機械語コードがどこに配置されるかを特定します。静的アーカイブはリンク対象のバイナリにオブジェクトファイルを提供しますが、動的ライブラリは独立したイメージとして残ります。そのため、プラグイン内のシンボルを非表示にしても、別個の依存 dylib のエクスポートテーブルは変更できません。Apple はこの違いを Dynamic Library Usage Guidelines で説明しています。
| 依存ライブラリの構成 | 制御を適用する場所 | 確認すること |
|---|---|---|
| 依存ライブラリのソースをプラグインにコンパイルする | 依存ライブラリのコンパイルとプラグインの最終リンク | 非公開の定義が含まれているが、エクスポートされていない |
| 静的アーカイブをプラグインにリンクする | 可能であればアーカイブのコンパイルと、プラグインの最終リンク | 必要なアーカイブメンバーが含まれており、依存ライブラリのエクスポートがない |
| 独立した依存 dylib | 依存 dylib 自体のビルド | その公開インターフェースと、プラグインに記録されたインポート |
| C++ ヘッダーから依存ライブラリのコードを生成する | それらのヘッダーをコンパイルするすべてのターゲット | 出力されたインライン関数、テンプレート、および関連データ |
この表は、コンパイルとリンクの境界を踏まえた判断材料です。たとえば、ラッパーターゲットに指定したエクスポートリストは、別のターゲットに出力されたシンボルを自動的に非表示にするわけではありません。
macOS はデフォルトで 2 レベル名前空間も使用します。dylib に対して解決される参照には、どのライブラリがその参照を提供したかが記録されます。フラット名前空間では、この動作が変わります。弱い外部定義も、ローダーがそれらを統合する可能性があるため、考慮が必要です。Apple は両方の仕組みを リンカーマニュアル で説明しています。そのため、名前が一致するだけでは、プラグインが予期しない実装を呼び出した理由は分かりません。
非表示のコンパイルと明示的なエクスポートリストを組み合わせる
コンパイラとリンカーの制御は、それぞれ異なる段階で作用します。Clang の -fvisibility=hidden は、コンパイル対象のソースファイルにあるグローバル定義のデフォルト可視性を設定します。既存のアーカイブを再コンパイルするものではありません。Clang はインライン C++ メンバー関数向けに -fvisibility-inlines-hidden も別途説明しています。これは、インターフェースに適しているかを検討したうえで使う、対象を絞ったオプションとして扱ってください。Clang コマンドラインリファレンス を参照してください。
意図した公開エントリポイントには __attribute__((visibility("default"))) を付けます。可視性属性はコンパイル時のデフォルト設定を上書きできるため、依存ライブラリのヘッダーに明示的なエクスポート注釈がないか確認してください。Apple はこの動作を シンボルの可視性の制御 で説明しています。
最終リンク時には -exported_symbols_list を使って公開インターフェースを列挙します。Apple が説明するエクスポートリストの形式では、C シンボルの先頭にアンダースコアを付けるなど、リンカーのシンボル名を使用します。エクスポートリストか非エクスポートシンボルリストのいずれかを選び、両方の方式を組み合わせないでください。Dynamic Library Design Guidelines を参照してください。
インターフェースが小さいプラグインでは、明示的なエクスポートリストを使うとレビューが簡単になります。エクスポートされる名前にはすべて、公開する理由が必要です。ソースの注釈とリストに一貫性を保ってください。注釈はコード内で API を示し、リストは最終バイナリを制約します。
ローカルで完結する例をビルドする
この例では意図的に C の依存ライブラリを使います。互換性のない C++ 定義を移植可能な言語の手法として示すことなく、Mach-O のエクスポート制御を説明できます。ホスト側の依存関数は 10 を返し、プラグイン内の非公開ビルドは 20 を返します。これらは説明用の値です。
Xcode Command Line Tools が利用できる Mac で、同じターミナルシェル内で以下のブロックを順番に実行してください。新しい一時ディレクトリを作成し、アクティブなツールチェーンのデフォルトアーキテクチャ向けにビルドします。この例は確認しましたが、調査時に利用できた環境は Apple のツールチェーンがない Linux だったため、コンパイルも実行もしていません。
依存ライブラリとプラグインを作成する
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 の Dynamic Library Usage Guidelines では、この依存関係の確認方法が説明されています。
実際のプラグインでは、次の 3 つの問いを個別に確認してください。必要な実装が含まれているか。エクスポートされているか。呼び出しが競合する実装をインポートし続けていないか。ビルドが成功しただけでは、どの問いにも答えられません。
Xcode で同じ制御を適用する
最終的なプラグインバイナリを生成するターゲットで、Exported Symbols File(EXPORTED_SYMBOLS_FILE)にエクスポートリストのプロジェクト相対パスを設定します。非公開実装をコンパイルする C および C++ のターゲットでは、Symbols Hidden by Default(GCC_SYMBOLS_PRIVATE_EXTERN)を有効にします。Apple の現行の Build settings reference に、両方の設定が記載されています。
依存ライブラリが独立した静的ライブラリターゲットである場合は、そのターゲットにも設定してください。プラグインのコンパイラ設定だけを変更しても、依存ライブラリの既存のコンパイル済みオブジェクトはそのまま残ります。影響を受けるターゲットを再ビルドし、配布する各アーキテクチャの生成物を確認してください。
ビルド済みアーカイブにも、最終エクスポートリストは引き続き有効です。Apple が公開しているリンカーマニュアルには、通常のアーカイブ抽出を使いながら、アーカイブ内のグローバルシンボルを非表示として扱う -load_hidden も記載されています。このマニュアルの対象は ld-classic です。この追加オプションを選ぶ前に、インストール済みリンカーのドキュメントを確認してください。-force_load はアーカイブメンバーを含める制御であり、別の問題に対処するものです。Apple リンカーマニュアル を参照してください。
C++ と Swift のインターフェース要件を維持する
C++ の可視性制御では、互換性のない定義を修正できない
C++ の単一定義規則は、複数の定義が許される場合のトークンの一致や名前検索の要件を含め、翻訳単位をまたぐ定義に要件を課します。シンボルを非表示にしても、競合する C++ 定義を組み合わせてよいことにはなりません。独立してバージョン管理された実装では、可能であれば個別の名前空間またはシンボル名を使ってください。C++ 作業草案の単一定義規則 を参照してください。
意図的な C++ インターフェースに必要な可視性も維持してください。Apple は、ライブラリをまたぐ RTTI、例外、動的キャスト、インライン関数のアドレス同一性、共有静的オブジェクトでは注意が必要だと説明しています。実用的な設計としては、依存ライブラリの型と所有権をプラグイン内に保持し、合意済みのスカラー値または不透明ハンドルを使った小さなインターフェースを公開します。シンボルの可視性の制御 を参照してください。
Swift のインポートアクセス制御は別の境界を扱う
Swift の internal import は、インポートされた宣言を参照できる範囲を制限します。Swift 6.0 で実装された SE-0409 では、@_implementationOnly import の使用を internal import 以下に置き換えることが推奨されています。これは Mach-O の依存関係の名前変更やエクスポートのフィルタリング機能を定義するものではありません。ネイティブバイナリの設定と確認は引き続き個別に行ってください。インポート宣言のアクセスレベル修飾子 を参照してください。
Swift/C++ の直接相互運用を使う場合は、ツールチェーンの制約も守ってください。サポートは Swift 5.9 から始まり、混在コードでは同じ C++ 標準ライブラリを使う必要があります。Apple プラットフォームでは libc++ を使います。インポートされた C++ 参照型を使う場合、macOS 13.3 以降が最低デプロイメントバージョンになります。エクスポートのフィルタリングによって、これらの要件が変わることはありません。C++ 相互運用でサポートされる機能と制約 を参照してください。
意図したエントリポイントが利用可能で、依存ライブラリの非公開定義がローダーのエクスポートから除外され、インポートが想定どおりのライブラリと一致していると確認できたら、プラグインは統合テストに進められます。合成した例のような動作テストとともにこれらの確認を続けて行えば、後の依存ライブラリ更新でインターフェースの変更と実装選択の変更の両方を検出できます。
参考資料
- Build settings reference — Apple。
- Dynamic Library Design Guidelines — Apple。
- Dynamic Library Usage Guidelines — Apple。
- Controlling Symbol Visibility — Apple。
- Clang command line argument reference — LLVM Project。
- ld-classic(1) manual — Apple。
- dlopen(3) manual — Apple。
- nm(1) manual — Apple。
- dyld_info(1) manual — Apple。
- 単一定義規則 — C++ 作業草案、Tim Song によるレンダリング。
- インポート宣言のアクセスレベル修飾子 — Swift Project。
- C++ 相互運用でサポートされる機能と制約 — Swift.org。