Damit eine mit einem nativen Mac-Plug-in gebündelte Abhängigkeit privat bleibt, kompiliere ihre Implementierung mit verborgener Sichtbarkeit und exportiere beim abschließenden Linken nur die öffentlichen Einstiegspunkte des Plug-ins. Wende diese Kontrollen auf die Binärdatei an, die die Abhängigkeit tatsächlich enthält.
Diese Anleitung richtet sich an Entwickler von Mac-Desktopsoftware, die ein zur Laufzeit geladenes Plug-in mit einer C- oder C++-Abhängigkeit erstellen. Ziel ist es, die eigene Implementierung des Plug-ins beizubehalten und zugleich eine kleine Schnittstelle für den Host bereitzustellen. Ein synthetisches Beispiel bindet eine C-Abhängigkeit in ein C++-Plug-in ein und überprüft sowohl dessen Ergebnis als auch die exportierten Symbole.
Für welche Binärdatei müssen die Exporte begrenzt werden?
Ermittle zunächst, wo der Maschinencode der Abhängigkeit landet. Ein statisches Archiv stellt Objektdateien für die Binärdatei bereit, die gerade gelinkt wird; eine dynamische Bibliothek bleibt ein separates Abbild. Daher kann das Verbergen von Symbolen in einem Plug-in die Exporttabelle einer separaten Abhängigkeits-Dylib nicht ändern. Apple beschreibt diesen Unterschied in den Richtlinien zur Verwendung dynamischer Bibliotheken.
| Anordnung der Abhängigkeit | Wo die Kontrolle angewendet wird | Was zu überprüfen ist |
|---|---|---|
| Quellcode der Abhängigkeit wird in das Plug-in kompiliert | Kompilierung der Abhängigkeit und abschließendes Linken des Plug-ins | Private Definitionen sind vorhanden, aber nicht exportiert |
| Statisches Archiv wird in das Plug-in gelinkt | Wenn möglich beim Kompilieren des Archivs und zusätzlich beim abschließenden Linken des Plug-ins | Erforderliche Archivmitglieder wurden eingebunden; Abhängigkeitsexporte fehlen |
| Separate Abhängigkeits-Dylib | Eigener Build der Abhängigkeits-Dylib | Öffentliche Schnittstelle und aufgezeichnete Importe des Plug-ins |
| Abhängigkeitscode wird über C++-Header instanziiert | Jedes Ziel, das diese Header kompiliert | Ausgegebene Inline-Funktionen, Templates und zugehörige Daten |
Diese Tabelle dient als Entscheidungshilfe und leitet sich aus den Grenzen zwischen Kompilierung und Linken ab. Eine Exportliste für ein Wrapper-Ziel verbirgt beispielsweise nicht automatisch Symbole, die in ein anderes Ziel ausgegeben werden.
macOS verwendet standardmäßig außerdem einen zweistufigen Namensraum: Verweise, die gegen eine Dylib aufgelöst werden, halten fest, welche Bibliothek sie bereitgestellt hat. Der flache Namensraum ändert dieses Verhalten. Schwache externe Definitionen sind ein weiterer Aspekt, da der Loader sie zusammenführen kann. Apple dokumentiert beide Mechanismen im Linker-Handbuch. Gleiche Namen allein belegen daher nicht, weshalb ein Plug-in eine unerwartete Implementierung aufgerufen hat.
Verborgene Sichtbarkeit bei der Kompilierung mit einer expliziten Exportliste kombinieren
Compiler- und Linkeroptionen greifen in unterschiedlichen Phasen. Clangs -fvisibility=hidden legt die Standardsichtbarkeit globaler Definitionen in den kompilierten Quelldateien fest. Vorhandene Archive werden dadurch nicht neu kompiliert. Clang dokumentiert für Inline-C++-Memberfunktionen außerdem separat -fvisibility-inlines-hidden; betrachte dies als gezielte Option, deren Eignung von der Schnittstelle abhängt. Siehe die Clang-Referenz für Kommandozeilenoptionen.
Kennzeichne die vorgesehenen öffentlichen Einstiegspunkte mit __attribute__((visibility("default"))). Sichtbarkeitsattribute können die Kompilierungsstandardeinstellung überschreiben. Prüfe daher Abhängigkeitsheader auf explizite Exportannotationen. Apple erläutert dieses Verhalten unter Symbolsichtbarkeit steuern.
Verwende beim abschließenden Linken -exported_symbols_list, um die öffentliche Schnittstelle aufzulisten. Apples dokumentiertes Format für Exportlisten verwendet Linker-Symbolnamen, einschließlich des vorangestellten Unterstrichs bei C-Symbolen. Entscheide dich für eine Exportliste oder eine Liste nicht exportierter Symbole, statt beide Strategien zu kombinieren. Siehe Richtlinien zum Entwurf dynamischer Bibliotheken.
Bei einer kleinen Plug-in-Schnittstelle lässt sich eine explizite Exportliste leicht prüfen: Für jeden exportierten Namen muss es einen Grund geben, ihn öffentlich bereitzustellen. Halte Quellcodeannotationen und Liste konsistent: Die Annotation kennzeichnet die API im Code, und die Liste beschränkt die endgültige Binärdatei.
Ein vollständiges lokales Beispiel erstellen
Das Beispiel verwendet bewusst eine C-Abhängigkeit: Es veranschaulicht die Exportkontrolle für Mach-O, ohne inkompatible C++-Definitionen als portable Sprachmethode darzustellen. Die Abhängigkeit des Hosts gibt 10 zurück; der private Build des Plug-ins gibt 20 zurück. Diese Werte dienen der Veranschaulichung.
Führe die folgenden Blöcke der Reihe nach in derselben Terminal-Shell auf einem Mac mit verfügbaren Xcode Command Line Tools aus. Sie erstellen ein neues temporäres Verzeichnis und bauen für die Standardarchitektur der aktiven Toolchain. Das Beispiel wurde geprüft, aber während dieser Recherche nicht kompiliert oder ausgeführt, da die verfügbare Umgebung Linux ohne Apples Toolchain war.
Abhängigkeit und Plug-in erstellen
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
Der Header der Abhängigkeit gibt dem C++-Wrapper die korrekte Deklaration für die Sprachverknüpfung mit C. Auch der öffentliche Wrapper verwendet die C-Sprachverknüpfung, wodurch der Linkername dieses Einstiegspunkts einfach bleibt. C++-Namen werden in der Regel umgewandelt; Apple erläutert diesen Unterschied unter Symbolsichtbarkeit steuern.
Host erstellen
Der Host lädt das Plug-in und ruft seine öffentliche Funktion über den zurückgegebenen Bibliothekshandle ab. Apple dokumentiert die Unterstützung von dlopen für dynamische Bibliotheken und Mach-O-Bundles. Die Option RTLD_LOCAL begrenzt die Verfügbarkeit der exportierten Symbole des geladenen Abbilds; sie ersetzt keine Sichtbarkeitskontrollen zur Build-Zeit. Siehe Apples dlopen-Handbuch.
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
Kompilieren und linken
# 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
Die erwartete Ausgabe lautet host=10 plugin=20. Der Host exportiert sein Abhängigkeitssymbol ausdrücklich, damit das Beispiel eine extern sichtbare Implementierung mit demselben C-Namen enthält. Das Plug-in muss seine private Implementierung trotzdem enthalten.
Dies ist ein Prüfbeispiel und keine Behauptung, dass das Ergebnis zwangsläufig fehlschlägt, wenn die Kontrollen entfernt werden. Die standardmäßige Bindung von Mach-O kann Definitionen in vielen Anordnungen bereits unterscheiden. Entscheidend sind die ausgewählte Implementierung und die endgültige Exportoberfläche.
Exporte und Importe getrennt überprüfen
Führe diese Befehle im Beispielverzeichnis aus:
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
Im ersten Befehl wählt -g externe Symbole aus, -U schließt undefinierte Symbole aus und -j gibt Namen aus. Für dieses minimale Plug-in lautet der erwartete definierte externe Name _demo_plugin_value; _demo_dependency_value sollte nicht vorhanden sein. Apples nm-Handbuch dokumentiert diese Optionen und erläutert, dass sich die Symboltabelleninformationen von den Informationen unterscheiden können, die der Loader verwendet.
Verwende dyld_info -exports, um die für den Loader sichtbaren Exporte zu prüfen, und -imports, um die importierten Symbole zu untersuchen. Die Abhängigkeitsfunktion sollte in keiner der beiden Listen erscheinen: Sie ist privat in diesem Plug-in implementiert. Diese Prüfoptionen sind in Apples dyld_info-Handbuch dokumentiert.
Verwende otool -L, um die tatsächlichen Dylib-Abhängigkeiten zu prüfen. Das statische Archiv sollte nicht als separate Laufzeitabhängigkeit erscheinen. Apple dokumentiert diese Abhängigkeitsprüfung in den Richtlinien zur Verwendung dynamischer Bibliotheken.
Halte bei einem echten Plug-in drei Fragen auseinander: Ist die erforderliche Implementierung enthalten? Wird sie exportiert? Importiert ein Aufruf weiterhin eine konkurrierende Implementierung? Ein erfolgreicher Build beantwortet keine dieser Fragen für sich allein.
Dieselben Kontrollen in Xcode anwenden
Lege für das Ziel, das die endgültige Plug-in-Binärdatei erzeugt, unter Exported Symbols File (EXPORTED_SYMBOLS_FILE) den projektrelativen Pfad zur Exportliste fest. Aktiviere Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) für die C- und C++-Ziele, die die private Implementierung kompilieren. Apples aktuelle Referenz zu Build-Einstellungen dokumentiert beide Einstellungen.
Wenn die Abhängigkeit ein separates Ziel für eine statische Bibliothek ist, konfiguriere auch dieses Ziel. Werden nur die Compilereinstellungen des Plug-ins geändert, bleiben bereits kompilierte Abhängigkeitsobjekte unverändert. Baue die betroffenen Ziele neu und prüfe das resultierende Produkt für jede Architektur, die du verteilst.
Bei einem vorkompilierten Archiv ist die abschließende Exportliste weiterhin nützlich. Apples veröffentlichtes Linker-Handbuch dokumentiert außerdem -load_hidden, womit die globalen Symbole eines Archivs bei normaler Archivauswahl als verborgen behandelt werden. Dieses Handbuch gilt für ld-classic; prüfe die Dokumentation des installierten Linkers, bevor du diese zusätzliche Option auswählst. -force_load steuert die Einbindung von Archivmitgliedern und betrifft eine andere Fragestellung. Siehe das Linker-Handbuch von Apple.
Anforderungen an C++- und Swift-Schnittstellen berücksichtigen
C++-Sichtbarkeit behebt keine inkompatiblen Definitionen
Die One-Definition-Rule von C++ stellt Anforderungen an Definitionen über Übersetzungseinheiten hinweg, einschließlich Anforderungen an übereinstimmende Tokens und Namensauflösung, wenn mehrere Definitionen zulässig sind. Ein verborgenes Symbol berechtigt nicht dazu, widersprüchliche C++-Definitionen zu kombinieren. Verwende für unabhängig versionierte Implementierungen nach Möglichkeit unterschiedliche Namensräume oder Symbolnamen. Siehe die One-Definition-Rule im C++-Arbeitsentwurf.
Behalte außerdem die Sichtbarkeit bei, die eine beabsichtigte C++-Schnittstelle erfordert. Apple nennt RTTI über Bibliotheksgrenzen hinweg, Ausnahmen, dynamische Casts, die Adressidentität von Inline-Funktionen und gemeinsam genutzte statische Objekte als Fälle, die besondere Sorgfalt erfordern. Ein praktikabler Entwurf besteht darin, Abhängigkeitstypen und deren Besitz im Plug-in zu belassen und eine kleine Schnittstelle mit vereinbarten Skalarwerten oder undurchsichtigen Handles bereitzustellen. Siehe Symbolsichtbarkeit steuern.
Swift-Importzugriff steuert eine andere Grenze
Swifts internal import beschränkt, wo importierte Deklarationen referenziert werden können. SE-0409, implementiert in Swift 6.0, empfiehlt, Verwendungen von @_implementationOnly import durch einen internen Import oder eine niedrigere Zugriffsebene zu ersetzen. Es definiert keinen Mechanismus zum Umbenennen von Mach-O-Abhängigkeiten oder zum Filtern von Exporten. Konfiguriere und prüfe die nativen Binärdateien weiterhin separat. Siehe Zugriffsebenenmodifikatoren für Importdeklarationen.
Wenn du direkte Swift/C++-Interoperabilität verwendest, beachte deren Toolchain-Anforderungen: Die Unterstützung beginnt mit Swift 5.9, gemischter Code muss dieselbe C++-Standardbibliothek verwenden, und auf Apple-Plattformen kommt libc++ zum Einsatz. Importierte C++-Referenztypen erfordern außerdem eine macOS-Mindestversion von 13.3. Die Exportfilterung ändert nichts an diesen Anforderungen. Siehe Unterstützte Funktionen und Einschränkungen der C++-Interoperabilität.
Ein Plug-in ist für Integrationstests bereit, wenn seine vorgesehenen Einstiegspunkte verfügbar bleiben, seine privaten Abhängigkeitsdefinitionen in den Loader-Exporten fehlen und seine Importe den erwarteten Bibliotheken entsprechen. Ergänze diese Prüfungen um einen Verhaltenstest wie das synthetische Beispiel, damit spätere Abhängigkeitsaktualisierungen sowohl Änderungen an der Schnittstelle als auch an der ausgewählten Implementierung erkennen lassen.
Quellen
- Referenz zu Build-Einstellungen — Apple.
- Richtlinien zum Entwurf dynamischer Bibliotheken — Apple.
- Richtlinien zur Verwendung dynamischer Bibliotheken — Apple.
- Symbolsichtbarkeit steuern — Apple.
- Referenz zu Clang-Kommandozeilenargumenten — LLVM Project.
- Handbuch zu ld-classic(1) — Apple.
- Handbuch zu dlopen(3) — Apple.
- Handbuch zu nm(1) — Apple.
- Handbuch zu dyld_info(1) — Apple.
- One-Definition-Rule — C++-Arbeitsentwurf, wiedergegeben von Tim Song.
- Zugriffsebenenmodifikatoren für Importdeklarationen — Swift Project.
- Unterstützte Funktionen und Einschränkungen der C++-Interoperabilität — Swift.org.