Pour garder privée une dépendance incluse dans un plug-in Mac natif, compilez son implémentation avec une visibilité masquée et n’exportez que les points d’entrée publics du plug-in lors de l’édition de liens finale. Appliquez ces contrôles au binaire qui contient réellement la dépendance.
Ce guide s’adresse aux développeurs d’applications Mac de bureau qui créent un plug-in chargé à l’exécution et incluant une dépendance C ou C++. L’objectif concret est de conserver l’implémentation propre au plug-in tout en exposant une petite interface destinée à l’application hôte. Un exemple synthétique intègre une dépendance C à un plug-in C++ et vérifie à la fois son résultat et ses symboles exportés.
Quel binaire doit avoir ses exports limités ?
Commencez par déterminer où se retrouve le code machine de la dépendance. Une archive statique fournit des fichiers objet au binaire en cours d’édition de liens ; une bibliothèque dynamique reste une image distincte. Par conséquent, masquer des symboles dans un plug-in ne permet pas de réécrire la table d’exportation d’une dylib de dépendance distincte. Apple décrit cette distinction dans ses Consignes d’utilisation des bibliothèques dynamiques.
| Organisation de la dépendance | Où appliquer le contrôle | Éléments à vérifier |
|---|---|---|
| Sources de la dépendance compilées dans le plug-in | Compilation de la dépendance et édition de liens finale du plug-in | Les définitions privées sont présentes, mais non exportées |
| Archive statique liée au plug-in | Compilation de l’archive si possible, ainsi qu’édition de liens finale du plug-in | Les membres nécessaires de l’archive ont été inclus ; les exports de la dépendance sont absents |
| Dylib de dépendance distincte | Compilation propre à la dylib de dépendance | Son interface publique et les imports enregistrés par le plug-in |
| Code de dépendance instancié à partir d’en-têtes C++ | Chaque cible qui compile ces en-têtes | Les fonctions inline émises, les modèles et les données associées |
Ce tableau est un outil d’aide à la décision fondé sur ces frontières de compilation et d’édition de liens. Par exemple, une liste d’exportation associée à une cible d’enrobage ne masque pas automatiquement les symboles émis dans une autre cible.
macOS utilise également par défaut un espace de noms à deux niveaux : les références résolues par rapport à une dylib enregistrent la bibliothèque qui les a fournies. L’espace de noms plat modifie ce comportement. Les définitions externes faibles constituent un autre facteur, car le chargeur peut les fusionner. Apple décrit ces deux mécanismes dans son manuel de l’éditeur de liens. La simple correspondance des noms ne permet donc pas de déterminer pourquoi un plug-in a appelé une implémentation inattendue.
Associer une compilation à visibilité masquée à une liste d’exportation explicite
Les contrôles du compilateur et de l’éditeur de liens interviennent à des étapes différentes. -fvisibility=hidden de Clang définit la visibilité par défaut des définitions globales dans les fichiers sources en cours de compilation. Il ne recompile pas une archive existante. Clang documente séparément -fvisibility-inlines-hidden pour les fonctions membres inline en C++ ; considérez cette option ciblée et vérifiez si elle convient à l’interface. Consultez la référence des options de ligne de commande de Clang.
Marquez les points d’entrée publics prévus avec __attribute__((visibility("default"))). Les attributs de visibilité peuvent remplacer la valeur par défaut de la compilation ; examinez donc les en-têtes de la dépendance pour repérer les annotations d’exportation explicites. Apple explique ce comportement dans Contrôle de la visibilité des symboles.
Lors de l’édition de liens finale, utilisez -exported_symbols_list pour énumérer l’interface publique. Le format de liste d’exportation documenté par Apple utilise les noms des symboles de l’éditeur de liens, avec notamment le trait de soulignement initial pour les symboles C. Choisissez une liste d’exportation ou une liste de symboles non exportés au lieu de combiner les deux stratégies. Consultez les Consignes de conception des bibliothèques dynamiques.
Pour une petite interface de plug-in, une liste d’exportation explicite facilite la revue : chaque nom exporté doit avoir une raison d’être public. Veillez à la cohérence entre l’annotation source et la liste ; l’annotation désigne l’API dans le code, et la liste limite le binaire final.
Créer un exemple local complet
L’exemple utilise volontairement une dépendance C : il montre le contrôle des exports Mach-O sans présenter des définitions C++ incompatibles comme une technique de langage portable. La dépendance de l’hôte renvoie 10 ; la compilation privée du plug-in renvoie 20. Ces valeurs sont données à titre d’exemple.
Exécutez les blocs suivants dans l’ordre, dans le même shell Terminal sur un Mac où les outils de ligne de commande Xcode sont disponibles. Ils créent un nouveau répertoire temporaire et compilent pour l’architecture par défaut de la chaîne d’outils active. L’exemple a été examiné, mais n’a pas été compilé ni exécuté lors de cette recherche, car l’environnement disponible était Linux et ne disposait pas de la chaîne d’outils Apple.
Créer la dépendance et le plug-in
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
L’en-tête de la dépendance fournit au wrapper C++ la déclaration de liaison correcte avec le langage C. Le wrapper public utilise également la liaison C, ce qui simplifie le nom de ce point d’entrée dans l’éditeur de liens. Les noms C++ nécessitent généralement une décoration ; Apple explique cette distinction dans Contrôle de la visibilité des symboles.
Créer l’hôte
L’hôte charge le plug-in et obtient sa fonction publique à partir du descripteur de bibliothèque renvoyé. Apple documente la prise en charge de dlopen pour les bibliothèques dynamiques comme pour les bundles Mach-O. L’option RTLD_LOCAL limite la disponibilité des symboles exportés par l’image chargée ; elle ne remplace pas les contrôles de visibilité à la compilation. Consultez le manuel dlopen d’Apple.
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
Compiler et lier
# 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
La sortie attendue est host=10 plugin=20. L’hôte exporte explicitement son symbole de dépendance afin que l’exemple inclue une implémentation visible de l’extérieur portant le même nom C. Le plug-in doit tout de même contenir son implémentation privée.
Il s’agit d’un exemple de vérification, et non de l’affirmation que la suppression des contrôles entraînerait nécessairement un échec. Dans de nombreuses configurations, la liaison Mach-O par défaut distingue déjà les définitions. Les vérifications utiles portent sur l’implémentation sélectionnée et la surface d’exportation finale.
Vérifier séparément les exports et les imports
Exécutez ces commandes dans le répertoire de l’exemple :
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
Dans la première commande, -g sélectionne les symboles externes, -U exclut les symboles non définis et -j affiche les noms. Pour ce plug-in minimal, le nom externe défini attendu est _demo_plugin_value ; _demo_dependency_value doit être absent. Le manuel nm d’Apple documente ces options et explique que les informations de la table des symboles peuvent différer de celles utilisées par le chargeur.
Utilisez dyld_info -exports pour examiner les exports visibles par le chargeur et -imports pour examiner les symboles importés. La fonction de dépendance ne devrait figurer dans aucune de ces listes : elle est implémentée en privé dans ce plug-in. Ces options d’inspection sont documentées dans le manuel dyld_info d’Apple.
Utilisez otool -L pour vérifier les dépendances dylib réelles. L’archive statique ne doit pas apparaître comme dépendance d’exécution distincte. Apple documente l’inspection de ces dépendances dans ses Consignes d’utilisation des bibliothèques dynamiques.
Pour un vrai plug-in, examinez séparément trois questions : l’implémentation nécessaire est-elle incluse ? Est-elle exportée ? Un appel importe-t-il encore une implémentation concurrente ? Une compilation réussie ne répond à aucune de ces questions à elle seule.
Appliquer les mêmes contrôles dans Xcode
Pour la cible qui produit le binaire final du plug-in, définissez Exported Symbols File (EXPORTED_SYMBOLS_FILE) sur le chemin relatif au projet de la liste d’exportation. Activez Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) sur les cibles C et C++ qui compilent l’implémentation privée. La Référence des réglages de compilation actuelle d’Apple documente ces deux réglages.
Si la dépendance est une cible de bibliothèque statique distincte, configurez-la également. Modifier uniquement les réglages du compilateur du plug-in ne change pas les fichiers objet de la dépendance déjà compilés. Recompilez les cibles concernées et inspectez le produit obtenu pour chaque architecture distribuée.
Pour une archive précompilée, la liste d’exportation finale reste utile. Le manuel de l’éditeur de liens publié par Apple documente également -load_hidden, qui traite les symboles globaux d’une archive comme masqués tout en conservant l’extraction normale de l’archive. Ce manuel concerne ld-classic ; consultez la documentation de l’éditeur de liens installé avant de choisir cette option supplémentaire. -force_load contrôle l’inclusion des membres de l’archive, une question distincte. Consultez le manuel de l’éditeur de liens d’Apple.
Préserver les exigences des interfaces C++ et Swift
La visibilité C++ ne corrige pas les définitions incompatibles
La règle de définition unique du C++ impose des exigences aux définitions présentes dans plusieurs unités de traduction, notamment des exigences relatives à la correspondance des jetons et à la résolution des noms lorsque plusieurs définitions sont autorisées. Masquer un symbole n’autorise pas à combiner des définitions C++ incompatibles. Pour des implémentations gérées indépendamment par version, utilisez si possible des espaces de noms ou des noms de symboles distincts. Consultez la règle de définition unique du brouillon de travail C++.
Préservez également la visibilité nécessaire à une interface C++ voulue. Apple signale que les RTTI entre bibliothèques, les exceptions, les conversions dynamiques, l’identité d’adresse des fonctions inline et les objets statiques partagés exigent une attention particulière. Une conception pratique consiste à conserver les types et la propriété de la dépendance à l’intérieur du plug-in, et à exposer une petite interface fondée sur des valeurs scalaires convenues ou des handles opaques. Consultez Contrôle de la visibilité des symboles.
Le contrôle d’accès à l’importation Swift agit sur une autre frontière
Dans Swift, internal import limite les endroits où les déclarations importées peuvent être référencées. SE-0409, implémentée dans Swift 6.0, recommande de remplacer les utilisations de @_implementationOnly import par un import interne ou d’un niveau inférieur. Cette fonctionnalité ne définit pas de mécanisme de renommage des dépendances Mach-O ni de filtrage des exports. Continuez à configurer et à inspecter séparément les binaires natifs. Consultez Modificateurs de niveau d’accès sur les déclarations d’importation.
Si vous utilisez directement l’interopérabilité Swift/C++, respectez ses contraintes d’outillage : la prise en charge débute avec Swift 5.9, le code mixte doit utiliser la même bibliothèque standard C++, et les plateformes Apple utilisent libc++. Les types référence C++ importés imposent également une version minimale de macOS 13.3 comme cible de déploiement. Le filtrage des exports ne change aucune de ces exigences. Consultez Fonctionnalités prises en charge et contraintes de l’interopérabilité C++.
Un plug-in est prêt pour les tests d’intégration lorsque ses points d’entrée prévus restent disponibles, que les définitions de sa dépendance privée sont absentes des exports du chargeur et que ses imports correspondent aux bibliothèques qu’il est censé utiliser. Conservez ces vérifications avec un test comportemental comme l’exemple synthétique afin que les mises à jour ultérieures des dépendances révèlent à la fois les changements d’interface et les changements d’implémentation sélectionnée.
Sources
- Référence des réglages de compilation — Apple.
- Consignes de conception des bibliothèques dynamiques — Apple.
- Consignes d’utilisation des bibliothèques dynamiques — Apple.
- Contrôle de la visibilité des symboles — Apple.
- Référence des arguments de ligne de commande de Clang — Projet LLVM.
- Manuel ld-classic(1) — Apple.
- Manuel dlopen(3) — Apple.
- Manuel nm(1) — Apple.
- Manuel dyld_info(1) — Apple.
- Règle de définition unique — Brouillon de travail C++, rendu par Tim Song.
- Modificateurs de niveau d’accès sur les déclarations d’importation — Projet Swift.
- Fonctionnalités prises en charge et contraintes de l’interopérabilité C++ — Swift.org.