Para manter privada uma dependência incluída em um plug-in nativo para Mac, compile sua implementação com visibilidade oculta e exporte apenas os pontos de entrada públicos do plug-in na etapa final de vinculação. Aplique esses controles ao binário que realmente contém a dependência.
Este guia destina-se a um desenvolvedor de aplicativos para desktop do Mac que está criando um plug-in carregado em tempo de execução e que inclui uma dependência em C ou C++. A tarefa concreta é manter a implementação própria do plug-in e, ao mesmo tempo, expor uma pequena interface voltada ao host. Um exemplo sintético compila uma dependência em C dentro de um plug-in em C++ e verifica tanto seu resultado quanto os símbolos exportados.
Qual binário precisa ter suas exportações limitadas?
Comece identificando onde o código de máquina da dependência fica. Um arquivo estático fornece arquivos-objeto ao binário que está sendo vinculado; uma biblioteca dinâmica permanece uma imagem separada. Portanto, ocultar símbolos em um plug-in não pode reescrever a tabela de exportações de uma dylib de dependência separada. A Apple descreve essa distinção em suas Diretrizes de uso de bibliotecas dinâmicas.
| Organização da dependência | Onde aplicar o controle | O que verificar |
|---|---|---|
| Código-fonte da dependência compilado no plug-in | Compilação da dependência e vinculação final do plug-in | As definições privadas estão presentes, mas não são exportadas |
| Arquivo estático vinculado ao plug-in | Compilação do arquivo quando possível, além da vinculação final do plug-in | Os membros necessários do arquivo foram incluídos; as exportações da dependência estão ausentes |
| Dylib de dependência separada | Compilação da própria dylib de dependência | Sua interface pública e as importações registradas pelo plug-in |
| Código da dependência instanciado a partir de cabeçalhos C++ | Cada destino que compila esses cabeçalhos | Funções inline, modelos e dados associados emitidos |
Esta tabela é um auxílio à decisão derivado desses limites de compilação e vinculação. Por exemplo, uma lista de exportações anexada a um destino de wrapper não oculta automaticamente os símbolos emitidos em outro destino.
O macOS também usa, por padrão, um namespace de dois níveis: as referências resolvidas em relação a uma dylib registram qual biblioteca as forneceu. O namespace plano altera esse comportamento. Definições externas fracas introduzem outra consideração, pois o carregador pode unificá-las. A Apple documenta os dois mecanismos em seu manual do vinculador. Portanto, a correspondência entre nomes, por si só, não esclarece por que um plug-in chamou uma implementação inesperada.
Combine compilação oculta com uma lista explícita de exportações
Os controles do compilador e do vinculador atuam em etapas diferentes. -fvisibility=hidden do Clang define a visibilidade padrão das definições globais nos arquivos de origem que estão sendo compilados. Ele não recompila um arquivo existente. O Clang documenta separadamente -fvisibility-inlines-hidden para funções-membro inline em C++; trate essa opção como específica, cuja adequação depende da interface. Consulte a referência de linha de comando do Clang.
Marque os pontos de entrada públicos pretendidos com __attribute__((visibility("default"))). Atributos de visibilidade podem substituir o padrão de compilação, portanto verifique se os cabeçalhos da dependência contêm anotações explícitas de exportação. A Apple explica esse comportamento em Controlando a visibilidade de símbolos.
Na vinculação final, use -exported_symbols_list para enumerar a interface pública. O formato documentado pela Apple para listas de exportação usa nomes de símbolos do vinculador, incluindo o sublinhado inicial para símbolos C. Escolha uma lista de exportações ou uma lista de símbolos não exportados, em vez de combinar as duas estratégias. Consulte Diretrizes de projeto de bibliotecas dinâmicas.
Para uma interface pequena de plug-in, uma lista explícita de exportações facilita a revisão: cada nome exportado precisa ter uma justificativa para ser público. Mantenha a anotação no código e a lista consistentes; a anotação identifica a API no código, e a lista restringe o binário final.
Crie um exemplo local completo
O exemplo usa deliberadamente uma dependência em C: ele demonstra o controle de exportações de Mach-O sem apresentar definições incompatíveis em C++ como uma técnica de linguagem portátil. A dependência do host retorna 10; a compilação privada do plug-in retorna 20. Esses valores são entradas ilustrativas.
Execute os blocos a seguir na ordem, no mesmo shell do Terminal, em um Mac com as ferramentas de linha de comando do Xcode disponíveis. Eles criam um diretório temporário novo e compilam para a arquitetura padrão do conjunto de ferramentas ativo. O exemplo foi inspecionado, mas não foi compilado nem executado durante esta pesquisa porque o ambiente disponível era Linux, sem as ferramentas da Apple.
Crie a dependência e o 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
O cabeçalho da dependência fornece ao wrapper em C++ a declaração correta de vinculação à linguagem C. O wrapper público também usa vinculação C, o que mantém simples o nome de vinculação deste ponto de entrada. Em geral, os nomes C++ exigem decoração; a Apple discute essa distinção em Controlando a visibilidade de símbolos.
Crie o host
O host carrega o plug-in e obtém sua função pública pelo identificador da biblioteca retornado. A Apple documenta o suporte de dlopen tanto para bibliotecas dinâmicas quanto para bundles Mach-O. Sua opção RTLD_LOCAL limita a disponibilidade dos símbolos exportados da imagem carregada; ela não substitui os controles de visibilidade em tempo de compilação. Consulte o manual de dlopen da 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
Compile e vincule
# 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
A saída esperada é host=10 plugin=20. O host exporta explicitamente o símbolo da dependência para que o exemplo inclua uma implementação visível externamente com o mesmo nome C. Ainda assim, o plug-in precisa conter sua implementação privada.
Este é um exemplo de verificação, não uma afirmação de que remover os controles necessariamente fará o resultado falhar. A vinculação padrão do Mach-O já consegue distinguir definições em muitas configurações. As verificações úteis são a implementação selecionada e a superfície de exportação final.
Verifique exportações e importações separadamente
Execute estes comandos no diretório do exemplo:
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
No primeiro comando, -g seleciona símbolos externos, -U exclui símbolos indefinidos e -j exibe os nomes. Para este plug-in mínimo, o nome externo definido esperado é _demo_plugin_value; _demo_dependency_value deve estar ausente. O manual de nm da Apple documenta essas opções e explica que as informações da tabela de símbolos podem diferir das informações usadas pelo carregador.
Use dyld_info -exports para inspecionar as exportações visíveis ao carregador e -imports para inspecionar os símbolos importados. A função da dependência não deve aparecer em nenhuma das listas: ela é implementada privadamente dentro deste plug-in. Essas opções de inspeção estão documentadas no manual de dyld_info da Apple.
Use otool -L para verificar as dependências reais de dylibs. O arquivo estático não deve aparecer como uma dependência separada em tempo de execução. A Apple documenta essa inspeção de dependências nas Diretrizes de uso de bibliotecas dinâmicas.
Para um plug-in real, mantenha separadas três perguntas: A implementação necessária foi incluída? Ela é exportada? Alguma chamada ainda importa uma implementação concorrente? Uma compilação bem-sucedida, por si só, não responde a nenhuma dessas perguntas.
Aplique os mesmos controles no Xcode
Para o destino que produz o binário final do plug-in, defina Exported Symbols File (EXPORTED_SYMBOLS_FILE) como o caminho relativo ao projeto da lista de exportações. Ative Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) nos destinos C e C++ que compilam a implementação privada. A Referência atual de configurações de compilação da Apple documenta ambas as configurações.
Se a dependência for um destino de biblioteca estática separado, configure esse destino também. Alterar apenas as configurações do compilador do plug-in deixa intactos os objetos de dependência compilados anteriormente. Recompile os destinos afetados e inspecione o produto resultante para cada arquitetura que você distribui.
Para um arquivo pré-compilado, a lista final de exportações continua sendo útil. O manual do vinculador publicado pela Apple também documenta -load_hidden para tratar os símbolos globais de um arquivo como ocultos enquanto usa a extração normal do arquivo. Esse manual é para ld-classic; consulte a documentação do vinculador instalado antes de escolher essa opção adicional. -force_load controla a inclusão de membros do arquivo, que é uma questão separada. Consulte o manual do vinculador da Apple.
Preserve os requisitos das interfaces C++ e Swift
A visibilidade de C++ não corrige definições incompatíveis
A regra de definição única de C++ impõe requisitos às definições entre unidades de tradução, incluindo requisitos de tokens correspondentes e de consulta de nomes quando várias definições são permitidas. Ocultar um símbolo não dá permissão para combinar definições C++ conflitantes. Para implementações com versões independentes, use namespaces ou nomes de símbolos distintos sempre que possível. Consulte a regra de definição única do rascunho de trabalho de C++.
Preserve também a visibilidade necessária para uma interface C++ intencional. A Apple destaca que exigem cuidado o RTTI entre bibliotecas, as exceções, os casts dinâmicos, a identidade de endereço de funções inline e os objetos estáticos compartilhados. Uma abordagem prática é manter os tipos e a propriedade da dependência dentro do plug-in e expor uma pequena interface que use valores escalares acordados ou identificadores opacos. Consulte Controlando a visibilidade de símbolos.
O controle de acesso à importação do Swift estabelece outro limite
internal import do Swift restringe os locais em que as declarações importadas podem ser referenciadas. A SE-0409, implementada no Swift 6.0, recomenda substituir o uso de @_implementationOnly import por uma importação internal ou de nível inferior. Ela não define um mecanismo de renomeação de dependências ou filtragem de exportações do Mach-O. Continue configurando e inspecionando os binários nativos separadamente. Consulte Modificadores de nível de acesso em declarações de importação.
Se usar a interoperabilidade direta entre Swift e C++, mantenha as restrições das ferramentas: o suporte começa no Swift 5.9, o código misto precisa usar a mesma biblioteca padrão de C++ e as plataformas da Apple usam libc++. Tipos de referência C++ importados também impõem uma versão mínima de implantação do macOS 13.3. A filtragem de exportações não altera nenhum desses requisitos. Consulte Recursos compatíveis e restrições da interoperabilidade de C++.
Um plug-in está pronto para testes de integração quando seus pontos de entrada pretendidos continuam disponíveis, suas definições privadas de dependências estão ausentes das exportações visíveis ao carregador e suas importações correspondem às bibliotecas que deveria usar. Mantenha essas verificações junto a um teste comportamental como o exemplo sintético, para que atualizações posteriores de dependências revelem tanto mudanças na interface quanto mudanças na seleção da implementação.
Fontes
- Referência de configurações de compilação — Apple.
- Diretrizes de projeto de bibliotecas dinâmicas — Apple.
- Diretrizes de uso de bibliotecas dinâmicas — Apple.
- Controlando a visibilidade de símbolos — Apple.
- Referência de argumentos de linha de comando do Clang — Projeto LLVM.
- Manual de ld-classic(1) — Apple.
- Manual de dlopen(3) — Apple.
- Manual de nm(1) — Apple.
- Manual de dyld_info(1) — Apple.
- Regra de definição única — rascunho de trabalho de C++, renderizado por Tim Song.
- Modificadores de nível de acesso em declarações de importação — Projeto Swift.
- Recursos compatíveis e restrições da interoperabilidade de C++ — Swift.org.