반응형

실행파일 A가 플러그인 B를 동적로딩하고 이를 실행하는 예제를 살펴본다. 이 때, 각각 공통된 헤더(Log.h)를 사용하고 있고 이 Log.h의 구현부는 별도의 정적라이브러리(lib_src)로 실행파일 A 측에 구현되어있다.

 

1. 프로젝트 구조

.
├── CMakeLists.txt
├── main.cpp              # 실행 파일 (A)
├── plugin.cpp            # 플러그인 (B)
├── include_main/         # 메인용 헤더 디렉토리
│   └── Log.h
├── include_plugin/       # 플러그인용 헤더 디렉토리 (내용은 동일)
│   └── Log.h
└── lib_src/              # 정적 라이브러리 소스
    └── Log.cpp

 

 

2. 소스코드

Log.h (양쪽 폴더에 동일하게 복사)

#pragma once
class Log {
public:
    void print();
};



lib_src/Log.cpp (정적 라이브러리 구현부)

#include "Log.h" // 컴파일 시점의 경로를 따름
#include <iostream>

void Log::print() {
    std::cout << "[Main App's Static Lib] Hello from Log::print!" << std::endl;
}



main.cpp (실행 파일 A)

#include <iostream>
#include <dlfcn.h>
#include "Log.h" // include_main의 헤더 사용

int main() {
    Log mainLog;
    mainLog.print(); // 메인에서 사용 (whole-archive 없이 포함되도록 함)

    void* handle = dlopen("./libpluginB.so", RTLD_NOW);
    if (!handle) {
        std::cerr << "Load fail: " << dlerror() << std::endl;
        return 1;
    }

    auto run = (void (*)())dlsym(handle, "run_plugin");
    if (run) run();

    dlclose(handle);
    return 0;
}

 


plugin.cpp (공유 라이브러리 B)

#include "Log.h" // include_plugin의 헤더 사용 (경로가 다름)

extern "C" void run_plugin() {
    Log pluginLog;
    // 구현체가 없지만, 실행 시 메인 바이너리의 심볼을 찾아갑니다.
    pluginLog.print(); 
}

 

 

CMakeLists.txt

핵심은 --export-dynamic과 헤더 경로 설정이다.

cmake_minimum_required(VERSION 3.10)
project(CrossDirTest)

# 1. 정적 라이브러리 생성 (Main용 헤더 참조)
add_library(LogStatic STATIC lib_src/Log.cpp)
target_include_directories(LogStatic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include_main)

# 2. 메인 실행 파일 생성
add_executable(AppA main.cpp)
target_link_libraries(AppA PRIVATE LogStatic ${CMAKE_DL_LIBS})

# [핵심] 메인 내부의 LogStatic 심볼을 플러그인이 볼 수 있게 공개
target_link_options(AppA PRIVATE "-Wl,--export-dynamic")

# 3. 플러그인 생성 (Plugin 전용 별도 헤더 경로 참조)
add_library(pluginB SHARED plugin.cpp)
# 구현체(Log.cpp)를 링크하지 않고 헤더만 참조함
target_include_directories(pluginB PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include_plugin)
set_target_properties(pluginB PROPERTIES PREFIX "lib" OUTPUT_NAME "pluginB")

 

 

3. 분석

위 코드를 빌드하고 실행하면 다음과 같은 흐름이 발생한다.

  1. 빌드 단계
    • AppA는 LogStatic을 링크하여 Log::print의 기계어 코드를 자기 몸체에 포함한다.
    • pluginB는 Log::print가 어디 있는지 모르지만, "나중에 누군가 채워주겠지" 하고 구멍(Undefined Symbol)을 남겨둔 채 빌드된다.
  2. 실행 단계 (./AppA)
    • main()이 호출되어 첫 번째 "Hello..."를 출력한다.
    • dlopen("./libpluginB.so")가 호출된다.
    • 시스템의 동적 링커(ld.so)가 pluginB를 로드하면서 빈 구멍(Log::print)을 채우려 한다.
    • AppA가 --export-dynamic으로 빌드되었으므로, 링커는 AppA 내부에서 Log::print 심볼을 찾아 pluginB와 연결해 준다.

4. 실행 결과

[Main App's Static Lib] Hello from Log::print!
[Main App's Static Lib] Hello from Log::print!

 

 결과적으로, 이처럼 동일한 헤더 파일의 물리적 위치가 다르더라도, 클래스 이름과 함수 시그니처가 같으면 링커는 이를 동일한 대상으로 간주하고 성공적으로 연결한다. (--export-dynamic을 활용해야한다.)

 단, 앞서 설명한 대로 한쪽 헤더에만 멤버 변수를 추가하는 등의 실수를 하면 Segmentation Fault가 발생할 수 있으니 내용 일치에만 주의하시면 된다.

 

5. 그림

위의 내용들을 간단하게 그림으로 나타내면 아래와 같다.

 

- 빌드타임

 

- 런타임

 

반응형