C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]
이 글의 핵심
게임 로직·플러그인·핫 리로드가 필요할 때 C++에 Lua를 붙이는 방법. Lua C API, lua_State, 스택 연산, C++↔Lua 데이터 전달, 테이블 조작, 흔한 에러, 베스트 프랙티스, 프로덕션 패턴까지 실전 코드로 정리합니다.
들어가며: “스킬 밸런스 하나 바꾸려고 전체 빌드 15분 걸려요”
실제 겪는 문제 시나리오
게임·도구·플러그인 시스템을 만들 때 자주 겪는 상황입니다. 문제: 게임 로직·밸런스·이벤트 처리를 C++에 직접 넣으면, 수정할 때마다 전체 재컴파일이 필요합니다. 해결: Lua 같은 경량 스크립트 언어를 C++ 엔진에 붙여서, 런타임에 스크립트만 바꿔도 로직을 갱신할 수 있게 합니다. 비유하면 “집(C++ 엔진)은 그대로 두고, 인테리어(로직)만 바꿀 수 있는 것”과 같습니다.
flowchart TD
subgraph wrong[❌ C++ 하드코딩]
W1[밸런스 수정] --> W2[소스 수정]
W2 --> W3[전체 재빌드 15분]
W3 --> W4[테스트]
W4 --> W5[반복 비용 큼]
end
subgraph right[✅ Lua 스크립팅]
R1[밸런스 수정] --> R2[Lua 파일만 수정]
R2 --> R3[재시작 또는 핫 리로드]
R3 --> R4[즉시 테스트]
R4 --> R5[빠른 반복]
end
이 글에서 다루는 것:
- 문제 시나리오: Lua 스크립팅이 필요한 실제 상황
- Lua C API:
lua_State, 스택 연산, 데이터 타입 - 완전한 예제: C++→Lua, Lua→C++, 테이블 조작
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴
요구 환경: C++17 이상, Lua 5.3 이상 (권장: Lua 5.4)
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: Lua 스크립팅이 필요한 순간
- Lua C API 기본
- 스택 연산 상세
- C++에서 Lua로 데이터 전달
- Lua에서 C++로 데이터 전달
- 테이블 조작
- 완전한 Lua 스크립팅 예제
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 문제 시나리오: Lua 스크립팅이 필요한 순간
시나리오 1: 게임 밸런스 수정 시마다 15분 빌드
문제: 스킬 데미지, 이동 속도, 아이템 드롭률 같은 밸런스 값이 C++ 상수로 박혀 있습니다. 기획자가 “이 스킬 데미지를 100에서 120으로” 요청할 때마다 C++ 수정 → 전체 빌드 → 테스트가 반복됩니다.
해결: Lua 테이블로 밸런스 데이터를 분리하고, 런타임에 로드합니다. 스크립트만 수정하면 재시작 없이 적용 가능합니다.
-- balance.lua
return {
skill_damage = 120,
move_speed = 5.0,
drop_rate = 0.15
}
시나리오 2: 사용자 플러그인·모드 지원
문제: 에디터·도구에서 사용자가 커스텀 동작을 추가하고 싶어 합니다. C++ 플러그인 DLL은 빌드 환경이 복잡하고, 보안 위험도 있습니다.
해결: Lua 스크립트로 플러그인 API를 노출하면, 사용자가 스크립트만 작성해 확장할 수 있습니다. 샌드박스로 제한된 API만 제공해 안전하게 합니다.
시나리오 3: 이벤트·퀘스트 시퀀스
문제: 퀘스트·이벤트·대화 시퀀스가 복잡한 조건 분기로 이어집니다. C++에 하드코딩하면 가독성과 유지보수가 어렵습니다.
해결: Lua 테이블이나 스크립트로 이벤트 시퀀스를 정의하면, 기획·스크립터가 직접 수정하기 쉽습니다.
-- quest_events.lua
function on_quest_start(quest_id)
if quest_id == 1 then
spawn_npc("merchant", 100, 200)
show_dialog("Welcome, adventurer!")
end
end
시나리오 4: AI·행동 트리
문제: NPC 행동 로직이 C++에 있으면, “공격 거리 5 → 7로 바꿔볼까?” 같은 작은 실험을 10번 하려면 30분이 걸립니다.
해결: Lua로 행동 트리·AI 조건을 작성하면, 스크립트만 수정해 빠르게 반복할 수 있습니다.
시나리오 5: 설정 파일·데이터 테이블
문제: JSON·XML 파싱은 오버헤드가 있고, C++에서 직접 수정하기 어렵습니다.
해결: Lua 테이블은 문법이 간단하고, dofile로 로드하면 바로 Lua 값으로 사용할 수 있습니다.
2. Lua C API 기본
lua_State란?
lua_State*는 Lua 가상 머신의 핸들입니다. 모든 Lua C API 함수는 이 포인터를 첫 인자로 받습니다. Lua와 C++ 간의 모든 데이터 교환은 스택을 통해 이루어집니다.
flowchart TB
subgraph cpp[C++]
A[게임 엔진] --> B[lua_State*]
end
subgraph lua[Lua]
B --> C[스택]
C --> D[값/함수/테이블]
B --> E[글로벌 환경]
end
Lua 초기화 및 종료
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include <string>
#include <stdexcept>
class LuaEngine {
lua_State* L_ = nullptr;
public:
LuaEngine() {
L_ = luaL_newstate();
if (!L_) {
throw std::runtime_error("luaL_newstate failed");
}
luaL_openlibs(L_); // base, table, string, math 등 표준 라이브러리
}
~LuaEngine() {
if (L_) {
lua_close(L_);
L_ = nullptr;
}
}
lua_State* getState() const { return L_; }
};
주의점:
luaL_newstate(): 새 Lua VM 생성luaL_openlibs(L): 표준 라이브러리(base, table, string, math, io, os 등) 로드lua_close(L): VM 해제, 이후L사용 금지
스택 인덱스 규칙
Lua 스택은 1-based입니다. 스택 바닥은 1, 꼭대기는 lua_gettop(L)로 얻습니다.
| 인덱스 | 의미 |
|---|---|
| 1 | 스택 바닥 (가장 먼저 푸시된 값) |
| -1 | 스택 꼭대기 (가장 최근 푸시된 값) |
| -2 | 꼭대기에서 두 번째 |
// 스택 크기 확인
int top = lua_gettop(L);
// 인덱스 변환: 절대 인덱스 ↔ 상대 인덱스
// 양수: 바닥부터 1, 2, 3, ...
// 음수: 꼭대기부터 -1, -2, -3, ...
3. 스택 연산 상세
푸시 연산 (C++ → 스택)
// 정수
lua_pushinteger(L, 42);
// 부동소수
lua_pushnumber(L, 3.14);
// 문자열 (Lua가 내부 복사본 보관)
lua_pushstring(L, "hello");
// 불리언
lua_pushboolean(L, 1); // true
lua_pushboolean(L, 0); // false
// nil
lua_pushnil(L);
// C 함수를 Lua에 등록
lua_pushcfunction(L, my_c_function);
// light userdata (Lua가 GC하지 않음, 포인터만 저장)
lua_pushlightuserdata(L, ptr);
// nil 반환 (반환값 없을 때)
// return 0;
조회 연산 (스택 → C++)
// 타입 확인
int type = lua_type(L, index);
// LUA_TNIL, LUA_TBOOLEAN, LUA_TLIGHTUSERDATA, LUA_TNUMBER,
// LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD
// 값 읽기 (타입 확인 후)
lua_Integer ival = lua_tointeger(L, index);
lua_Number nval = lua_tonumber(L, index);
const char* sval = lua_tostring(L, index); // Lua가 소유, 수정 금지
bool bval = lua_toboolean(L, index);
void* pval = lua_touserdata(L, index);
// 안전한 조회 (타입 불일치 시 에러)
lua_Integer ival = luaL_checkinteger(L, 1); // 인자 1이 정수가 아니면 에러
lua_Number nval = luaL_checknumber(L, 2);
const char* sval = luaL_checkstring(L, 3);
// 선택적 조회 (기본값 사용)
lua_Integer ival = luaL_optinteger(L, 1, 0); // 없으면 0
const char* sval = luaL_optstring(L, 2, "");
스택 조작
// 꼭대기 값 제거 (1개)
lua_pop(L, 1);
// 인덱스 값을 꼭대기로 복사
lua_pushvalue(L, index);
// 스택 크기 설정 (늘리거나 줄임)
lua_settop(L, new_top);
// 인덱스 삽입 (해당 위치에 꼭대기 값 이동)
lua_insert(L, index);
// 스택 n개 제거
lua_pop(L, n); // lua_settop(L, -(n)-1)와 동일
4. C++에서 Lua로 데이터 전달
C++ 함수를 Lua에 등록
// C 함수 시그니처: int lua_cfunc(lua_State* L)
// 반환값: 스택에 남길 값의 개수
static int lua_add(lua_State* L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);
lua_pushinteger(L, a + b);
return 1; // 반환값 1개
}
static int lua_log(lua_State* L) {
const char* msg = luaL_checkstring(L, 1);
printf("[Lua] %s\n", msg);
return 0; // 반환값 없음
}
void register_api(lua_State* L) {
lua_register(L, "add", lua_add);
lua_register(L, "log", lua_log);
// lua_register는 lua_pushcfunction + lua_setglobal과 동일
}
upvalue로 C++ 객체 전달
Lua C 함수는 upvalue로 외부 데이터를 받을 수 있습니다. lua_pushcclosure로 클로저를 만들 때 upvalue를 묶습니다.
struct GameEngine;
static int lua_create_entity(lua_State* L) {
// upvalue 1에서 GameEngine* 가져옴
auto* engine = static_cast<GameEngine*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!engine) return 0;
int entity_id = engine->createEntity();
lua_pushinteger(L, entity_id);
return 1;
}
void register_entity_api(lua_State* L, GameEngine* engine) {
lua_pushlightuserdata(L, engine); // upvalue로 전달
lua_pushcclosure(L, lua_create_entity, 1); // upvalue 1개
lua_setglobal(L, "create_entity");
}
C++ 구조체/객체를 Lua 테이블로 전달
struct Vec2 {
float x, y;
};
static int push_vec2(lua_State* L, const Vec2& v) {
lua_createtable(L, 0, 2);
lua_pushnumber(L, v.x);
lua_setfield(L, -2, "x");
lua_pushnumber(L, v.y);
lua_setfield(L, -2, "y");
return 1; // 테이블 1개 푸시
}
5. Lua에서 C++로 데이터 전달
Lua 함수 호출 (C++에서)
bool call_lua_function(lua_State* L, const char* func_name, int a, int b) {
lua_getglobal(L, func_name);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
return false;
}
lua_pushinteger(L, a);
lua_pushinteger(L, b);
// lua_pcall(L, 인자 개수, 반환값 개수, 에러 핸들러 인덱스)
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return false;
}
lua_Integer result = lua_tointeger(L, -1);
lua_pop(L, 1);
printf("Result: %lld\n", (long long)result);
return true;
}
Lua 테이블에서 C++로 값 읽기
// Lua: config = { damage = 100, speed = 5.0 }
void read_config(lua_State* L) {
lua_getglobal(L, "config");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return;
}
lua_getfield(L, -1, "damage");
int damage = lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "speed");
double speed = lua_tonumber(L, -1);
lua_pop(L, 1);
lua_pop(L, 1); // config 테이블 제거
printf("damage=%d, speed=%.1f\n", damage, speed);
}
Lua 반환값 처리
// Lua: return a, b, c
// C++에서 여러 반환값 받기
void handle_multiple_returns(lua_State* L) {
int nresults = lua_gettop(L); // 반환값 개수
if (nresults >= 1) {
lua_Integer a = lua_tointeger(L, 1);
// ...
}
if (nresults >= 2) {
lua_Number b = lua_tonumber(L, 2);
// ...
}
lua_settop(L, 0); // 스택 비우기
}
6. 테이블 조작
C++에서 Lua 테이블 생성
// Lua: t = { x = 10, y = 20, name = "player" }
void create_table(lua_State* L) {
lua_createtable(L, 0, 3); // 배열 부분 0, 해시 부분 3
lua_pushinteger(L, 10);
lua_setfield(L, -2, "x");
lua_pushinteger(L, 20);
lua_setfield(L, -2, "y");
lua_pushstring(L, "player");
lua_setfield(L, -2, "name");
lua_setglobal(L, "t");
}
배열 형태 테이블
// Lua: arr = { 10, 20, 30 }
void create_array(lua_State* L) {
lua_createtable(L, 3, 0); // 배열 3개
lua_pushinteger(L, 10);
lua_rawseti(L, -2, 1);
lua_pushinteger(L, 20);
lua_rawseti(L, -2, 2);
lua_pushinteger(L, 30);
lua_rawseti(L, -2, 3);
lua_setglobal(L, "arr");
}
테이블 순회
// Lua: for k, v in pairs(t) do ... end
// C++에서 테이블 순회
void iterate_table(lua_State* L, int table_index) {
lua_pushnil(L); // 첫 번째 키로 nil = 시작
while (lua_next(L, table_index) != 0) {
// 스택: ... key value
// key는 -2, value는 -1
if (lua_type(L, -2) == LUA_TSTRING) {
const char* key = lua_tostring(L, -2);
if (lua_isnumber(L, -1)) {
lua_Number val = lua_tonumber(L, -1);
printf("%s = %g\n", key, val);
}
}
lua_pop(L, 1); // value 제거, key는 다음 next용으로 유지
}
}
Lua 테이블에서 C++로 구조체 읽기
struct Balance {
int damage;
double speed;
std::string name;
};
bool read_balance_from_lua(lua_State* L, const char* table_name, Balance& out) {
lua_getglobal(L, table_name);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return false;
}
lua_getfield(L, -1, "damage");
out.damage = static_cast<int>(luaL_optinteger(L, -1, 0));
lua_pop(L, 1);
lua_getfield(L, -1, "speed");
out.speed = static_cast<double>(luaL_optnumber(L, -1, 1.0));
lua_pop(L, 1);
lua_getfield(L, -1, "name");
out.name = luaL_optstring(L, -1, "");
lua_pop(L, 1);
lua_pop(L, 1); // 테이블 제거
return true;
}
require로 로드한 모듈에서 테이블 가져오기
-- balance.lua
return {
damage = 100,
speed = 5.0,
name = "default"
}
// C++에서 balance.lua 로드 후 테이블 사용
bool load_balance(lua_State* L, const std::string& path) {
if (luaL_dofile(L, path.c_str()) != LUA_OK) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return false;
}
// 스택 꼭대기에 return된 테이블이 있음
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return false;
}
lua_setglobal(L, "balance"); // balance라는 이름으로 저장
return true;
}
7. 완전한 Lua 스크립팅 예제
예제 1: 게임 엔진 API 전체
// game_lua.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) {
if (luaL_dofile(L_, path.c_str()) != LUA_OK) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L_, -1));
lua_pop(L_, 1);
return false;
}
return true;
}
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);
if (lua_pcall(L_, 2, 0, 0) != LUA_OK) {
fprintf(stderr, "on_collision error: %s\n", lua_tostring(L_, -1));
lua_pop(L_, 1);
}
} else {
lua_pop(L_, 1);
}
}
int get_score() const { return score_; }
};
Lua 게임 로직 스크립트
-- init.lua: 게임 초기화
local player = create_entity()
set_position(player, 100, 200)
local coin = create_entity()
set_position(coin, 150, 250)
-- collision_handler.lua: 충돌 시
function on_collision(a_id, b_id)
add_score(10)
destroy_entity(b_id)
end
예제 2: 밸런스 테이블 로드
-- balance.lua
return {
skill_damage = 120,
move_speed = 5.0,
drop_rate = 0.15,
levels = { 100, 250, 500, 1000 }
}
// C++에서 밸런스 로드
struct GameBalance {
int skill_damage;
double move_speed;
double drop_rate;
std::vector<int> levels;
};
bool load_balance(lua_State* L, const std::string& path, GameBalance& out) {
if (luaL_dofile(L, path.c_str()) != LUA_OK) {
return false;
}
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return false;
}
lua_getfield(L, -1, "skill_damage");
out.skill_damage = static_cast<int>(lua_tointeger(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "move_speed");
out.move_speed = lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "drop_rate");
out.drop_rate = lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "levels");
if (lua_istable(L, -1)) {
int len = static_cast<int>(lua_rawlen(L, -1));
out.levels.reserve(len);
for (int i = 1; i <= len; ++i) {
lua_rawgeti(L, -1, i);
out.levels.push_back(static_cast<int>(lua_tointeger(L, -1)));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_pop(L, 1); // balance 테이블
return true;
}
예제 3: CMake 빌드
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(LuaGame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LUA REQUIRED lua5.4)
add_executable(game_app main.cpp game_lua.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
8. 자주 발생하는 에러와 해결법
에러 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"); // 그 다음 스크립트 실행
}
-- ❌ 잘못된 예: create_entity 호출 시점에 아직 등록 안 됨
local id = create_entity() -- nil 호출 에러
에러 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_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); // 에러 메시지 제거
}
// ✅ 호출 전후 스택 높이 일치 확인
int top_before = lua_gettop(L);
// ... 작업 ...
lua_settop(L, top_before); // 복원
에러 4: Lua userdata가 가리키는 C++ 객체 수명
원인: Lua userdata가 가리키는 C++ 객체가 먼저 파괴되면, Lua에서 접근 시 크래시가 발생합니다.
해결법:
// shared_ptr을 userdata로 저장하고, __gc 메타메서드에서 정리
// 또는 Lua가 참조하는 동안 C++ 객체 수명을 연장 (예: 엔진이 소유)
// lightuserdata는 Lua가 GC하지 않으므로, C++ 측에서 수명 관리 필수
에러 5: “module ‘xxx’ not found”
원인: Lua에서 require "mymodule"을 썼는데, package.path에 해당 경로가 없습니다.
해결법:
-- Lua 스크립트 상단에서 경로 추가
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");
lua_pop(L_, 1);
에러 6: lua_tostring 반환값 수명
원인: lua_tostring(L, i) 반환값은 Lua가 관리합니다. lua_pop 후에는 무효화됩니다.
해결법:
// ❌ 잘못된 예
const char* s = lua_tostring(L, -1);
lua_pop(L, 1);
printf("%s\n", s); // s는 이미 무효화됐을 수 있음
// ✅ 올바른 예: 즉시 복사
std::string str = lua_tostring(L, -1);
lua_pop(L, 1);
printf("%s\n", str.c_str());
에러 7: lua_next 사용 시 테이블 무결성
원인: lua_next 순회 중에 테이블을 수정하면 undefined behavior입니다.
해결법: 순회할 값들을 먼저 수집한 뒤 별도로 수정합니다.
9. 베스트 프랙티스
1. 스택 균형 유지
- 모든 C 함수에서
lua_push*와return n개수가 일치해야 합니다. - 에러 시
lua_pop으로 스택 정리 후return 0또는lua_error.
2. luaL_check* / luaL_opt* 사용
lua_tointeger대신luaL_checkinteger로 타입 검증.- 잘못된 인자 시 Lua가 에러 메시지와 함께 중단.
3. upvalue로 상태 전달
- 전역 변수 대신 upvalue로
this포인터 전달. - 스레드 안전성과 명확한 소유권.
4. 에러 핸들러 (traceback)
static 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_pushcfunction(L, traceback); lua_insert(L, err_idx);
// lua_pcall(L, nargs, nresults, err_idx);
5. 스크립트 사전 컴파일
// 반복 실행 시 load 한 번, pcall 여러 번
luaL_loadfile(L_, "update.lua");
// 매 프레임
lua_pushvalue(L_, -1);
lua_pcall(L_, 0, 0, 0);
6. local 사용 권장
-- ❌ 전역 변수 (느림)
player_id = create_entity()
-- ✅ 로컬 변수
local player_id = create_entity()
10. 프로덕션 패턴
패턴 1: 샌드박싱
// 위험한 함수는 등록하지 않음
// luaopen_io, luaopen_os 등 제외
lua_State* L = luaL_newstate();
luaopen_base(L);
luaopen_table(L);
luaopen_string(L);
luaopen_math(L);
// luaopen_io(L); // 제외
// luaopen_os(L); // 제외
패턴 2: 스크립트 타임아웃
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);
패턴 3: 핫 리로드
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;
}
}
패턴 4: 스크립트 버전 검사
-- script_version: 2
int get_script_version(const std::string& path) {
std::ifstream f(path);
std::string line;
if (std::getline(f, line)) {
// "script_version: N" 파싱
}
return 0;
}
패턴 5: LuaJIT 고려
- LuaJIT 사용 시 JIT 컴파일로 10~50배 가속.
luajit개발 패키지로 교체 가능.
11. 구현 체크리스트
-
luaL_newstate/lua_close쌍 호출 - API 등록 후 스크립트 실행
-
luaL_check*/luaL_opt*로 인자 검증 - C 함수 반환값 개수 정확히
- 에러 시
lua_pop으로 스택 정리 -
lua_tostring반환값 즉시 복사 - upvalue로 C++ 객체 전달
- traceback 에러 핸들러 사용
- 샌드박스: 위험 함수 제외
- 스크립트 타임아웃 (후크)
-
package.path설정 (require 사용 시)
정리
| 항목 | 설명 |
|---|---|
| lua_State | Lua VM 핸들, 모든 API의 첫 인자 |
| 스택 | C++↔Lua 데이터 교환 통로, 1-based 인덱스 |
| 푸시 | lua_pushinteger, lua_pushstring, lua_pushcclosure 등 |
| 조회 | lua_tointeger, lua_tostring, luaL_checkinteger 등 |
| 테이블 | lua_createtable, lua_setfield, lua_getfield, lua_rawseti |
| upvalue | lua_pushcclosure로 C++ 객체 전달 |
| 에러 처리 | lua_pcall + traceback, lua_pop 정리 |
핵심 원칙:
- 스택 균형을 항상 유지한다.
- luaL_check*로 인자 검증을 한다.
- upvalue로 상태를 전달한다.
- 샌드박스로 위험 함수를 제외한다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 게임 로직, 밸런스 테이블, 플러그인, 핫 리로드, 사용자 스크립트 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. Lua vs LuaJIT 차이는?
A. LuaJIT은 JIT 컴파일로 10~50배 빠르지만, Lua 5.2 수준 호환입니다. Lua 5.4 최신 기능이 필요하면 표준 Lua를, 성능이 중요하면 LuaJIT을 고려하세요.
Q. 더 깊이 공부하려면?
A. Lua 5.4 Reference Manual과 Programming in Lua 책을 참고하세요.
참고 자료
- Lua 5.4 Reference Manual
- Programming in Lua (4th ed.)
- Lua C API 간단 참조
- C++ 시리즈 #55-2: 동적 로딩
- C++ 시리즈 #55-1: 플러그인 시스템
한 줄 요약: Lua C API·스택·테이블·upvalue를 마스터하면 C++ 게임 엔진에 안정적인 Lua 스크립팅을 구축할 수 있습니다.
관련 글
- C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]
- C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
- C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
- C++ 플러그인 시스템 | 동적 로딩·인터페이스·버전 관리 [#55-2]