C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]

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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오: Lua 스크립팅이 필요한 순간
  2. Lua C API 기본
  3. 스택 연산 상세
  4. C++에서 Lua로 데이터 전달
  5. Lua에서 C++로 데이터 전달
  6. 테이블 조작
  7. 완전한 Lua 스크립팅 예제
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 프로덕션 패턴
  11. 구현 체크리스트

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_StateLua 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
upvaluelua_pushcclosure로 C++ 객체 전달
에러 처리lua_pcall + traceback, lua_pop 정리

핵심 원칙:

  1. 스택 균형을 항상 유지한다.
  2. luaL_check*로 인자 검증을 한다.
  3. upvalue로 상태를 전달한다.
  4. 샌드박스로 위험 함수를 제외한다.

자주 묻는 질문 (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 ManualProgramming in Lua 책을 참고하세요.


참고 자료


한 줄 요약: Lua C API·스택·테이블·upvalue를 마스터하면 C++ 게임 엔진에 안정적인 Lua 스크립팅을 구축할 수 있습니다.


관련 글

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