Aby zachować prywatność dołączonej zależności natywnej wtyczki na Maca, skompiluj jej implementację z ukrytą widocznością i eksportuj tylko publiczne punkty wejścia wtyczki podczas końcowego łączenia. Zastosuj te ustawienia do pliku binarnego, który faktycznie zawiera zależność.

Ten przewodnik jest przeznaczony dla dewelopera aplikacji desktopowych na Maca, który tworzy wtyczkę ładowaną w czasie działania i dołącza do niej zależność w C lub C++. Celem jest zachowanie własnej implementacji wtyczki przy jednoczesnym udostępnieniu niewielkiego interfejsu dla aplikacji hosta. Przykład oparty na sztucznych danych kompiluje zależność w C do wtyczki w C++ i sprawdza zarówno jej wynik, jak i eksportowane symbole.

W którym pliku binarnym należy ograniczyć eksporty?

Zacznij od ustalenia, gdzie trafia kod maszynowy zależności. Archiwum statyczne dostarcza pliki obiektowe do łączonego pliku binarnego, natomiast biblioteka dynamiczna pozostaje osobnym obrazem. W związku z tym ukrycie symboli we wtyczce nie może zmienić tablicy eksportów osobnej biblioteki dylib. Apple opisuje tę różnicę w dokumencie Wytyczne dotyczące korzystania z bibliotek dynamicznych.

Sposób dołączenia zależności Gdzie zastosować ustawienie Co sprawdzić
Kod źródłowy zależności skompilowany do wtyczki Kompilacja zależności i końcowe łączenie wtyczki Prywatne definicje są obecne, ale nie są eksportowane
Archiwum statyczne połączone z wtyczką Kompilacja archiwum, jeśli to możliwe, oraz końcowe łączenie wtyczki Wymagane elementy archiwum zostały dołączone; eksporty zależności są nieobecne
Osobna biblioteka dylib zależności Kompilacja samej biblioteki dylib Jej publiczny interfejs oraz odnotowane importy wtyczki
Kod zależności tworzony z nagłówków C++ Każdy cel kompilujący te nagłówki Wygenerowane funkcje inline, szablony i powiązane dane

Ta tabela pomaga podjąć decyzję na podstawie granic kompilacji i łączenia. Na przykład lista eksportów przypisana do celu opakowującego nie ukrywa automatycznie symboli wygenerowanych w innym celu.

macOS domyślnie używa także przestrzeni nazw dwupoziomowej: odwołania rozwiązywane względem biblioteki dylib zapisują, która biblioteka je dostarczyła. Płaska przestrzeń nazw zmienia to zachowanie. Słabe definicje zewnętrzne są dodatkowym czynnikiem, ponieważ program ładujący może je scalać. Apple opisuje oba mechanizmy w swoim podręczniku linkera. Sama zgodność nazw nie wyjaśnia więc, dlaczego wtyczka wywołała nieoczekiwaną implementację.

Połącz ukrytą widoczność podczas kompilacji z jawną listą eksportów

Ustawienia kompilatora i linkera działają na różnych etapach. Opcja Clanga -fvisibility=hidden ustawia domyślną widoczność definicji globalnych w kompilowanych plikach źródłowych. Nie kompiluje ponownie istniejącego archiwum. Clang osobno dokumentuje opcję -fvisibility-inlines-hidden dla inline’owych metod składowych C++; traktuj ją jako opcję o konkretnym zastosowaniu, której przydatność zależy od interfejsu. Zobacz dokumentację opcji wiersza poleceń Clanga.

Oznacz zamierzone publiczne punkty wejścia za pomocą __attribute__((visibility("default"))). Atrybuty widoczności mogą zastąpić domyślne ustawienie kompilacji, dlatego sprawdź nagłówki zależności pod kątem jawnych adnotacji eksportu. Apple wyjaśnia to działanie w dokumencie Kontrolowanie widoczności symboli.

Podczas końcowego łączenia użyj -exported_symbols_list, aby wyliczyć publiczny interfejs. Udokumentowany przez Apple format listy eksportów używa nazw symboli linkera, w tym początkowego podkreślenia dla symboli C. Wybierz listę eksportów albo listę symboli nieeksportowanych, zamiast łączyć obie strategie. Zobacz Wytyczne dotyczące projektowania bibliotek dynamicznych.

W przypadku niewielkiego interfejsu wtyczki jawna lista eksportów ułatwia przegląd: każda eksportowana nazwa powinna mieć uzasadnienie jako element publiczny. Zachowaj zgodność adnotacji w kodzie z listą: adnotacja wskazuje API w kodzie, a lista ogranicza końcowy plik binarny.

Zbuduj kompletny przykład lokalny

Przykład celowo wykorzystuje zależność w C: pokazuje kontrolę eksportu Mach-O bez przedstawiania niezgodnych definicji C++ jako przenośnej techniki językowej. Zależność hosta zwraca 10, a prywatna kompilacja wtyczki — 20. Te wartości są przykładowymi danymi wejściowymi.

Uruchom poniższe bloki po kolei w tej samej powłoce Terminala na Macu z dostępnymi narzędziami wiersza poleceń Xcode. Utworzą nowy katalog tymczasowy i zbudują program dla domyślnej architektury aktywnego łańcucha narzędzi. Przykład został przeanalizowany, ale nie został skompilowany ani uruchomiony w ramach tych badań, ponieważ dostępne środowisko działało pod Linuksem i nie miało łańcucha narzędzi Apple.

Utwórz zależność i wtyczkę

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

Nagłówek zależności udostępnia opakowaniu C++ deklarację z prawidłowym wiązaniem języka C. Publiczne opakowanie również używa wiązania C, dzięki czemu nazwa tego punktu wejścia w linkerze jest prosta. Nazwy C++ zwykle wymagają manglingu; Apple omawia tę różnicę w dokumencie Kontrolowanie widoczności symboli.

Utwórz hosta

Host ładuje wtyczkę i pobiera jej publiczną funkcję za pomocą zwróconego uchwytu biblioteki. Apple dokumentuje obsługę dlopen zarówno dla bibliotek dynamicznych, jak i pakietów Mach-O. Opcja RTLD_LOCAL ogranicza dostępność eksportowanych symboli załadowanego obrazu; nie zastępuje ustawień widoczności stosowanych podczas kompilacji. Zobacz podręcznik Apple dotyczący 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

Skompiluj i połącz

# 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

Oczekiwany wynik to host=10 plugin=20. Host jawnie eksportuje swój symbol zależności, aby przykład obejmował zewnętrznie widoczną implementację o tej samej nazwie C. Wtyczka nadal musi zawierać swoją prywatną implementację.

To przykład weryfikacyjny, a nie twierdzenie, że usunięcie tych ustawień musi spowodować błędny wynik. Domyślne wiązanie Mach-O może już rozróżniać definicje w wielu konfiguracjach. Przydatne kontrole dotyczą wybranej implementacji i końcowej powierzchni eksportu.

Osobno sprawdź eksporty i importy

Uruchom te polecenia w katalogu przykładu:

xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib

W pierwszym poleceniu -g wybiera symbole zewnętrzne, -U wyklucza symbole niezdefiniowane, a -j wyświetla nazwy. W przypadku tej minimalnej wtyczki oczekiwaną zdefiniowaną nazwą zewnętrzną jest _demo_plugin_value; _demo_dependency_value powinno być nieobecne. Opcje te opisano w podręczniku Apple dotyczącym nm, który wyjaśnia też, że informacje tablicy symboli mogą różnić się od informacji używanych przez program ładujący.

Użyj dyld_info -exports, aby sprawdzić eksporty widoczne dla programu ładującego, i -imports, aby sprawdzić importowane symbole. Funkcja zależności nie powinna pojawić się na żadnej z tych list: jej implementacja znajduje się prywatnie wewnątrz tej wtyczki. Opcje inspekcji opisano w podręczniku Apple dotyczącym dyld_info.

Użyj otool -L, aby sprawdzić rzeczywiste zależności od bibliotek dylib. Archiwum statyczne nie powinno pojawić się jako osobna zależność środowiska wykonawczego. Apple opisuje sprawdzanie tych zależności w dokumencie Wytyczne dotyczące korzystania z bibliotek dynamicznych.

W przypadku rzeczywistej wtyczki rozpatruj osobno trzy pytania: Czy wymagana implementacja została dołączona? Czy jest eksportowana? Czy jakiekolwiek wywołanie nadal importuje konkurencyjną implementację? Sam udany proces kompilacji nie odpowiada na żadne z nich.

Zastosuj te same ustawienia w Xcode

Dla celu tworzącego końcowy plik binarny wtyczki ustaw Exported Symbols File (EXPORTED_SYMBOLS_FILE) na ścieżkę listy eksportów względem projektu. Włącz Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) w celach C i C++, które kompilują prywatną implementację. Oba ustawienia opisano w aktualnym dokumencie Apple o ustawieniach kompilacji.

Jeśli zależność jest osobnym celem biblioteki statycznej, skonfiguruj również ten cel. Zmiana wyłącznie ustawień kompilatora wtyczki nie zmienia wcześniej skompilowanych obiektów zależności. Ponownie zbuduj zmodyfikowane cele i sprawdź otrzymany produkt dla każdej dystrybuowanej architektury.

W przypadku gotowego archiwum lista eksportów nadal jest przydatna. Opublikowany przez Apple podręcznik linkera opisuje również -load_hidden, które traktuje symbole globalne archiwum jako ukryte przy zwykłym dołączaniu elementów archiwum. Podręcznik dotyczy ld-classic; przed wybraniem tej dodatkowej opcji sprawdź dokumentację zainstalowanego linkera. -force_load steruje dołączaniem elementów archiwum, co jest odrębną kwestią. Zobacz podręcznik linkera Apple.

Zachowaj wymagania interfejsów C++ i Swift

Widoczność C++ nie naprawia niezgodnych definicji

Reguła jednej definicji w C++ nakłada wymagania na definicje w jednostkach translacji, w tym na zgodność tokenów i wyszukiwania nazw, gdy dozwolonych jest wiele definicji. Ukrycie symbolu nie daje prawa do łączenia sprzecznych definicji C++. W przypadku niezależnie wersjonowanych implementacji używaj, jeśli to możliwe, odrębnych przestrzeni nazw lub nazw symboli. Zobacz regułę jednej definicji w roboczym projekcie standardu C++.

Zachowaj też widoczność wymaganą przez zamierzony interfejs C++. Apple wskazuje, że ostrożności wymagają RTTI między bibliotekami, wyjątki, rzutowania dynamiczne, tożsamość adresów funkcji inline oraz współdzielone obiekty statyczne. Praktyczne rozwiązanie polega na utrzymaniu typów zależności i zarządzania ich czasem życia wewnątrz wtyczki oraz udostępnieniu niewielkiego interfejsu korzystającego z uzgodnionych wartości skalarnych lub nieprzezroczystych uchwytów. Zobacz Kontrolowanie widoczności symboli.

Kontrola dostępu do importów Swift dotyczy innej granicy

Swiftowe internal import ogranicza miejsca, w których można odwoływać się do zaimportowanych deklaracji. SE-0409, wdrożone w Swift 6.0, zaleca zastąpienie użyć @_implementationOnly import importem wewnętrznym lub o niższym poziomie dostępu. Nie definiuje mechanizmu zmiany nazw zależności Mach-O ani filtrowania eksportów. Nadal osobno konfiguruj i sprawdzaj pliki binarne natywne. Zobacz Modyfikatory poziomu dostępu w deklaracjach importu.

Jeśli używasz bezpośredniej współpracy Swift/C++, zachowaj jej ograniczenia łańcucha narzędzi: obsługa zaczyna się od Swift 5.9, kod mieszany musi korzystać z tej samej biblioteki standardowej C++, a platformy Apple używają libc++. Importowane typy referencyjne C++ wymagają także minimalnej wersji docelowej macOS 13.3. Filtrowanie eksportów nie zmienia żadnego z tych wymagań. Zobacz Obsługiwane funkcje i ograniczenia współpracy C++.

Wtyczka jest gotowa do testów integracyjnych, gdy jej zamierzone punkty wejścia pozostają dostępne, prywatne definicje zależności nie występują w eksportach widocznych dla programu ładującego, a importy odpowiadają bibliotekom, z których wtyczka ma korzystać. Połącz te kontrole z testem zachowania, takim jak przykład oparty na sztucznych danych, aby późniejsze aktualizacje zależności ujawniały zarówno zmiany interfejsu, jak i zmiany w wyborze implementacji.

Źródła