C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]

C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]

이 글의 핵심

게임 로직·플러그인·핫 리로드가 필요할 때 C++에 Lua·Python·JavaScript를 붙이는 방법. Lua C API, pybind11, V8 바인딩, 샌드박싱, 성능 최적화까지 실전 코드로 정리합니다.

들어가며: 게임 로직을 C++에 박아두면 수정할 때마다 재빌드해야 해요

”스킬 밸런스 하나 바꾸려고 전체 빌드 15분 걸려요”

게임·도구·플러그인 시스템을 만들 때 자주 겪는 상황입니다. 문제: 게임 로직·밸런스·이벤트 처리를 C++에 직접 넣으면, 수정할 때마다 전체 재컴파일이 필요합니다. 해결: Lua·Python·JavaScript 같은 스크립트 언어를 C++ 엔진에 붙여서, 런타임에 스크립트만 바꿔도 로직을 갱신할 수 있게 합니다. 비유하면 “집(C++ 엔진)은 그대로 두고, 인테리어(로직)만 바꿀 수 있는 것”과 같습니다.

이 글에서 다루는 것:

  • 문제 시나리오: 스크립팅이 필요한 실제 상황
  • Lua C API: 게임에서 가장 많이 쓰는 경량 스크립트 통합
  • pybind11: Python과 C++ 바인딩 (AI·데이터 파이프라인)
  • V8 JavaScript: Node.js·웹과 동일한 JS 엔진 통합
  • 완전한 통합 예제: API 등록, 콜백, 에러 처리
  • 자주 발생하는 에러와 해결법
  • 성능 최적화프로덕션 패턴

요구 환경: C++17 이상, Lua 5.x / Python 3.6+ / V8 (선택)


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

목차

  1. 문제 시나리오: 스크립팅이 필요한 순간
  2. 스크립트 엔진 선택 가이드
  3. Lua C API 통합
  4. pybind11 Python 통합
  5. V8 JavaScript 통합
  6. 완전한 스크립팅 통합 예제
  7. 자주 발생하는 에러와 해결법
  8. 성능 최적화
  9. 프로덕션 패턴
  10. 정리

1. 문제 시나리오: 스크립팅이 필요한 순간

시나리오 1: 게임 밸런스 수정 시마다 15분 빌드

문제: 스킬 데미지, 이동 속도, 아이템 드롭률 같은 밸런스 값이 C++ 상수로 박혀 있습니다. 기획자가 “이 스킬 데미지를 100에서 120으로” 요청할 때마다 C++ 수정 → 전체 빌드 → 테스트가 반복됩니다.

해결: Lua나 JSON으로 밸런스 테이블을 분리하고, 런타임에 로드합니다. 스크립트만 수정하면 재시작 없이 적용 가능합니다.

시나리오 2: 사용자 플러그인·모드 지원

문제: 에디터·도구에서 사용자가 커스텀 동작을 추가하고 싶어 합니다. C++ 플러그인 DLL은 빌드 환경이 복잡하고, 보안 위험도 있습니다.

해결: Lua·Python 스크립트로 플러그인 API를 노출하면, 사용자가 스크립트만 작성해 확장할 수 있습니다. 샌드박스로 제한된 API만 제공해 안전하게 합니다.

시나리오 3: AI·데이터 파이프라인에서 C++ 연산 호출

문제: Python으로 전처리·학습 파이프라인을 짜고 있는데, 특정 루프만 C++로 옮기고 싶습니다. ctypes·cffi는 수동 래핑이 번거롭습니다.

해결: pybind11으로 C++ 함수·클래스를 Python 모듈로 노출하면, import engine 한 줄로 고성능 연산을 호출할 수 있습니다.

시나리오 4: 웹·Node.js와 동일한 JS 로직 공유

문제: 서버(Node.js)와 클라이언트(브라우저)에서 같은 비즈니스 로직을 쓰고, 네이티브 앱(C++)에서도 동일 로직이 필요합니다.

해결: V8 엔진을 C++에 임베드해 JavaScript를 실행하면, 한 번 작성한 JS 코드를 여러 환경에서 재사용할 수 있습니다.

시나리오 5: 설정 파일·이벤트 시퀀스

문제: 퀘스트·이벤트·대화 시퀀스가 복잡한 조건 분기로 이어집니다. C++에 하드코딩하면 가독성과 유지보수가 어렵습니다.

해결: Lua 테이블이나 스크립트로 이벤트 시퀀스를 정의하면, 기획·스크립터가 직접 수정하기 쉽습니다.


2. 스크립트 엔진 선택 가이드

엔진용도장점단점
Lua게임 로직, 밸런스, 플러그인경량, 임베드 용이, C API 단순라이브러리 생태계 제한적
PythonAI·데이터, 도구, 프로토타입풍부한 라이브러리, 생산성GIL, 메모리·시작 비용
JavaScript (V8)웹·Node 로직 공유, 크로스 플랫폼웹 생태계, 비동기 친화V8 빌드 복잡, 메모리 사용량
flowchart TB
    subgraph choice[선택 기준]
        A[게임/임베디드?] --> B[Lua]
        C[AI/데이터/도구?] --> D[Python]
        E[웹/Node 공유?] --> F[JavaScript V8]
    end
    B --> G[경량, C API]
    D --> H[pybind11, NumPy]
    F --> I[V8, Isolate]

3. Lua C API 통합

기본 구조

Lua는 C API로 설계되어 C++와 직접 연동됩니다. lua_State*가 가상 머신 역할을 하고, 스택으로 값과 함수 인자·반환값을 주고받습니다.

flowchart LR
    subgraph cpp[C++]
        A[게임 엔진] --> B[lua_State*]
    end
    subgraph lua[Lua]
        B --> C[스크립트]
        C --> D[create_entity]
        D --> B
    end

Lua 초기화 및 API 등록

// scripting_lua.cpp
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

#include <string>
#include <cstdio>

class LuaScriptEngine {
    lua_State* L_ = nullptr;

public:
    LuaScriptEngine() {
        L_ = luaL_newstate();
        if (!L_) throw std::runtime_error("luaL_newstate failed");
        luaL_openlibs(L_);  // base, table, string 등 표준 라이브러리
    }

    ~LuaScriptEngine() {
        if (L_) lua_close(L_);
    }

    // C++ 함수를 Lua에 등록: add(a, b) -> a + b
    void register_api() {
        lua_register(L_, "add",  -> int {
            int a = static_cast<int>(luaL_checkinteger(L, 1));
            int b = static_cast<int>(luaL_checkinteger(L, 2));
            lua_pushinteger(L, a + b);
            return 1;  // 반환값 개수
        });

        lua_register(L_, "log",  -> int {
            const char* msg = luaL_checkstring(L, 1);
            printf("[Lua] %s\n", msg);
            return 0;
        });
    }

    bool run_script(const std::string& script) {
        if (luaL_loadstring(L_, script.c_str()) != LUA_OK) {
            fprintf(stderr, "Lua load error: %s\n", lua_tostring(L_, -1));
            lua_pop(L_, 1);
            return false;
        }
        if (lua_pcall(L_, 0, 0, 0) != LUA_OK) {
            fprintf(stderr, "Lua runtime error: %s\n", lua_tostring(L_, -1));
            lua_pop(L_, 1);
            return false;
        }
        return true;
    }

    bool run_file(const std::string& path) {
        if (luaL_dofile(L_, path.c_str()) != LUA_OK) {
            fprintf(stderr, "Lua file error: %s\n", lua_tostring(L_, -1));
            lua_pop(L_, 1);
            return false;
        }
        return true;
    }
};

Lua 스크립트 예시

-- game_logic.lua
log("Game starting...")
local result = add(10, 20)
log("10 + 20 = " .. tostring(result))

upvalue로 C++ 객체 전달

Lua C 함수는 upvalue로 외부 데이터를 받을 수 있습니다. lua_pushcclosure로 클로저를 만들 때 upvalue를 묶습니다.

// EntityManager*를 upvalue로 전달
void LuaScriptEngine::register_entity_api(EntityManager* em) {
    lua_pushlightuserdata(L_, em);
    lua_pushcclosure(L_,  -> int {
        auto* em = static_cast<EntityManager*>(lua_touserdata(L, lua_upvalueindex(1)));
        if (!em) return 0;
        auto& e = em->create_entity();
        lua_pushinteger(L, static_cast<lua_Integer>(e.get_id()));
        return 1;
    }, 1);
    lua_setglobal(L_, "create_entity");
}

Lua에서 C++ 콜백 호출

C++에서 Lua 함수를 호출할 때는 lua_getgloballua_pcall 순서로 진행합니다.

void LuaScriptEngine::call_on_collision(int entity_a, int entity_b) {
    lua_getglobal(L_, "on_collision");
    if (!lua_isfunction(L_, -1)) {
        lua_pop(L_, 1);
        return;
    }
    lua_pushinteger(L_, entity_a);
    lua_pushinteger(L_, entity_b);
    if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
        fprintf(stderr, "Lua callback error: %s\n", lua_tostring(L_, -1));
        lua_pop(L_, 1);
    }
}
-- Lua 측: on_collision 정의
function on_collision(a_id, b_id)
    if get_entity_tag(a_id) == "player" and get_entity_tag(b_id) == "coin" then
        add_score(10)
        destroy_entity(b_id)
    end
end

4. pybind11 Python 통합

pybind11이란

pybind11은 C++ 타입을 Python에서 사용할 수 있게 바인딩하는 헤더 전용 라이브러리입니다. Boost.Python보다 가볍고, NumPy 연동을 지원합니다. 자세한 내용은 pybind11 글을 참고하세요.

최소 예제: C++ 함수·클래스 노출

// engine_module.cpp
#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int a, int b) { return a + b; }

class GameEngine {
public:
    void load_level(const std::string& path) { /* ... */ }
    int get_score() const { return score_; }
private:
    int score_ = 0;
};

PYBIND11_MODULE(game_engine, m) {
    m.doc() = "C++ Game Engine Python binding";
    m.def("add", &add, "Add two integers");

    py::class_<GameEngine>(m, "GameEngine")
        .def(py::init<>())
        .def("load_level", &GameEngine::load_level)
        .def("get_score", &GameEngine::get_score);
}
# Python 사용
import game_engine
print(game_engine.add(1, 2))  # 3
engine = game_engine.GameEngine()
engine.load_level("level1.json")
print(engine.get_score())

CMake 빌드

# CMakeLists.txt
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(game_engine engine_module.cpp)
target_link_libraries(game_engine PRIVATE pybind11::embed)

5. V8 JavaScript 통합

V8 개요

V8은 Chrome·Node.js에서 사용하는 JavaScript 엔진입니다. C++에서 V8을 임베드하면 JS 코드를 네이티브 앱 내에서 실행할 수 있습니다. Isolate가 JS 실행 컨텍스트이고, Context가 전역 스코프입니다.

flowchart TB
    subgraph v8[V8]
        I[Isolate] --> C[Context]
        C --> S[Script]
        S --> F[Function]
    end
    subgraph cpp[C++]
        A[엔진] --> I
        F --> A
    end

V8 최소 예제 (개념)

// v8_embed.cpp (개념 코드, V8 빌드 필요)
#include <v8.h>
#include <libplatform/libplatform.h>

void run_script(const std::string& js_code) {
    v8::V8::InitializeICUDefaultLocation("");
    v8::V8::InitializeExternalStartupData("");
    std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
    v8::V8::InitializePlatform(platform.get());
    v8::V8::Initialize();

    v8::Isolate::CreateParams params;
    params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
    v8::Isolate* isolate = v8::Isolate::New(params);

    {
        v8::Isolate::Scope isolate_scope(isolate);
        v8::HandleScope handle_scope(isolate);
        v8::Local<v8::Context> context = v8::Context::New(isolate);
        v8::Context::Scope context_scope(context);

        v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, js_code.c_str()).ToLocalChecked();
        v8::Local<v8::Script> script;
        if (!v8::Script::Compile(context, source).ToLocal(&script)) {
            // 컴파일 에러 처리
            return;
        }
        v8::Local<v8::Value> result;
        if (!script->Run(context).ToLocal(&result)) {
            // 런타임 에러 처리
            return;
        }
    }

    isolate->Dispose();
    v8::V8::Dispose();
    v8::V8::ShutdownPlatform();
    delete params.array_buffer_allocator;
}

C++ 함수를 JS에 노출

V8에서 C++ 함수를 호출하려면 v8::FunctionTemplate으로 래핑하고 context->Global()에 바인딩합니다. 상세 구현은 V8 공식 문서를 참고하세요.


6. 완전한 스크립팅 통합 예제

예제 1: Lua 게임 엔진 API (전체)

// game_scripting.cpp
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

#include <string>
#include <unordered_map>
#include <memory>
#include <cstdio>

struct Entity {
    int id;
    float x, y;
    std::string tag;
};

class EntityManager {
    int next_id_ = 0;
    std::unordered_map<int, Entity> entities_;
public:
    Entity& create_entity() {
        int id = next_id_++;
        entities_[id] = Entity{id, 0, 0, ""};
        return entities_[id];
    }
    Entity* get_entity(int id) {
        auto it = entities_.find(id);
        return it != entities_.end() ? &it->second : nullptr;
    }
    void destroy_entity(int id) { entities_.erase(id); }
};

class GameScripting {
    lua_State* L_;
    std::unique_ptr<EntityManager> entities_;
    int score_ = 0;

    static int lua_create_entity(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        auto& e = self->entities_->create_entity();
        lua_pushinteger(L, e.id);
        return 1;
    }

    static int lua_set_position(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int id = static_cast<int>(luaL_checkinteger(L, 1));
        float x = static_cast<float>(luaL_checknumber(L, 2));
        float y = static_cast<float>(luaL_checknumber(L, 3));
        auto* e = self->entities_->get_entity(id);
        if (e) { e->x = x; e->y = y; }
        return 0;
    }

    static int lua_add_score(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int delta = static_cast<int>(luaL_checkinteger(L, 1));
        self->score_ += delta;
        return 0;
    }

    static int lua_destroy_entity(lua_State* L) {
        auto* self = static_cast<GameScripting*>(lua_touserdata(L, lua_upvalueindex(1)));
        int id = static_cast<int>(luaL_checkinteger(L, 1));
        self->entities_->destroy_entity(id);
        return 0;
    }

public:
    GameScripting() : entities_(std::make_unique<EntityManager>()) {
        L_ = luaL_newstate();
        luaL_openlibs(L_);

        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_create_entity, 1);
        lua_setglobal(L_, "create_entity");

        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_set_position, 1);
        lua_setglobal(L_, "set_position");

        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_add_score, 1);
        lua_setglobal(L_, "add_score");

        lua_pushlightuserdata(L_, this);
        lua_pushcclosure(L_, lua_destroy_entity, 1);
        lua_setglobal(L_, "destroy_entity");
    }

    ~GameScripting() { lua_close(L_); }

    bool run_file(const std::string& path) {
        return luaL_dofile(L_, path.c_str()) == LUA_OK;
    }

    void fire_collision(int a, int b) {
        lua_getglobal(L_, "on_collision");
        if (lua_isfunction(L_, -1)) {
            lua_pushinteger(L_, a);
            lua_pushinteger(L_, b);
            lua_pcall(L_, 2, 0, 0);
        } else {
            lua_pop(L_, 1);
        }
    }

    int get_score() const { return score_; }
};

Lua 게임 로직 스크립트

-- init.lua: 게임 초기화
local player = create_entity()
set_position(player, 100, 200)

-- collision_handler.lua: 충돌 시
function on_collision(a_id, b_id)
    -- 플레이어가 코인을 먹으면
    add_score(10)
    destroy_entity(b_id)
end

예제 2: pybind11로 게임 엔진 노출

// game_pybind.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

struct Vec2 {
    float x, y;
};

class ScriptableEngine {
    std::vector<Vec2> positions_;
public:
    int spawn(float x, float y) {
        positions_.push_back({x, y});
        return static_cast<int>(positions_.size()) - 1;
    }
    std::pair<float, float> get_position(int id) const {
        if (id < 0 || id >= static_cast<int>(positions_.size()))
            return {0, 0};
        return {positions_[id].x, positions_[id].y};
    }
};

PYBIND11_MODULE(script_engine, m) {
    py::class_<Vec2>(m, "Vec2")
        .def_readwrite("x", &Vec2::x)
        .def_readwrite("y", &Vec2::y);

    py::class_<ScriptableEngine>(m, "Engine")
        .def(py::init<>())
        .def("spawn", &ScriptableEngine::spawn)
        .def("get_position", &ScriptableEngine::get_position);
}
# game_script.py
import script_engine

engine = script_engine.Engine()
pid = engine.spawn(100.0, 200.0)
x, y = engine.get_position(pid)
print(f"Player at ({x}, {y})")

Lua 빌드 (CMake)

# CMakeLists.txt - Lua 임베딩
find_package(PkgConfig REQUIRED)
pkg_check_modules(LUA REQUIRED lua5.4)  # 또는 lua5.3, luajit

add_executable(game_app main.cpp game_scripting.cpp)
target_include_directories(game_app PRIVATE ${LUA_INCLUDE_DIRS})
target_link_libraries(game_app PRIVATE ${LUA_LIBRARIES})
# Ubuntu/Debian
sudo apt install liblua5.4-dev

# vcpkg
vcpkg install lua

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

Lua

문제 1: “attempt to call a nil value (global ‘create_entity’)”

원인: C++에서 create_entity를 Lua에 등록하기 전에 스크립트가 실행됐거나, 등록 시 전역 이름이 다릅니다.

해결법:

// ✅ API 등록 후 스크립트 실행
void GameScripting::init() {
    register_api();  // create_entity 등 등록
    run_file("init.lua");  // 그 다음 스크립트 실행
}

문제 2: “bad argument #1 to ‘set_position’ (number expected, got nil)”

원인: Lua에서 set_position(entity_id, x, y) 호출 시 entity_id가 nil이거나 잘못된 타입입니다.

해결법:

-- ❌ 잘못된 예
set_position(nil, 100, 200)

-- ✅ 올바른 예
local id = create_entity()
set_position(id, 100, 200)
// C++에서 방어 코드
static int lua_set_position(lua_State* L) {
    if (lua_gettop(L) < 3) {
        return luaL_error(L, "set_position(entity_id, x, y) requires 3 arguments");
    }
    if (!lua_isnumber(L, 1)) {
        return luaL_error(L, "entity_id must be a number");
    }
    // ...
}

문제 3: Lua 스택 오버플로우 / 불균형

원인: lua_push*lua_pop 개수가 맞지 않아 스택이 쌓이거나 부족합니다.

해결법:

// ✅ 반환값 개수 정확히
lua_pushinteger(L, result);
return 1;  // 1개 반환

// ✅ 에러 시 스택 정리
if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
    fprintf(stderr, "%s\n", lua_tostring(L_, -1));
    lua_pop(L_, 1);  // 에러 메시지 제거
}

문제 3-2: Lua에서 C++ 객체 수명 관리

원인: Lua userdata가 가리키는 C++ 객체가 먼저 파괴되면, Lua에서 접근 시 크래시가 발생합니다.

해결법:

// shared_ptr을 userdata로 저장하고, __gc 메타메서드에서 정리
// 또는 Lua가 참조하는 동안 C++ 객체 수명을 연장 (예: 엔진이 소유)

문제 3-3: “module ‘xxx’ not found”

원인: Lua에서 require "mymodule"을 썼는데, package.path에 해당 경로가 없습니다.

해결법:

-- 스크립트 상단에서 경로 추가
package.path = package.path .. ";./scripts/?.lua"
// C++에서 package.path 설정
lua_getglobal(L_, "package");
lua_getfield(L_, -1, "path");
std::string path = lua_tostring(L_, -1);
path += ";./scripts/?.lua";
lua_pop(L_, 2);
lua_pushstring(L_, path.c_str());
lua_setfield(L_, -2, "path");

pybind11

문제 4: “ImportError: undefined symbol”

원인: C++ 확장 모듈이 링크할 때 필요한 라이브러리와 링크되지 않았거나, ABI 불일치입니다.

해결법:

# CMake에서 필요한 라이브러리 링크
target_link_libraries(game_engine PRIVATE
    pybind11::embed
    ${Python3_LIBRARIES}  # 필요 시
)

문제 5: GIL(Global Interpreter Lock) 데드락

원인: C++ 스레드에서 Python API를 호출할 때 GIL을 잡지 않아 데드락이 발생합니다.

해결법:

#include <pybind11/pybind11.h>
#include <pybind11/gil.h>

void callback_from_cpp_thread() {
    py::gil_scoped_acquire acquire;  // GIL 획득
    py::object result = py::module::import("mymodule").attr("on_event")(42);
}

V8

문제 6: “Isolate가 이미 dispose된 후 사용”

원인: Isolate::Dispose() 호출 후 해당 Isolate를 사용하려 함.

해결법:

// ✅ Isolate 생명주기 명확히
{
    v8::Isolate::Scope scope(isolate);
    // ... 사용
}  // scope 종료
isolate->Dispose();  // 이후 isolate 사용 금지

8. 성능 최적화

Lua

기법설명
스크립트 사전 컴파일luaL_loadfile로 바이트코드 로드, 반복 luaL_dostring 대신 lua_pcall 재사용
upvalue 활용매 호출마다 lua_getglobal 대신 upvalue로 C++ 객체 전달
로컬 변수Lua에서 local 사용으로 전역 테이블 접근 최소화
LuaJIT가능하면 LuaJIT 사용 시 JIT 컴파일로 10~50배 가속
// ✅ 사전 컴파일 후 반복 실행
luaL_loadfile(L_, "update.lua");  // 한 번만 로드
// 매 프레임
lua_pushvalue(L_, -1);  // 함수 복사
lua_pcall(L_, 0, 0, 0);

pybind11

기법설명
NumPy 버퍼 공유py::array_t로 복사 없이 request().ptr 사용
GIL 해제C++ 전용 연산 중 py::gil_scoped_release로 GIL 해제
불필요한 변환 최소화std::vector 대신 py::array_t 직접 사용
// GIL 해제 후 순수 C++ 연산
void heavy_compute(py::array_t<double> arr) {
    py::gil_scoped_release release;
    double* ptr = static_cast<double*>(arr.request().ptr);
    for (size_t i = 0; i < arr.size(); ++i) {
        ptr[i] = /* ... */;
    }
}

스크립트 호출 빈도

  • 매 프레임 수천 번 호출되는 함수는 C++로 두고, 스크립트는 이벤트·초기화 수준으로 제한하는 것이 좋습니다.
  • Lua의 __index 메타테이블 남용은 느려지므로, 핫 경로에서는 직접 C 함수 호출을 권장합니다.

9. 프로덕션 패턴

샌드박싱: 제한된 API만 노출

// 위험한 함수는 등록하지 않음
// lua_register(L_, "os.execute", ...);  // ❌
// lua_register(L_, "io.open", ...);     // ❌
luaL_openlibs(L_);  // base, table, string만 필요 시 선택적 로드

// 또는 커스텀 환경에서 표준 라이브러리 제외
lua_State* L = luaL_newstate();
// luaL_openlibs(L);  // 전체 대신
luaopen_base(L);   // 최소한
luaopen_table(L);
luaopen_string(L);
// luaopen_io, luaopen_os 등은 제외

스크립트 핫 리로드

void GameScripting::reload_script(const std::string& path) {
    if (luaL_dofile(L_, path.c_str()) != LUA_OK) {
        log_error("Reload failed: %s", lua_tostring(L_, -1));
        lua_pop(L_, 1);
        return;
    }
    // 기존 상태 유지, 새 함수만 갱신
}

에러 처리 및 로깅

int traceback(lua_State* L) {
    lua_getglobal(L, "debug");
    lua_getfield(L, -1, "traceback");
    lua_pushvalue(L, 1);
    lua_pushinteger(L, 2);
    lua_call(L, 2, 1);
    return 1;
}

// lua_pcall 호출 시 에러 핸들러로 traceback 사용
lua_pushcfunction(L_, traceback);
int err_idx = lua_gettop(L_);
lua_insert(L_, -(nargs + 1));  // 함수 아래에 에러 핸들러
if (lua_pcall(L_, nargs, nresults, err_idx) != LUA_OK) {
    fprintf(stderr, "%s\n", lua_tostring(L_, -1));
    lua_pop(L_, 2);  // 에러 메시지 + traceback
}

스크립트 타임아웃 (Lua)

Lua 자체에는 타임아웃이 없으므로, 후크로 명령 수를 제한합니다.

static void lua_hook(lua_State* L, lua_Debug* ar) {
    (void)ar;
    // 일정 횟수 초과 시 에러
    static int count = 0;
    if (++count > 1000000) {
        luaL_error(L, "script timeout (instruction limit)");
    }
}

lua_sethook(L_, lua_hook, LUA_MASKCOUNT, 10000);
lua_pcall(L_, 0, 0, 0);
lua_sethook(L_, nullptr, 0, 0);

스크립트 버전 관리

배포된 앱과 스크립트 버전이 맞지 않으면 API 호환성 문제가 발생합니다.

// 스크립트 상단에 버전 명시
// -- script_version: 2
// C++에서 파싱해 호환 여부 확인
int get_script_version(lua_State* L, const std::string& path) {
    // 파일 첫 줄에서 "script_version: N" 추출
    // 엔진 최소 버전과 비교
    return 0;
}

디버깅 팁

Lua: debug.traceback()을 에러 핸들러에서 호출해 스택 트레이스 확보. print 대신 C++ 로거로 리다이렉트.

-- Lua에서 디버그 출력
debug_print = function(...) end  -- C++에서 오버라이드
// C++에서 debug_print 등록
lua_register(L_, "debug_print",  -> int {
    const char* msg = luaL_optstring(L, 1, "");
    logger_->debug("[Lua] {}", msg);
    return 0;
});

pybind11: Python 예외를 C++에서 잡을 때 py::error_already_set 사용. py::module::import("traceback").attr("format_exc")로 전체 트레이스백 문자열 획득.

체크리스트

  • 스크립트 API 문서화 (Lua/Python/JS 각각)
  • 위험 함수(os.execute, io 등) 샌드박스에서 제외
  • 스크립트 에러 시 엔진 크래시 방지 (pcall, try-catch)
  • 스크립트 실행 시간/명령 수 제한 (타임아웃)
  • 스크립트 파일 경로 화이트리스트 (임의 경로 로드 방지)
  • 프로덕션 빌드에서 디버그 로그 비활성화
  • 스크립트·엔진 버전 호환성 검사
  • 외부 입력(사용자 스크립트) 검증 및 사이즈 제한

10. 정리

항목LuaPython (pybind11)JavaScript (V8)
용도게임, 임베디드AI, 데이터, 도구웹, Node 공유
통합 방식C API, 스택pybind11 모듈V8 Isolate
성능매우 경량GIL 고려JIT 빠름
샌드박스라이브러리 제한import 제한별도 Context

핵심 원칙:

  1. 용도에 맞는 엔진 선택: 게임→Lua, AI/데이터→Python, 웹 공유→V8
  2. 제한된 API만 노출: 샌드박스로 안전성 확보
  3. 에러 처리 철저히: 스크립트 오류가 엔진 크래시로 이어지지 않게
  4. 성능 병목 구간은 C++ 유지: 스크립트는 이벤트·초기화 위주

자주 묻는 질문 (FAQ)

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

A. 게임 로직, 설정 파일, 핫 리로드, 사용자 스크립트, 플러그인 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


참고 자료

한 줄 요약: Lua·Python·JavaScript 바인딩을 마스터해 게임·도구·플러그인에 스크립팅을 통합할 수 있습니다.


관련 글

  • C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]
  • C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
  • C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
  • C++ 플러그인 시스템 | 동적 로딩·인터페이스·버전 관리 [#55-2]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3