Para mantener privada una dependencia incluida en un complemento nativo de Mac, compila su implementación con visibilidad oculta y exporta solo los puntos de entrada públicos del complemento durante el enlace final. Aplica estos controles al binario que realmente contiene la dependencia.

Esta guía está dirigida a desarrolladores de aplicaciones de escritorio para Mac que crean un complemento cargado en tiempo de ejecución e incluyen una dependencia de C o C++. La tarea concreta consiste en conservar la implementación propia del complemento y exponer una interfaz pequeña para el programa anfitrión. Un ejemplo sintético compila una dependencia de C dentro de un complemento de C++ y comprueba tanto el resultado como los símbolos exportados.

¿A qué binario hay que limitarle las exportaciones?

Empieza por identificar dónde acaba el código máquina de la dependencia. Un archivo estático proporciona archivos objeto al binario que se está enlazando; una biblioteca dinámica sigue siendo una imagen independiente. Por tanto, ocultar símbolos en un complemento no puede reescribir la tabla de exportaciones de una dylib de dependencia independiente. Apple describe esta diferencia en sus Directrices de uso de bibliotecas dinámicas.

Disposición de la dependencia Dónde aplicar el control Qué verificar
Código fuente de la dependencia compilado dentro del complemento Compilación de la dependencia y enlace final del complemento Las definiciones privadas están presentes, pero no se exportan
Archivo estático enlazado al complemento Compilación del archivo cuando sea posible y enlace final del complemento Se incluyeron los miembros necesarios del archivo; no aparecen exportaciones de la dependencia
Dylib de dependencia independiente Compilación propia de la dylib de dependencia Su interfaz pública y las importaciones registradas del complemento
Código de dependencia instanciado desde encabezados de C++ Todos los destinos que compilan esos encabezados Funciones inline emitidas, plantillas y datos asociados

Esta tabla sirve como ayuda para decidir, a partir de los límites de compilación y enlace. Por ejemplo, una lista de exportaciones asociada a un destino envoltorio no oculta automáticamente los símbolos emitidos en otro destino.

macOS también usa de forma predeterminada un espacio de nombres de dos niveles: las referencias resueltas contra una dylib registran qué biblioteca las proporcionó. El espacio de nombres plano cambia ese comportamiento. Las definiciones externas débiles introducen otra consideración, ya que el cargador puede fusionarlas. Apple documenta ambos mecanismos en su manual del enlazador. Por tanto, la coincidencia de nombres por sí sola no demuestra por qué un complemento llamó a una implementación inesperada.

Combinar la compilación con visibilidad oculta y una lista explícita de exportaciones

Los controles del compilador y del enlazador actúan en etapas distintas. Clang, con -fvisibility=hidden, establece la visibilidad predeterminada de las definiciones globales en los archivos fuente que se están compilando. No vuelve a compilar un archivo existente. Clang documenta por separado -fvisibility-inlines-hidden para las funciones miembro inline de C++; considera esa opción específica y evalúa si resulta adecuada según la interfaz. Consulta la referencia de la línea de comandos de Clang.

Marca los puntos de entrada públicos previstos con __attribute__((visibility("default"))). Los atributos de visibilidad pueden prevalecer sobre el valor predeterminado de compilación, así que inspecciona los encabezados de la dependencia en busca de anotaciones explícitas de exportación. Apple explica este comportamiento en Controlar la visibilidad de los símbolos.

En el enlace final, usa -exported_symbols_list para enumerar la interfaz pública. El formato documentado por Apple para las listas de exportación usa nombres de símbolos del enlazador, incluido el guion bajo inicial para los símbolos de C. Elige una lista de exportaciones o una lista de símbolos no exportados, en lugar de combinar ambas estrategias. Consulta las Directrices de diseño de bibliotecas dinámicas.

En una interfaz pequeña de complemento, una lista explícita de exportaciones facilita la revisión: cada nombre exportado debe tener una razón para ser público. Mantén coherentes la anotación en el código fuente y la lista; la anotación identifica la API en el código y la lista restringe el binario final.

Crear un ejemplo local completo

El ejemplo usa deliberadamente una dependencia de C: demuestra el control de exportaciones de Mach-O sin presentar definiciones de C++ incompatibles como una técnica de lenguaje portátil. La dependencia del anfitrión devuelve 10; la compilación privada del complemento devuelve 20. Estos valores son datos ilustrativos.

Ejecuta los siguientes bloques en orden, en la misma ventana de Terminal de un Mac con las herramientas de línea de comandos de Xcode disponibles. Crean un directorio temporal nuevo y compilan para la arquitectura predeterminada de la cadena de herramientas activa. Se inspeccionó el ejemplo, pero no se compiló ni ejecutó durante esta investigación porque el entorno disponible era Linux, sin las herramientas de Apple.

Crear la dependencia y el complemento

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

El encabezado de la dependencia proporciona al envoltorio de C++ la declaración correcta de enlace con lenguaje C. El envoltorio público también usa enlace con lenguaje C, lo que simplifica el nombre de enlace de este punto de entrada. Los nombres de C++ suelen requerir decoración de nombres; Apple analiza esa diferencia en Controlar la visibilidad de los símbolos.

Crear el anfitrión

El anfitrión carga el complemento y obtiene su función pública mediante el identificador de la biblioteca devuelto. Apple documenta que dlopen es compatible tanto con bibliotecas dinámicas como con paquetes Mach-O. Su opción RTLD_LOCAL limita la disponibilidad de los símbolos exportados de la imagen cargada; no sustituye los controles de visibilidad durante la compilación. Consulta el manual de dlopen de 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

Compilar y enlazar

# 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

El resultado esperado es host=10 plugin=20. El anfitrión exporta explícitamente el símbolo de su dependencia para que el ejemplo incluya una implementación visible externamente con el mismo nombre de C. El complemento debe seguir conteniendo su implementación privada.

Este es un ejemplo de verificación, no una afirmación de que eliminar los controles necesariamente haga fallar el resultado. El enlace predeterminado de Mach-O ya puede distinguir definiciones en muchas configuraciones. Las comprobaciones útiles son la implementación seleccionada y la superficie de exportación final.

Verificar las exportaciones e importaciones por separado

Ejecuta estos comandos en el directorio del ejemplo:

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

En el primer comando, -g selecciona los símbolos externos, -U excluye los símbolos indefinidos y -j imprime los nombres. Para este complemento mínimo, el nombre externo definido esperado es _demo_plugin_value; _demo_dependency_value debería estar ausente. El manual de nm de Apple documenta estas opciones y explica que la información de la tabla de símbolos puede diferir de la que usa el cargador.

Usa dyld_info -exports para inspeccionar las exportaciones visibles para el cargador y -imports para inspeccionar los símbolos importados. La función de la dependencia no debería aparecer en ninguna de las listas: está implementada de forma privada dentro de este complemento. Estas opciones de inspección están documentadas en el manual de dyld_info de Apple.

Usa otool -L para comprobar las dependencias reales de dylib. El archivo estático no debería aparecer como dependencia independiente en tiempo de ejecución. Apple documenta esta inspección de dependencias en las Directrices de uso de bibliotecas dinámicas.

En un complemento real, mantén separadas estas tres preguntas: ¿Está incluida la implementación necesaria? ¿Se exporta? ¿Hay alguna llamada que siga importando una implementación que compite con ella? Una compilación correcta no responde por sí sola a ninguna de estas preguntas.

Aplicar los mismos controles en Xcode

Para el destino que produce el binario final del complemento, establece Exported Symbols File (EXPORTED_SYMBOLS_FILE) con la ruta relativa al proyecto de la lista de exportaciones. Activa Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN) en los destinos de C y C++ que compilan la implementación privada. La referencia actual de ajustes de compilación de Apple documenta ambos ajustes.

Si la dependencia es un destino de biblioteca estática independiente, configúralo también. Cambiar solo los ajustes del compilador del complemento deja intactos los objetos de dependencia compilados previamente. Vuelve a compilar los destinos afectados e inspecciona el producto resultante para cada arquitectura que distribuyas.

Para un archivo precompilado, la lista final de exportaciones sigue siendo útil. El manual del enlazador publicado por Apple también documenta -load_hidden para tratar como ocultos los símbolos globales de un archivo, manteniendo la extracción normal del archivo. Ese manual corresponde a ld-classic; consulta la documentación del enlazador instalado antes de elegir esta opción adicional. -force_load controla la inclusión de miembros del archivo, una cuestión distinta. Consulta el manual del enlazador de Apple.

Conservar los requisitos de interfaz de C++ y Swift

La visibilidad de C++ no corrige definiciones incompatibles

La regla de una definición de C++ impone requisitos a las definiciones en distintas unidades de traducción, incluidos los requisitos de coincidencia de tokens y de resolución de nombres cuando se permiten varias definiciones. Ocultar un símbolo no da permiso para combinar definiciones de C++ en conflicto. Para implementaciones con versiones independientes, usa espacios de nombres o nombres de símbolos distintos cuando sea viable. Consulta la regla de una definición del borrador de trabajo de C++.

Conserva también la visibilidad necesaria para una interfaz de C++ intencional. Apple señala que los RTTI entre bibliotecas, las excepciones, las conversiones dinámicas, la identidad de direcciones de funciones inline y los objetos estáticos compartidos requieren atención. Un diseño práctico consiste en mantener los tipos y la gestión de propiedad de la dependencia dentro del complemento y exponer una interfaz pequeña que use valores escalares acordados o identificadores opacos. Consulta Controlar la visibilidad de los símbolos.

El control de acceso a importaciones de Swift regula otro límite

internal import en Swift restringe dónde se pueden usar las declaraciones importadas. SE-0409, implementado en Swift 6.0, recomienda reemplazar los usos de @_implementationOnly import por una importación interna o de nivel inferior. No define un mecanismo para cambiar el nombre de dependencias de Mach-O ni para filtrar exportaciones. Sigue configurando e inspeccionando los binarios nativos por separado. Consulta Modificadores de nivel de acceso en declaraciones de importación.

Si usas interoperabilidad directa entre Swift y C++, conserva las restricciones de la cadena de herramientas: la compatibilidad comienza con Swift 5.9, el código mixto debe usar la misma biblioteca estándar de C++ y las plataformas de Apple usan libc++. Los tipos de referencia de C++ importados también imponen una versión mínima de despliegue de macOS 13.3. El filtrado de exportaciones no cambia ninguno de estos requisitos. Consulta Funciones y restricciones compatibles de la interoperabilidad con C++.

Un complemento está listo para las pruebas de integración cuando sus puntos de entrada previstos siguen disponibles, sus definiciones privadas de dependencia están ausentes de las exportaciones del cargador y sus importaciones coinciden con las bibliotecas que debe usar. Mantén estas comprobaciones junto con una prueba de comportamiento como el ejemplo sintético, para que las actualizaciones posteriores de dependencias revelen tanto los cambios de interfaz como los cambios en la selección de implementaciones.

Fuentes