네이티브 Mac 플러그인에 번들로 포함된 종속성을 비공개로 유지하려면 구현을 숨김 가시성으로 컴파일하고, 최종 링크 단계에서 플러그인의 공개 진입점만 내보내세요. 이러한 제어는 종속성이 실제로 포함된 바이너리에 적용하세요.
이 가이드는 C 또는 C++ 종속성을 포함하는 런타임 로드 플러그인을 만드는 Mac 데스크톱 개발자를 위한 것입니다. 구체적인 목표는 플러그인 자체 구현은 유지하면서 호스트에 공개하는 인터페이스를 작게 만드는 것입니다. 예제에서는 C 종속성을 C++ 플러그인에 빌드하고, 결과와 내보낸 심볼을 모두 확인합니다.
내보내기를 제한해야 하는 바이너리는 무엇인가요?
먼저 종속성의 기계어 코드가 어디에 들어가는지 확인하세요. 정적 아카이브는 링크 대상 바이너리에 오브젝트 파일을 제공하고, 동적 라이브러리는 별도의 이미지로 남습니다. 따라서 플러그인에서 심볼을 숨겨도 별도 종속성 dylib의 내보내기 테이블을 다시 작성할 수는 없습니다. Apple은 Dynamic Library Usage Guidelines에서 이 차이를 설명합니다.
| 종속성 구성 | 제어를 적용할 위치 | 확인할 사항 |
|---|---|---|
| 플러그인에 컴파일되는 종속성 소스 | 종속성 컴파일 및 최종 플러그인 링크 | 비공개 정의는 포함되어 있지만 내보내지 않음 |
| 플러그인에 링크되는 정적 아카이브 | 가능한 경우 아카이브 컴파일과 최종 플러그인 링크 | 필요한 아카이브 멤버가 포함됨, 종속성 내보내기가 없음 |
| 별도 종속성 dylib | 종속성 dylib 자체의 빌드 | 공개 인터페이스와 플러그인에 기록된 임포트 |
| C++ 헤더에서 인스턴스화되는 종속성 코드 | 해당 헤더를 컴파일하는 모든 대상 | 생성된 인라인 함수, 템플릿 및 관련 데이터 |
이 표는 컴파일 및 링크 경계를 바탕으로 한 의사 결정 도구입니다. 예를 들어 래퍼 대상에 지정한 내보내기 목록이 다른 대상에 생성된 심볼까지 자동으로 숨기지는 않습니다.
macOS는 기본적으로 2단계 네임스페이스도 사용합니다. dylib에 대해 확인된 참조에는 해당 심볼을 제공한 라이브러리가 기록됩니다. 플랫 네임스페이스는 이 동작을 바꿉니다. 약한 외부 정의는 로더가 이를 병합할 수 있으므로 또 다른 고려 사항입니다. Apple은 두 메커니즘을 링커 매뉴얼에 설명합니다. 따라서 이름이 일치한다는 사실만으로 플러그인이 예상치 못한 구현을 호출한 이유를 알 수는 없습니다.
숨김 컴파일과 명시적 내보내기 목록을 함께 사용하세요
컴파일러 제어와 링커 제어는 서로 다른 단계에서 작동합니다. Clang의 -fvisibility=hidden는 컴파일 중인 소스 파일에서 전역 정의의 기본 가시성을 설정합니다. 기존 아카이브를 다시 컴파일하지는 않습니다. Clang은 인라인 C++ 멤버 함수에 사용하는 -fvisibility-inlines-hidden도 별도로 문서화합니다. 인터페이스에 적합한지 고려해 이 옵션을 특정 용도에 맞게 사용하세요. 자세한 내용은 Clang 명령줄 참조를 확인하세요.
의도한 공개 진입점에는 __attribute__((visibility("default")))를 표시하세요. 가시성 특성은 컴파일 기본값을 덮어쓸 수 있으므로 종속성 헤더에 명시적인 내보내기 주석이 있는지 살펴보세요. Apple은 심볼 가시성 제어에서 이 동작을 설명합니다.
최종 링크 단계에서는 -exported_symbols_list를 사용해 공개 인터페이스를 열거하세요. Apple이 문서화한 내보내기 목록 형식은 C 심볼 앞에 밑줄을 붙이는 등 링커 심볼 이름을 사용합니다. 내보내기 목록 또는 비내보내기 심볼 목록 중 하나를 선택하고 두 전략을 함께 사용하지 마세요. 자세한 내용은 Dynamic Library Design Guidelines를 확인하세요.
플러그인 인터페이스가 작다면 명시적 내보내기 목록을 사용하면 검토가 간단해집니다. 내보내는 모든 이름에 공개해야 할 이유가 있어야 합니다. 소스의 주석과 목록을 일치시키세요. 주석은 코드에서 API를 식별하고, 목록은 최종 바이너리를 제한합니다.
로컬에서 완전한 예제 빌드하기
이 예제에서는 의도적으로 C 종속성을 사용합니다. 호환되지 않는 C++ 정의를 이식 가능한 언어 기법인 것처럼 제시하지 않으면서 Mach-O 내보내기 제어를 보여주기 위한 것입니다. 호스트의 종속성은 10을 반환하고, 플러그인의 비공개 빌드는 20를 반환합니다. 이 값들은 예시 입력입니다.
Xcode Command Line Tools를 사용할 수 있는 Mac에서 같은 Terminal 셸에 다음 블록을 순서대로 실행하세요. 새 임시 디렉터리를 만들고 활성 툴체인의 기본 아키텍처로 빌드합니다. 이 연구를 진행한 환경은 Apple 툴체인이 없는 Linux였으므로 예제를 살펴보기만 했으며 컴파일하거나 실행하지는 않았습니다.
종속성과 플러그인 만들기
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
종속성 헤더는 C++ 래퍼에 올바른 C 언어 연결 선언을 제공합니다. 공개 래퍼도 C 연결을 사용하므로 이 진입점의 링커 이름을 간단하게 유지할 수 있습니다. C++ 이름은 일반적으로 맹글링이 필요합니다. Apple은 심볼 가시성 제어에서 이 차이를 설명합니다.
호스트 만들기
호스트는 플러그인을 로드하고 반환된 라이브러리 핸들을 통해 공개 함수를 가져옵니다. Apple은 동적 라이브러리와 Mach-O 번들 모두에서 dlopen를 지원한다고 문서화합니다. RTLD_LOCAL 옵션은 로드된 이미지에서 내보낸 심볼의 가용성을 제한하지만 빌드 시점의 가시성 제어를 대체하지는 않습니다. Apple의 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
컴파일 및 링크
# 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
예상 출력은 host=10 plugin=20입니다. 예제에 동일한 C 이름을 가진 외부에 표시되는 구현이 포함되도록 호스트는 종속성 심볼을 명시적으로 내보냅니다. 플러그인에는 비공개 구현이 여전히 포함되어 있어야 합니다.
이는 검증용 예제이며 제어를 제거하면 반드시 결과가 실패한다는 주장은 아닙니다. 여러 구성에서 Mach-O의 기본 바인딩만으로도 정의를 구분할 수 있습니다. 확인할 사항은 선택된 구현과 최종 내보내기 범위입니다.
내보내기와 임포트를 각각 확인하세요
예제 디렉터리에서 다음 명령을 실행하세요.
xcrun nm -g -U -j DemoPlugin.dylib
xcrun dyld_info -exports DemoPlugin.dylib
xcrun dyld_info -imports DemoPlugin.dylib
xcrun otool -L DemoPlugin.dylib
첫 번째 명령에서 -g는 외부 심볼을 선택하고, -U는 정의되지 않은 심볼을 제외하며, -j는 이름을 출력합니다. 이 최소 플러그인에서 정의된 외부 이름은 _demo_plugin_value이어야 하며 _demo_dependency_value은 없어야 합니다. Apple의 nm 매뉴얼은 이 옵션을 문서화하고, 심볼 테이블 정보가 로더에서 사용하는 정보와 다를 수 있다고 설명합니다.
dyld_info -exports를 사용하면 로더에 공개되는 내보내기를 확인할 수 있고, -imports를 사용하면 임포트된 심볼을 확인할 수 있습니다. 종속성 함수는 이 목록 어디에도 없어야 합니다. 이 플러그인 안에 비공개로 구현되어 있기 때문입니다. 이 검사 옵션은 Apple의 dyld_info 매뉴얼에 문서화되어 있습니다.
otool -L을 사용해 실제 dylib 종속성을 확인하세요. 정적 아카이브는 별도의 런타임 종속성으로 표시되지 않아야 합니다. Apple은 Dynamic Library Usage Guidelines에서 이 종속성 검사를 설명합니다.
실제 플러그인에서는 세 가지 질문을 따로 확인하세요. 필요한 구현이 포함되어 있는가? 내보내고 있는가? 경쟁하는 구현을 임포트하는 호출이 남아 있는가? 빌드 성공만으로는 이 질문에 답할 수 없습니다.
Xcode에서 같은 제어 적용하기
최종 플러그인 바이너리를 생성하는 대상에서 Exported Symbols File(EXPORTED_SYMBOLS_FILE)을 내보내기 목록의 프로젝트 상대 경로로 설정하세요. 비공개 구현을 컴파일하는 C 및 C++ 대상에서 Symbols Hidden by Default(GCC_SYMBOLS_PRIVATE_EXTERN)를 활성화하세요. Apple의 최신 빌드 설정 참조에 두 설정이 모두 문서화되어 있습니다.
종속성이 별도 정적 라이브러리 대상이라면 그 대상도 설정하세요. 플러그인의 컴파일러 설정만 바꾸면 이미 컴파일된 종속성 오브젝트는 그대로 남습니다. 영향을 받는 대상을 다시 빌드하고 배포하는 각 아키텍처의 결과물을 검사하세요.
미리 빌드된 아카이브에도 최종 내보내기 목록은 유용합니다. Apple의 공개 링커 매뉴얼에는 일반적인 아카이브 추출을 사용하면서 아카이브의 전역 심볼을 숨김으로 처리하는 -load_hidden도 설명되어 있습니다. 이 매뉴얼은 ld-classic용입니다. 이 추가 옵션을 선택하기 전에 설치된 링커의 문서를 확인하세요. -force_load는 아카이브 멤버 포함 여부를 제어하며, 이는 별개의 문제입니다. 자세한 내용은 Apple 링커 매뉴얼을 확인하세요.
C++ 및 Swift 인터페이스 요구 사항 유지하기
C++ 가시성은 호환되지 않는 정의를 해결하지 않습니다
C++의 단일 정의 규칙은 여러 번 정의가 허용되는 경우의 토큰 일치 및 이름 조회 요구 사항을 포함하여 번역 단위 전반의 정의에 요구 사항을 부과합니다. 심볼을 숨긴다고 해서 충돌하는 C++ 정의를 함께 사용할 수 있는 것은 아닙니다. 독립적으로 버전이 관리되는 구현에는 가능한 경우 서로 다른 네임스페이스나 심볼 이름을 사용하세요. C++ 작업 초안의 단일 정의 규칙을 확인하세요.
의도한 C++ 인터페이스에 필요한 가시성도 유지하세요. Apple은 라이브러리 간 RTTI, 예외, 동적 캐스트, 인라인 함수 주소의 동일성 및 공유 정적 객체를 주의가 필요한 사례로 제시합니다. 실용적인 설계는 종속성 형식과 소유권을 플러그인 내부에 두고, 합의된 스칼라 값이나 불투명 핸들을 사용하는 작은 인터페이스를 공개하는 것입니다. 심볼 가시성 제어를 확인하세요.
Swift 임포트 액세스 제어는 다른 경계를 제어합니다
Swift의 internal import는 임포트된 선언을 참조할 수 있는 위치를 제한합니다. Swift 6.0에서 구현된 SE-0409는 @_implementationOnly import 사용을 internal import 이하로 대체할 것을 권장합니다. Mach-O 종속성 이름 변경이나 내보내기 필터링 메커니즘을 정의하지는 않습니다. 네이티브 바이너리는 계속 별도로 설정하고 검사하세요. 임포트 선언의 액세스 수준 수정자를 확인하세요.
Swift/C++ 직접 상호 운용을 사용하는 경우 툴체인 제약도 유지하세요. Swift 5.9부터 지원되며, 혼합 코드는 같은 C++ 표준 라이브러리를 사용해야 하고, Apple 플랫폼에서는 libc++를 사용합니다. 가져온 C++ 참조 형식에는 macOS 13.3 이상의 최소 배포 버전도 필요합니다. 내보내기 필터링은 이러한 요구 사항을 바꾸지 않습니다. C++ 상호 운용의 지원 기능 및 제약을 확인하세요.
의도한 진입점을 계속 사용할 수 있고, 비공개 종속성 정의가 로더 내보내기에 없으며, 임포트가 사용해야 하는 라이브러리와 일치하면 플러그인을 통합 테스트에 사용할 준비가 된 것입니다. 합성 예제와 같은 동작 테스트와 함께 이러한 검사를 유지하면 이후 종속성 업데이트에서 인터페이스 변경과 구현 선택 변경을 모두 확인할 수 있습니다.
출처
- 빌드 설정 참조 — Apple.
- Dynamic Library Design Guidelines — Apple.
- Dynamic Library Usage Guidelines — Apple.
- 심볼 가시성 제어 — Apple.
- Clang 명령줄 인수 참조 — LLVM Project.
- ld-classic(1) 매뉴얼 — Apple.
- dlopen(3) 매뉴얼 — Apple.
- nm(1) 매뉴얼 — Apple.
- dyld_info(1) 매뉴얼 — Apple.
- 단일 정의 규칙 — Tim Song이 렌더링한 C++ 작업 초안.
- 임포트 선언의 액세스 수준 수정자 — Swift Project.
- C++ 상호 운용의 지원 기능 및 제약 — Swift.org.