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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: 스크립팅이 필요한 순간
- 스크립트 엔진 선택 가이드
- Lua C API 통합
- pybind11 Python 통합
- V8 JavaScript 통합
- 완전한 스크립팅 통합 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화
- 프로덕션 패턴
- 정리
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 단순 | 라이브러리 생태계 제한적 |
| Python | AI·데이터, 도구, 프로토타입 | 풍부한 라이브러리, 생산성 | 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_getglobal → lua_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. 정리
| 항목 | Lua | Python (pybind11) | JavaScript (V8) |
|---|---|---|---|
| 용도 | 게임, 임베디드 | AI, 데이터, 도구 | 웹, Node 공유 |
| 통합 방식 | C API, 스택 | pybind11 모듈 | V8 Isolate |
| 성능 | 매우 경량 | GIL 고려 | JIT 빠름 |
| 샌드박스 | 라이브러리 제한 | import 제한 | 별도 Context |
핵심 원칙:
- 용도에 맞는 엔진 선택: 게임→Lua, AI/데이터→Python, 웹 공유→V8
- 제한된 API만 노출: 샌드박스로 안전성 확보
- 에러 처리 철저히: 스크립트 오류가 엔진 크래시로 이어지지 않게
- 성능 병목 구간은 C++ 유지: 스크립트는 이벤트·초기화 위주
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 게임 로직, 설정 파일, 핫 리로드, 사용자 스크립트, 플러그인 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서(Lua, pybind11, V8)를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
참고 자료
- Lua 5.4 Reference Manual
- pybind11 Documentation
- V8 Embedder’s Guide
- 게임 엔진 기초 #50-3 — Lua 스크립팅 예시
- pybind11 #35-1 — Python 바인딩 상세
한 줄 요약: Lua·Python·JavaScript 바인딩을 마스터해 게임·도구·플러그인에 스크립팅을 통합할 수 있습니다.
관련 글
- C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]
- C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
- C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
- C++ 플러그인 시스템 | 동적 로딩·인터페이스·버전 관리 [#55-2]