C++ 플러그인 시스템 완벽 가이드 | dlopen·LoadLibrary·인터페이스·핫 리로드 [실전]

C++ 플러그인 시스템 완벽 가이드 | dlopen·LoadLibrary·인터페이스·핫 리로드 [실전]

이 글의 핵심

C++ 플러그인 아키텍처: 동적 라이브러리 로딩(dlopen/LoadLibrary), C ABI 인터페이스, 플러그인 매니저, 핫 리로드. 문제 시나리오, 완전한 예제, 흔한 에러, 베스트 프랙티스, 프로덕션 패턴.

들어가며: “플러그인 추가할 때마다 앱을 다시 빌드해야 하나요?”

실제 겪는 문제 시나리오

이미지 에디터에 새 필터를 넣거나, IDE에 새 언어 지원을 추가하거나, 게임에 모드를 끼워 넣을 때, 호스트 앱을 다시 컴파일하지 않고 외부 모듈을 불러와 실행하는 것이 플러그인 시스템의 목표입니다. 비유하면 USB 포트에 꽂기만 하면 바로 인식되는 주변기기처럼, 플러그인은 런타임에 동적으로 로드되어 호스트와 협력합니다.

flowchart TD
  subgraph wrong[❌ 정적 링크 방식]
    W1[필터 추가] --> W2[호스트 소스 수정]
    W2 --> W3[전체 재빌드]
    W3 --> W4[앱 재배포]
    W4 --> W5[개발·배포 비용 증가]
  end
  subgraph right[✅ 플러그인 시스템]
    R1[필터 추가] --> R2[플러그인 .so/.dll만 빌드]
    R2 --> R3[plugins/ 폴더에 복사]
    R3 --> R4[앱 재시작 없이 인식]
    R4 --> R5[빠른 반복 개발]
  end

문제의 핵심:

  • 호스트와 플러그인은 서로 다른 시점에 빌드됩니다.
  • C++ 클래스 레이아웃·vtable·name mangling은 컴파일러·버전마다 다릅니다.
  • 잘못 설계하면 ABI 불일치로 크래시가 발생합니다.

이 글에서 다루는 것:

  • 문제 시나리오: 플러그인 시스템이 필요한 실제 상황
  • 동적 로딩: dlopen/LoadLibrary로 .so/.dll 로드
  • 플러그인 인터페이스: C ABI로 ABI 안정성 확보
  • 플러그인 매니저: 디렉터리 스캔, 버전 관리
  • 핫 리로드: 개발 시 수정 즉시 반영
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스프로덕션 패턴

요구 환경: C++17 이상

이 글을 읽으면:

  • 플러그인 아키텍처의 핵심을 이해할 수 있습니다.
  • 실전에서 바로 활용할 수 있는 완전한 코드를 얻을 수 있습니다.
  • ABI 안정성을 유지하는 방법을 알 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오: 플러그인 시스템이 필요한 순간
  2. 기본 개념: 동적 로딩과 C ABI
  3. dlopen/LoadLibrary 플랫폼 추상화
  4. 플러그인 인터페이스 설계
  5. 플러그인 매니저 완전 구현
  6. 핫 리로드 구현
  7. 완전한 플러그인 시스템 예제
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 프로덕션 패턴
  11. 구현 체크리스트

1. 문제 시나리오: 플러그인 시스템이 필요한 순간

시나리오 1: 이미지 에디터에 필터 추가

문제: 사용자가 만든 블러·샤프닝 필터를 앱에 통합하려 합니다. 필터마다 앱을 다시 빌드·배포하면 개발 속도가 느리고 사용자도 원하는 필터만 선택해 설치할 수 없습니다.

잘못된 접근:

// ❌ 정적 링크 — 필터 추가 시마다 호스트 재빌드
#include "filter_blur.h"
#include "filter_sharpen.h"

void applyFilter(Image& img, FilterType type) {
    if (type == BLUR) applyBlur(img);
    else if (type == SHARPEN) applySharpen(img);
    // 새 필터 추가 → 소스 수정 → 전체 재빌드
}

해결: 플러그인으로 각 필터를 .so/.dll로 빌드하고, 앱은 런타임에 플러그인 디렉터리를 스캔해 로드합니다. 새 필터는 파일만 추가하면 됩니다.


시나리오 2: IDE에 새 언어 지원

문제: IDE가 C++, Python, Rust를 지원하는데, 새 언어 Zig를 추가하려면 IDE 소스 코드를 수정하고 전체를 다시 빌드해야 합니다. 서드파티가 Zig 플러그인을 배포하려면 IDE 소스에 접근할 수 있어야 합니다.

해결: 언어 지원을 플러그인으로 분리합니다. IDE는 플러그인 API만 공개하고, Zig 팀은 API에 맞춰 플러그인을 개발·배포합니다. IDE는 재빌드 없이 Zig 플러그인을 로드합니다.


시나리오 3: 게임 모드 확장

문제: 게임에 “서바이벌 모드”, “크리에이티브 모드”를 추가할 때마다 게임 엔진을 수정해야 합니다. 커뮤니티 모드도 지원하려면 엔진 소스를 공개해야 할까요?

해결: 게임 모드 인터페이스를 정의하고, 각 모드를 플러그인으로 구현합니다. 커뮤니티는 SDK만 받아 플러그인을 개발하고, 게임은 plugins/ 폴더의 .so/.dll을 로드합니다.


시나리오 4: ABI 불일치로 크래시

문제: 호스트 앱과 플러그인이 같은 PluginInterface 헤더를 쓰지만, 서로 다른 컴파일러(GCC vs Clang) 또는 다른 표준 라이브러리(libstdc++ vs libc++)로 빌드했습니다. std::string을 넘길 때 내부 레이아웃이 달라 세그멘테이션 폴트가 발생합니다.

잘못된 접근:

// ❌ C++ 클래스 경계 — 컴파일러마다 vtable 레이아웃 다름
class IPlugin {
public:
    virtual int process(const std::string& input) = 0;  // std::string ABI 불안정
};

해결: C ABI로 경계를 만들고, std::string 대신 const char*size_t를 사용합니다. C ABI는 컴파일러·플랫폼 간에 안정적입니다.


시나리오 5: 플러그인 버전 불일치

문제: 호스트 v2.0이 process() 외에 init() 함수를 추가했습니다. v1.0으로 빌드된 플러그인은 init을 모르므로, 호스트가 init을 호출하면 vtable 인덱스 오류로 크래시합니다.

해결: 버전 번호를 인터페이스에 포함하고, 호스트는 플러그인의 version을 확인한 뒤 version >= 2일 때만 init을 호출합니다. 가상 함수는 끝에만 추가해 기존 인덱스를 유지합니다.


시나리오 6: 플러그인 수정 시마다 앱 재시작

문제: 게임 AI 로직을 수정할 때마다 전체 앱을 종료하고 2~3분 빌드 후 다시 실행합니다. “공격 거리 5 → 7로 바꿔볼까?” 같은 작은 실험을 10번 하려면 30분이 걸립니다.

해결: 핫 리로드 — 파일 감시로 .so/.dll 변경 시 자동 리로드합니다. 수정 후 플러그인만 재빌드하면 앱은 계속 실행된 채 새 로직이 적용됩니다.


2. 기본 개념: 동적 로딩과 C ABI

동적 로딩이란?

정적 링크는 컴파일 시점에 라이브러리 코드가 실행 파일에 포함됩니다. 동적 로딩은 실행 시점에 .so(Linux/macOS) 또는 .dll(Windows)을 메모리에 로드하고, 심볼(함수·변수)을 조회해 호출합니다.

flowchart TB
    subgraph static[정적 링크]
        S1[main.o] --> S2[libplugin.a]
        S2 --> S3[실행파일]
        S3 --> |"코드 포함"| S4[단일 바이너리]
    end
    subgraph dynamic["동적 로딩 (플러그인)"]
        D1[호스트 앱] --> D2[dlopen/LoadLibrary]
        D2 --> D3[plugin.so / plugin.dll]
        D3 --> D4[런타임에 로드]
        D1 -.->|dlsym으로 함수 조회| D4
    end

플랫폼별 API:

플랫폼로드심볼 조회해제확장자
Linuxdlopendlsymdlclose.so
macOSdlopendlsymdlclose.dylib, .so
WindowsLoadLibraryGetProcAddressFreeLibrary.dll

C ABI가 필요한 이유

C++는 name mangling으로 함수 이름을 컴파일러마다 다르게 만듭니다. void foo(int)가 GCC에서는 _Z3fooi, MSVC에서는 ?foo@@YAXH@Z처럼 됩니다. 가상 함수 테이블(vtable) 레이아웃도 컴파일러·표준 라이브러리 버전에 따라 달라집니다.

C ABIextern "C"로 name mangling을 제거하고, 함수 호출 규약을 고정합니다. 따라서 호스트와 플러그인이 서로 다른 컴파일러로 빌드되어도 C 함수를 통해 안전하게 통신할 수 있습니다.

// C++ name mangling — 컴파일러마다 다름
void process(int x);  // _Z7processi (GCC) vs ?process@@YAXH@Z (MSVC)

// C ABI — 플랫폼 표준으로 고정
extern "C" void process(int x);  // process (동일)

플러그인 생명주기

sequenceDiagram
    participant H as 호스트
    participant P as 플러그인

    H->>P: dlopen / LoadLibrary
    H->>P: dlsym / GetProcAddress
    H->>P: create(config)
    P-->>H: instance handle
    H->>P: process(handle, input, output)
    P-->>H: result
    H->>P: destroy(handle)
    H->>P: dlclose / FreeLibrary
  1. 로드: dlopen/LoadLibrary로 .so/.dll 열기
  2. 심볼 조회: dlsym/GetProcAddresscreate, destroy, process 등 조회
  3. 생성: create(config)로 플러그인 인스턴스 생성
  4. 사용: process(handle, input, output) 등으로 작업 수행
  5. 파괴: destroy(handle)로 인스턴스 해제
  6. 언로드: dlclose/FreeLibrary로 라이브러리 해제

3. dlopen/LoadLibrary 플랫폼 추상화

완전한 플랫폼 추상화 레이어

동적 로딩 API는 플랫폼마다 다릅니다. 공통 인터페이스를 정의합니다.

// plugin_loader.h — 플랫폼 추상화
#pragma once

#include <string>
#include <memory>

#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif

class PluginLoader {
public:
    using Handle = void*;

    explicit PluginLoader(const std::string& path);
    ~PluginLoader();

    PluginLoader(const PluginLoader&) = delete;
    PluginLoader& operator=(const PluginLoader&) = delete;

    // 심볼 조회: dlsym / GetProcAddress
    void* getSymbol(const char* name) const;

    bool isLoaded() const { return handle_ != nullptr; }

private:
    Handle handle_ = nullptr;
};
// plugin_loader.cpp — 플랫폼별 구현
#include "plugin_loader.h"
#include <stdexcept>

#ifdef _WIN32
PluginLoader::PluginLoader(const std::string& path) {
    handle_ = LoadLibraryA(path.c_str());
    if (!handle_) {
        DWORD err = GetLastError();
        throw std::runtime_error("LoadLibrary failed: " + path + " (error " + std::to_string(err) + ")");
    }
}

PluginLoader::~PluginLoader() {
    if (handle_) {
        FreeLibrary(static_cast<HMODULE>(handle_));
    }
}

void* PluginLoader::getSymbol(const char* name) const {
    if (!handle_) return nullptr;
    return reinterpret_cast<void*>(
        GetProcAddress(static_cast<HMODULE>(handle_), name));
}
#else
PluginLoader::PluginLoader(const std::string& path) {
    // RTLD_NOW: 로드 시 모든 심볼 해석 (누락 시 즉시 실패)
    // RTLD_LOCAL: 플러그인 심볼을 다른 모듈에 노출하지 않음 (충돌 방지)
    handle_ = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
    if (!handle_) {
        const char* err = dlerror();
        throw std::runtime_error(std::string("dlopen failed: ") + (err ? err : "unknown"));
    }
}

PluginLoader::~PluginLoader() {
    if (handle_) {
        dlclose(handle_);
    }
}

void* PluginLoader::getSymbol(const char* name) const {
    if (!handle_) return nullptr;
    return dlsym(handle_, name);
}
#endif

주의점:

  • RTLD_NOW: 로드 시점에 모든 심볼을 해석 (지연 바인딩은 RTLD_LAZY)
  • RTLD_LOCAL: 플러그인 심볼이 다른 플러그인에 노출되지 않음 (심볼 충돌 방지)

4. 플러그인 인터페이스 설계

C ABI 플러그인 인터페이스

플러그인과 호스트 간 경계는 C 구조체 + 함수 포인터로 정의합니다.

// plugin_interface.h — 공개 헤더 (호스트·플러그인 공유)
#pragma once

#include <cstdint>
#include <cstddef>

#ifdef __cplusplus
extern "C" {
#endif

#define PLUGIN_API_VERSION 1

// 플러그인 API 구조체: create/destroy/process 등
struct PluginAPI {
    uint32_t version;  // PLUGIN_API_VERSION과 비교

    // 인스턴스 생명주기
    void* (*create)(const char* config);
    void (*destroy)(void* instance);

    // 핵심 작업
    int (*process)(void* instance, const void* input, size_t input_size,
                   void* output, size_t output_size);

    // 선택: 버전 2에서 추가
    // void (*init)(void* instance);  // version >= 2일 때만 사용
};

// 플러그인은 이 심볼을 export해야 함
#define PLUGIN_API_SYMBOL "plugin_api"

// 플러그인 메타데이터 (선택)
struct PluginInfo {
    const char* name;
    const char* version;
    const char* author;
};

#define PLUGIN_INFO_SYMBOL "plugin_info"

#ifdef __cplusplus
}
#endif

설계 원칙:

  • std::string, std::vector 등 STL 타입 사용 금지 — ABI 불안정
  • const char*, void*, size_t, uint32_t 등 C 기본 타입만 사용
  • 새 기능은 끝에 필드 추가, version으로 분기

플러그인 export 매크로

// plugin_export.h — 플랫폼별 export
#pragma once

#ifdef _WIN32
#define PLUGIN_EXPORT __declspec(dllexport)
#else
#define PLUGIN_EXPORT __attribute__((visibility("default")))
#endif

5. 플러그인 매니저 완전 구현

플러그인 매니저 클래스

// plugin_manager.h — 여러 플러그인 관리
#pragma once

#include "plugin_interface.h"
#include "plugin_loader.h"
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>

class PluginHost;  // 전방 선언

class PluginManager {
public:
    void scanDirectory(const std::string& path);
    PluginHost* getPlugin(const std::string& name);
    const std::vector<std::string>& getPluginNames() const {
        return plugin_names_;
    }
    void unloadAll();

private:
    std::vector<std::string> plugin_names_;
    std::unordered_map<std::string, std::unique_ptr<PluginHost>> plugins_;
};
// plugin_host.h — 단일 플러그인 래퍼
#pragma once

#include "plugin_interface.h"
#include <memory>
#include <string>

class PluginLoader;

class PluginHost {
public:
    explicit PluginHost(const std::string& plugin_path);
    ~PluginHost();

    PluginHost(const PluginHost&) = delete;
    PluginHost& operator=(const PluginHost&) = delete;

    int process(const void* input, size_t input_size,
                void* output, size_t output_size);

    uint32_t getVersion() const { return api_ ? api_->version : 0; }
    const PluginInfo* getInfo() const { return info_; }

private:
    class Impl;
    std::unique_ptr<Impl> impl_;
    const PluginAPI* api_ = nullptr;
    const PluginInfo* info_ = nullptr;
};
// plugin_host.cpp
#include "plugin_host.h"
#include "plugin_loader.h"
#include <stdexcept>

class PluginHost::Impl {
public:
    PluginLoader loader;
    const PluginAPI* api = nullptr;
    void* instance = nullptr;

    Impl(const std::string& path) : loader(path) {
        auto sym = loader.getSymbol(PLUGIN_API_SYMBOL);
        if (!sym) {
            throw std::runtime_error("plugin_api symbol not found");
        }
        api = static_cast<const PluginAPI*>(sym);
        if (api->version < PLUGIN_API_VERSION) {
            throw std::runtime_error("plugin version too old");
        }
        instance = api->create("");
        if (!instance) {
            throw std::runtime_error("plugin create failed");
        }
    }

    ~Impl() {
        if (api && instance) {
            api->destroy(instance);
        }
    }
};

PluginHost::PluginHost(const std::string& plugin_path)
    : impl_(std::make_unique<Impl>(plugin_path)),
      api_(impl_->api) {
    auto infoSym = impl_->loader.getSymbol(PLUGIN_INFO_SYMBOL);
    info_ = infoSym ? static_cast<const PluginInfo*>(infoSym) : nullptr;
}

PluginHost::~PluginHost() = default;

int PluginHost::process(const void* input, size_t input_size,
                        void* output, size_t output_size) {
    return api_->process(impl_->instance, input, input_size, output, output_size);
}
// plugin_manager.cpp
#include "plugin_manager.h"
#include "plugin_host.h"
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void PluginManager::scanDirectory(const std::string& path) {
    for (const auto& entry : fs::directory_iterator(path)) {
        if (!entry.is_regular_file()) continue;
        auto ext = entry.path().extension().string();
#ifdef _WIN32
        if (ext != ".dll") continue;
#else
        if (ext != ".so" && ext != ".dylib") continue;
#endif
        try {
            auto name = entry.path().stem().string();
            plugins_[name] = std::make_unique<PluginHost>(entry.path().string());
            plugin_names_.push_back(name);
        } catch (const std::exception& e) {
            std::cerr << "Failed to load " << entry.path() << ": " << e.what() << "\n";
        }
    }
}

PluginHost* PluginManager::getPlugin(const std::string& name) {
    auto it = plugins_.find(name);
    return it != plugins_.end() ? it->second.get() : nullptr;
}

void PluginManager::unloadAll() {
    plugins_.clear();
    plugin_names_.clear();
}

6. 핫 리로드 구현

핫 리로드 매니저

개발 중에는 플러그인 수정 후 재빌드만 하면 호스트가 자동으로 다시 로드하게 할 수 있습니다.

// hot_reload_manager.h
#pragma once

#include "plugin_host.h"
#include <string>
#include <functional>
#include <atomic>

class HotReloadManager {
public:
    using ReloadCallback = std::function<void(PluginHost*)>;

    explicit HotReloadManager(const std::string& plugin_path);
    ~HotReloadManager();

    PluginHost* getPlugin();
    void setReloadCallback(ReloadCallback cb) { callback_ = std::move(cb); }

    // 파일 변경 감지 시 호출 (별도 스레드 또는 이벤트 루프에서)
    void checkAndReload();

private:
    std::string path_;
    std::unique_ptr<PluginHost> host_;
    ReloadCallback callback_;
    std::atomic<uint64_t> last_modified_{0};
};
// hot_reload_manager.cpp
#include "hot_reload_manager.h"
#include <filesystem>
#include <chrono>

namespace fs = std::filesystem;

HotReloadManager::HotReloadManager(const std::string& plugin_path)
    : path_(plugin_path) {
    try {
        host_ = std::make_unique<PluginHost>(path_);
        last_modified_ = fs::last_write_time(path_).time_since_epoch().count();
    } catch (...) {
        // 초기 로드 실패 시 host_는 nullptr
    }
}

HotReloadManager::~HotReloadManager() = default;

PluginHost* HotReloadManager::getPlugin() {
    return host_.get();
}

void HotReloadManager::checkAndReload() {
    if (!fs::exists(path_)) return;

    auto mod_time = fs::last_write_time(path_).time_since_epoch().count();
    if (mod_time != last_modified_.load()) {
        last_modified_.store(mod_time);
        try {
            auto old_host = host_.get();
            host_ = std::make_unique<PluginHost>(path_);
            if (callback_) {
                callback_(host_.get());
            }
        } catch (const std::exception& e) {
            // 리로드 실패 — 기존 플러그인 유지
        }
    }
}

주의: 이미 사용 중인 인스턴스가 있으면 교체 시 크래시할 수 있으므로, 사용 중인 인스턴스가 없을 때만 리로드합니다. 또는 새 요청만 새 인스턴스로 처리하는 방식으로 설계합니다.


7. 완전한 플러그인 시스템 예제

예제 1: 그레이스케일 필터 플러그인

목표: 호스트 앱이 플러그인 이미지 필터를 로드해 실행합니다.

// plugin_greyscale.cpp — 그레이스케일 필터 플러그인
#include "plugin_interface.h"
#include "plugin_export.h"
#include <cstring>
#include <cstdint>

struct FilterState {
    // 필터 내부 상태 (필요 시)
};

static void* create(const char* config) {
    (void)config;
    return new FilterState();
}

static void destroy(void* instance) {
    delete static_cast<FilterState*>(instance);
}

// RGB -> Grayscale: Y = 0.299*R + 0.587*G + 0.114*B
static int process(void* instance, const void* input, size_t input_size,
                   void* output, size_t output_size) {
    (void)instance;
    if (!input || !output || output_size < input_size / 3) {
        return -1;
    }
    const std::uint8_t* in = static_cast<const std::uint8_t*>(input);
    std::uint8_t* out = static_cast<std::uint8_t*>(output);
    size_t pixels = input_size / 3;
    for (size_t i = 0; i < pixels; ++i) {
        std::uint8_t r = in[i * 3 + 0];
        std::uint8_t g = in[i * 3 + 1];
        std::uint8_t b = in[i * 3 + 2];
        out[i] = static_cast<std::uint8_t>(0.299 * r + 0.587 * g + 0.114 * b);
    }
    return static_cast<int>(pixels);
}

extern "C" {

PLUGIN_EXPORT PluginAPI plugin_api = {
    .version = PLUGIN_API_VERSION,
    .create = create,
    .destroy = destroy,
    .process = process,
};

PLUGIN_EXPORT PluginInfo plugin_info = {
    .name = "Greyscale Filter",
    .version = "1.0",
    .author = "pkglog",
};

}

예제 2: 호스트 main.cpp

// main.cpp — 호스트 앱
#include "plugin_manager.h"
#include <iostream>
#include <vector>

int main() {
    try {
        PluginManager mgr;
        mgr.scanDirectory("./plugins");

        for (const auto& name : mgr.getPluginNames()) {
            std::cout << "Loaded: " << name << "\n";
            auto* plugin = mgr.getPlugin(name);
            if (plugin && plugin->getInfo()) {
                std::cout << "  " << plugin->getInfo()->name
                          << " v" << plugin->getInfo()->version << "\n";
            }
        }

        auto* greyscale = mgr.getPlugin("plugin_greyscale");
        if (greyscale) {
            std::vector<std::uint8_t> input = {255, 0, 0, 0, 255, 0, 0, 0, 255};
            std::vector<std::uint8_t> output(3, 0);
            int ret = greyscale->process(input.data(), input.size(),
                                         output.data(), output.size());
            if (ret > 0) {
                std::cout << "Grayscale: " << (int)output[0] << " "
                          << (int)output[1] << " " << (int)output[2] << "\n";
            }
        }
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

예제 3: CMake 빌드 (크로스 플랫폼)

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(PluginDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# 플러그인 인터페이스 (헤더만, 호스트·플러그인 공유)
add_library(plugin_interface INTERFACE)
target_include_directories(plugin_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

# 플러그인
add_library(plugin_greyscale SHARED plugin_greyscale.cpp)
target_link_libraries(plugin_greyscale PUBLIC plugin_interface)
set_target_properties(plugin_greyscale PROPERTIES
    PREFIX ""
    OUTPUT_NAME "plugin_greyscale"
)
if(UNIX)
  target_compile_options(plugin_greyscale PRIVATE -fvisibility=hidden)
endif()

# 호스트
add_executable(host main.cpp plugin_host.cpp plugin_loader.cpp plugin_manager.cpp)
target_link_libraries(host PRIVATE plugin_interface)
if(UNIX AND NOT APPLE)
  target_link_libraries(host PRIVATE dl)
endif()

# 플러그인 출력 경로
set_target_properties(plugin_greyscale PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/plugins)

예제 4: 빌드 및 실행

# Linux/macOS
mkdir build && cd build
cmake ..
make

# 플러그인을 plugins/에 복사 후 실행
cp libplugin_greyscale.so plugins/  # Linux
cp libplugin_greyscale.dylib plugins/  # macOS
./host
# Windows
mkdir build
cd build
cmake .. -G "Visual Studio 17 2022" -A x64
cmake --build . --config Release

# plugins 폴더에 plugin_greyscale.dll 복사 후
.\Release\host.exe

8. 자주 발생하는 에러와 해결법

에러 1: “plugin_api symbol not found”

원인: 플러그인이 plugin_api 심볼을 export하지 않았거나, C 링크로 선언하지 않았습니다.

// ❌ 잘못된 예 — C++ 링크, name mangling 적용
PluginAPI plugin_api = { ... };  // _Z10plugin_api 등으로 맹글링됨
// ✅ 올바른 예 — extern "C"로 C 링크
extern "C" {
PLUGIN_EXPORT PluginAPI plugin_api = {
    .version = PLUGIN_API_VERSION,
    .create = create,
    .destroy = destroy,
    .process = process,
};
}

추가 확인: Windows에서는 __declspec(dllexport) 필요. Linux/macOS에서는 -fvisibility=hidden 사용 시 __attribute__((visibility("default"))) 필요.

에러 2: “undefined symbol: std::cout” (플러그인 로드 시)

원인: 플러그인과 호스트가 다른 C++ 표준 라이브러리를 링크했습니다. 플러그인은 -static-libstdc++로 빌드했는데 호스트는 동적 libstdc++를 쓰는 경우 등.

해결:

  • 플러그인과 호스트 모두 동적 libstdc++ 사용 (기본값)
  • 또는 플러그인 내부에서 C ABI 경계만 넘기고, std::string 등 STL을 경계에 사용하지 않기

에러 3: 세그멘테이션 폴트 (process 호출 시)

원인: input/output 버퍼가 nullptr이거나 크기가 부족한데 플러그인이 검사하지 않고 접근했습니다.

// ❌ 잘못된 예
static int process(void* instance, const void* input, size_t input_size,
                   void* output, size_t output_size) {
    const uint8_t* in = static_cast<const uint8_t*>(input);
    uint8_t* out = static_cast<uint8_t*>(output);
    for (size_t i = 0; i < input_size; ++i) {
        out[i] = in[i];  // output_size 검사 없음 — 버퍼 오버런
    }
    return 0;
}
// ✅ 올바른 예 — null·크기 검사
static int process(void* instance, const void* input, size_t input_size,
                   void* output, size_t output_size) {
    if (!input || !output || output_size < input_size) {
        return -1;
    }
    const uint8_t* in = static_cast<const uint8_t*>(input);
    uint8_t* out = static_cast<uint8_t*>(output);
    size_t n = (input_size < output_size) ? input_size : output_size;
    for (size_t i = 0; i < n; ++i) {
        out[i] = in[i];
    }
    return static_cast<int>(n);
}

에러 4: “cannot open shared object file” (Linux)

원인: LD_LIBRARY_PATH에 플러그인 경로가 없거나, 플러그인의 의존 라이브러리를 찾지 못함.

해결:

# 실행 시 경로 지정
LD_LIBRARY_PATH=./plugins:$LD_LIBRARY_PATH ./host

# 또는 RPATH를 빌드 시 설정
g++ -shared -fPIC -Wl,-rpath,'$ORIGIN' -o libplugin.so plugin.cpp

에러 5: Windows에서 “The specified module could not be found”

원인: 플러그인 .dll이 의존하는 다른 .dll(예: MSVC 런타임)을 찾지 못함.

해결:

  • 호스트와 플러그인을 같은 런타임으로 빌드 (/MD 또는 /MT 일치)
  • Dependencies 등으로 누락된 DLL 확인

에러 6: 플러그인 해제 순서로 인한 크래시

원인: 플러그인 A가 플러그인 B의 함수를 호출하는데, B를 먼저 dlclose했습니다. A가 B의 코드를 호출할 때 이미 언로드된 메모리 접근으로 크래시.

해결:

  • 플러그인 간 직접 호출 지양
  • 해제 시 의존성 역순으로 언로드 (DAG 기준 위상 정렬)
  • 또는 앱 종료 시 한꺼번에 해제 (순서 무관하게 종료)

에러 7: RTLD_GLOBAL 사용 시 심볼 충돌

원인: dlopen(path, RTLD_GLOBAL)로 로드하면 플러그인 내부 심볼이 전역에 노출됩니다. 두 플러그인이 같은 이름의 helper 함수를 갖고 있으면 나중에 로드된 쪽이 덮어씁니다.

해결: RTLD_LOCAL 사용 (기본 권장). 플러그인 간 통신이 필요하면 호스트가 중개하는 API를 제공합니다.

에러 8: Linux GCC에서 dlclose 후에도 라이브러리 메모리에 남음

원인: GCC의 -fno-gnu-unique 없이 빌드하면 dlclose가 참조 카운트만 줄이고 실제 언로드가 되지 않을 수 있습니다.

해결: GCC로 플러그인 빌드 시 -fno-gnu-unique 플래그 추가. Clang은 해당 이슈 없음.

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  target_compile_options(plugin_greyscale PRIVATE -fno-gnu-unique)
endif()

9. 베스트 프랙티스

1. C ABI 경계 유지

  • 호스트↔플러그인 경계에서는 C 타입만 사용: const char*, void*, size_t, uint32_t, int
  • std::string, std::vector, std::function 등은 경계를 넘기지 않기

2. 버전 검사

if (api->version < PLUGIN_API_VERSION) {
    // 구버전 플러그인 거부 또는 호환 모드
}
if (api->version >= 2) {
    // v2 전용 기능 사용
}

3. 에러 코드 규약

  • process 반환값: 0 = 성공, 음수 = 에러 코드
  • 에러 코드는 문서화해 플러그인 개발자가 참고할 수 있게 합니다.

4. 리소스 소유권

  • create로 생성된 인스턴스는 플러그인이 소유
  • 호스트는 destroy반드시 호출해 해제 (예외 발생 시에도 RAII 활용)

5. 스레드 안전성

  • 플러그인 API가 스레드 세이프한지 문서에 명시
  • 일반적으로 인스턴스당 한 스레드 또는 호출자가 락을 담당

6. 플러그인 메타데이터

struct PluginInfo {
    const char* name;
    const char* version;
    const char* author;
};

extern "C" PLUGIN_EXPORT PluginInfo plugin_info = {
    .name = "Greyscale Filter",
    .version = "1.0",
    .author = "Your Name",
};

호스트는 로드 시 plugin_info를 읽어 UI에 표시하거나, 호환성 검사에 활용할 수 있습니다.


10. 프로덕션 패턴

패턴 1: 샌드박스 / 격리

플러그인이 크래시해도 호스트가 죽지 않게 하려면:

  • 별도 프로세스에서 플러그인 실행 (IPC로 통신)
  • 또는 크래시 핸들러로 플러그인 호출을 try-catch하고, 크래시 시 해당 플러그인만 언로드

패턴 2: 지연 로딩 (Lazy Load)

플러그인 수가 많을 때, 사용 시점까지 로드를 미룹니다.

class LazyPlugin {
public:
    PluginHost* get() {
        if (!host_) {
            host_ = std::make_unique<PluginHost>(path_);
        }
        return host_.get();
    }
private:
    std::string path_;
    std::unique_ptr<PluginHost> host_;
};

패턴 3: 플러그인 의존성

플러그인 A가 플러그인 B의 기능을 쓸 때:

  • 호스트가 중개: 호스트가 “서비스 레지스트리”를 제공하고, B를 등록하면 A가 호스트를 통해 B를 호출
  • 의존성 선언: 플러그인 매니페스트에 depends: [plugin_b]를 두고, 호스트가 로드 순서를 보장

패턴 4: 호스트 콜백 (로깅/디버깅)

플러그인에서 호스트의 로깅 API를 사용하려면, create 시 로그 콜백을 넘깁니다.

struct PluginAPI {
    // ...
    void* (*create)(const char* config, void* host_context);
};

// host_context에 로그 함수 포인터 등 포함
struct HostContext {
    void (*log)(int level, const char* msg);
};

패턴 5: 플러그인 풀 (인스턴스 재사용)

create/destroy 비용이 크면, 인스턴스를 풀에 보관해 재사용합니다.

class PluginPool {
    std::vector<std::unique_ptr<PluginHost>> pool_;
    std::mutex mtx_;
public:
    PluginHost* acquire(const std::string& path);
    void release(PluginHost* p);
};

패턴 6: 배치 처리

플러그인 호출 오버헤드를 줄이려면, 여러 작업을 한 번에 넘기는 배치 API를 설계합니다.

int (*process_batch)(void* instance, const BatchInput* inputs, int count,
                     BatchOutput* outputs);

11. 구현 체크리스트

플러그인 시스템 도입 시 확인할 항목:

  • C ABI로 인터페이스 경계 정의 (extern "C")
  • plugin_api 심볼 export (PLUGIN_EXPORT 등)
  • 버전 필드 및 호환성 검사
  • create/destroy 쌍 호출 (RAII 또는 try-finally)
  • input/output null·크기 검사
  • 플랫폼별 로더 (dlopen/LoadLibrary) 추상화
  • RTLD_LOCAL 사용 (심볼 충돌 방지)
  • 플러그인 메타데이터 (plugin_info) 정의
  • 에러 코드 규약 문서화
  • 테스트: 다른 컴파일러·버전으로 호스트/플러그인 빌드
  • Linux GCC: -fno-gnu-unique (핫 리로드 시)

정리

항목설명
동적 로딩dlopen/LoadLibrary로 .so/.dll 런타임 로드
C ABIextern "C"로 경계 고정, ABI 안정성 확보
인터페이스PluginAPI 구조체에 create/destroy/process 등
플러그인 매니저디렉터리 스캔, 버전 검사, 수명 관리
핫 리로드파일 감시 → 언로드 → 재로드 (개발용)
에러 처리null·크기 검사, 에러 코드 규약
프로덕션샌드박스, 지연 로딩, 의존성 관리

핵심 원칙:

  1. C ABI로 호스트↔플러그인 경계를 고정한다.
  2. 버전 번호로 호환성을 검사하고, 새 기능은 끝에만 추가한다.
  3. 리소스 소유권을 명확히 하고, destroy를 반드시 호출한다.
  4. 플랫폼 추상화로 Linux/macOS/Windows를 한 인터페이스로 다룬다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 확장 가능한 애플리케이션, 모듈형 시스템, 서드파티 통합, IDE 확장, 게임 모드 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


참고 자료


한 줄 요약: 동적 로딩·C ABI 인터페이스·플러그인 매니저·핫 리로드를 마스터하면 안정적이고 확장 가능한 플러그인 시스템을 구축할 수 있습니다.


관련 글

  • C++ 플러그인 시스템 | 동적 로딩·인터페이스·버전 관리 [#55-2]
  • C++ 동적 로딩 완벽 가이드 | dlopen·LoadLibrary·실전 패턴 [#55-2]
  • C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]
  • C++ ABI 호환성 완벽 가이드 | PIMPL·C 인터페이스·버전 관리·프로덕션 패턴 [#55-4]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3