프로그래밍 언어 실전 가이드

루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍

devcomet 2025. 6. 13. 13:53
728x90
반응형

루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍
루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍

 

루아(Lua)의 가장 강력한 특징 중 하나는 바로 C 언어와의 완벽한 연동 기능입니다.

이번 포스트에서는 루아와 C를 연동하는 다양한 방법을 살펴보고,

FFI(Foreign Function Interface)를 활용한 고성능 프로그래밍 기법을 마스터해보겠습니다.


루아와 C 연동이 필요한 이유

루아는 스크립트 언어로서 빠른 개발과 유연성을 제공하지만, 계산 집약적인 작업에서는 성능상 한계가 있습니다.

이러한 문제를 해결하기 위해 C 언어의 강력한 성능과 루아의 편의성을 결합하는 것이 바로 루아-C 연동 프로그래밍의 핵심입니다.

주요 활용 사례

게임 개발: 게임 엔진의 핵심 로직은 C/C++로 구현하고, 게임플레이 스크립트는 루아로 작성
임베디드 시스템: 하드웨어 제어는 C로, 설정과 로직은 루아로 분리
웹 서버 확장: Nginx의 OpenResty처럼 C 기반 서버에 루아 스크립트 엔진 통합
과학 계산: 수치 연산 라이브러리는 C로, 데이터 처리 워크플로우는 루아로 관리


루아 C API 기초 이해하기

루아 C API는 루아 가상머신과 C 프로그램 간의 인터페이스를 제공합니다.

모든 루아-C 상호작용은 루아 스택(Lua Stack)을 통해 이루어지며, 이는 루아와 C 간의 데이터 교환을 위한 핵심 메커니즘입니다.

기본 C API 함수들

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

// 루아 상태 생성 및 초기화
lua_State *L = luaL_newstate();
luaL_openlibs(L);

// 스택에 값 푸시
lua_pushinteger(L, 42);
lua_pushstring(L, "Hello, Lua!");
lua_pushboolean(L, 1);

// 스택에서 값 가져오기
int number = lua_tointeger(L, -3);  // 스택 인덱스 -3에서 정수 가져오기
const char* text = lua_tostring(L, -2);  // 스택 인덱스 -2에서 문자열 가져오기
int boolean = lua_toboolean(L, -1);  // 스택 맨 위에서 불린 값 가져오기

// 루아 상태 정리
lua_close(L);

 

 

루아 스택 구조를 보여주는 다이어그램
루아 스택 구조를 보여주는 다이어그램

루아 스택 인덱싱 시스템

루아 스택은 양의 인덱스(1부터 시작)와 음의 인덱스(-1부터 시작)를 모두 지원합니다.

음의 인덱스는 스택의 맨 위부터 카운트하므로, 동적으로 변하는 스택 크기에 관계없이 안정적으로 접근할 수 있습니다.


C에서 루아 함수 호출하기

C 프로그램에서 루아 스크립트의 함수를 호출하는 것은 매우 일반적인 패턴입니다.

이를 통해 비즈니스 로직을 루아로 작성하고, C에서 필요할 때 호출할 수 있습니다.

기본적인 함수 호출 예제

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main() {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // 루아 스크립트 로드 및 실행
    const char* script = 
        "function calculate_area(width, height)\n"
        "    return width * height\n"
        "end\n"
        "\n"
        "function greet(name)\n"
        "    return 'Hello, ' .. name .. '!'\n"
        "end";

    if (luaL_dostring(L, script) != LUA_OK) {
        fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
        return 1;
    }

    // 'calculate_area' 함수 호출
    lua_getglobal(L, "calculate_area");  // 함수를 스택에 푸시
    lua_pushinteger(L, 10);              // 첫 번째 인자
    lua_pushinteger(L, 20);              // 두 번째 인자

    if (lua_pcall(L, 2, 1, 0) != LUA_OK) {  // 2개 인자, 1개 반환값
        fprintf(stderr, "Error calling function: %s\n", lua_tostring(L, -1));
        return 1;
    }

    int area = lua_tointeger(L, -1);
    printf("Area: %d\n", area);  // 출력: Area: 200
    lua_pop(L, 1);  // 반환값을 스택에서 제거

    lua_close(L);
    return 0;
}

에러 처리와 안전한 함수 호출

int safe_call_lua_function(lua_State *L, const char* func_name, 
                          int nargs, int nresults) {
    lua_getglobal(L, func_name);

    if (!lua_isfunction(L, -1)) {
        lua_pop(L, 1);
        printf("Error: '%s' is not a function\n", func_name);
        return -1;
    }

    // 인자들을 함수 위로 이동 (이미 스택에 있다고 가정)
    lua_insert(L, -(nargs + 1));

    if (lua_pcall(L, nargs, nresults, 0) != LUA_OK) {
        printf("Error calling %s: %s\n", func_name, lua_tostring(L, -1));
        lua_pop(L, 1);
        return -1;
    }

    return 0;  // 성공
}

루아에서 C 함수 호출하기

루아 스크립트에서 C 함수를 호출하려면, C 함수를 루아 호출 규약에 맞게 래핑해야 합니다.

모든 루아용 C 함수는 int function_name(lua_State *L) 형태의 시그니처를 가져야 합니다.

C 함수를 루아에 등록하기

#include <math.h>

// 수학 연산을 수행하는 C 함수
static int lua_fast_multiply(lua_State *L) {
    // 인자 개수 확인
    int n = lua_gettop(L);
    if (n != 2) {
        return luaL_error(L, "Expected 2 arguments, got %d", n);
    }

    // 인자 타입 확인 및 변환
    luaL_checktype(L, 1, LUA_TNUMBER);
    luaL_checktype(L, 2, LUA_TNUMBER);

    double a = lua_tonumber(L, 1);
    double b = lua_tonumber(L, 2);

    // 계산 수행
    double result = a * b;

    // 결과를 스택에 푸시
    lua_pushnumber(L, result);

    return 1;  // 반환값의 개수
}

// 문자열 처리 C 함수
static int lua_reverse_string(lua_State *L) {
    size_t len;
    const char* str = luaL_checklstring(L, 1, &len);

    // 메모리 할당
    char* reversed = malloc(len + 1);
    if (!reversed) {
        return luaL_error(L, "Memory allocation failed");
    }

    // 문자열 뒤집기
    for (size_t i = 0; i < len; i++) {
        reversed[i] = str[len - 1 - i];
    }
    reversed[len] = '\0';

    lua_pushstring(L, reversed);
    free(reversed);

    return 1;
}

// 함수 등록 테이블
static const luaL_Reg mylib[] = {
    {"fast_multiply", lua_fast_multiply},
    {"reverse_string", lua_reverse_string},
    {NULL, NULL}  // 배열 종료 표시
};

// 라이브러리 초기화 함수
int luaopen_mylib(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;
}

int main() {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // 사용자 정의 라이브러리 등록
    luaL_requiref(L, "mylib", luaopen_mylib, 1);
    lua_pop(L, 1);

    // 루아 스크립트 실행
    const char* script = 
        "local mylib = require('mylib')\n"
        "print('Fast multiply:', mylib.fast_multiply(123.45, 67.89))\n"
        "print('Reversed string:', mylib.reverse_string('Hello World'))";

    if (luaL_dostring(L, script) != LUA_OK) {
        fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    }

    lua_close(L);
    return 0;
}

 

 

루아-C 함수 호출 플로우차트
루아-C 함수 호출 플로우차트


LuaJIT FFI를 활용한 고급 연동

LuaJIT의 FFI(Foreign Function Interface)는 별도의 바인딩 코드 없이도 C 라이브러리를 직접 호출할 수 있는 혁신적인 기능입니다.

이를 통해 개발 생산성을 크게 향상시킬 수 있습니다.

FFI 기본 사용법

local ffi = require("ffi")

-- C 함수와 구조체 선언
ffi.cdef[[
    // 표준 C 라이브러리 함수들
    int printf(const char *fmt, ...);
    void *malloc(size_t size);
    void free(void *ptr);

    // 사용자 정의 구조체
    typedef struct {
        int x, y;
        char name[32];
    } Point;

    // 수학 함수들
    double sin(double x);
    double cos(double x);
    double sqrt(double x);
]]

-- C 함수 직접 호출
ffi.C.printf("Hello from C! Number: %d\n", 42)

-- 구조체 생성 및 사용
local point = ffi.new("Point")
point.x = 10
point.y = 20
ffi.copy(point.name, "MyPoint")

print(string.format("Point: (%d, %d) - %s", point.x, point.y, ffi.string(point.name)))

-- 수학 연산
local angle = math.pi / 4
local sin_val = ffi.C.sin(angle)
local cos_val = ffi.C.cos(angle)
print(string.format("sin(π/4) = %.6f, cos(π/4) = %.6f", sin_val, cos_val))

 

외부 라이브러리 연동 예제

local ffi = require("ffi")

-- 동적 라이브러리 로드 (Linux/macOS의 경우)
local libm = ffi.load("m")  -- 수학 라이브러리

-- 또는 Windows의 경우
-- local msvcrt = ffi.load("msvcrt")

-- 고성능 배열 처리
ffi.cdef[[
    typedef struct {
        double *data;
        size_t length;
        size_t capacity;
    } DoubleArray;

    DoubleArray* create_array(size_t capacity);
    void destroy_array(DoubleArray* arr);
    void array_push(DoubleArray* arr, double value);
    double array_sum(DoubleArray* arr);
    void array_sort(DoubleArray* arr);
]]

-- C 라이브러리 구현 (별도 .so/.dll 파일)
local mylib = ffi.load("./mymath")  -- mymath.so 또는 mymath.dll

-- 대용량 데이터 처리 예제
local function process_large_dataset(size)
    local start_time = os.clock()

    -- C 구조체로 배열 생성
    local arr = mylib.create_array(size)

    -- 데이터 추가
    for i = 1, size do
        mylib.array_push(arr, math.random() * 1000)
    end

    -- C에서 정렬 수행 (매우 빠름)
    mylib.array_sort(arr)

    -- 합계 계산
    local sum = mylib.array_sum(arr)

    -- 메모리 해제
    mylib.destroy_array(arr)

    local end_time = os.clock()

    return sum, end_time - start_time
end

-- 성능 테스트
local sum, elapsed = process_large_dataset(1000000)
print(string.format("Sum: %.2f, Time: %.4f seconds", sum, elapsed))

성능 최적화 전략

루아와 C의 연동에서 최고의 성능을 얻기 위해서는 몇 가지 중요한 최적화 전략을 이해해야 합니다.

데이터 타입 최적화

local ffi = require("ffi")

-- 효율적인 구조체 설계
ffi.cdef[[
    // 메모리 정렬을 고려한 구조체
    typedef struct {
        double position[3];    // 8바이트 정렬
        float velocity[3];     // 4바이트 정렬
        int32_t id;           // 4바이트
        uint8_t flags;        // 1바이트
        uint8_t padding[3];   // 패딩으로 정렬 보장
    } Particle;

    // 배치 처리를 위한 구조체
    typedef struct {
        Particle *particles;
        size_t count;
        size_t capacity;
    } ParticleSystem;
]]

-- 메모리 풀 활용 예제
local ParticlePool = {}
ParticlePool.__index = ParticlePool

function ParticlePool:new(max_particles)
    local pool = {
        particles = ffi.new("Particle[?]", max_particles),
        count = 0,
        capacity = max_particles,
        free_list = {}
    }
    return setmetatable(pool, self)
end

function ParticlePool:allocate()
    if #self.free_list > 0 then
        return table.remove(self.free_list)
    elseif self.count < self.capacity then
        local particle = self.particles + self.count
        self.count = self.count + 1
        return particle
    else
        return nil  -- 풀이 가득 참
    end
end

function ParticlePool:deallocate(particle)
    table.insert(self.free_list, particle)
end

배치 처리 최적화

-- 개별 호출 vs 배치 처리 비교

-- 비효율적인 방법: 개별 호출
local function update_particles_individually(particles, dt)
    for i = 1, #particles do
        update_single_particle(particles[i], dt)  -- C 함수 호출
    end
end

-- 효율적인 방법: 배치 처리
local function update_particles_batch(particles, dt)
    -- 한 번의 C 함수 호출로 모든 파티클 업데이트
    update_particle_batch(particles.data, particles.count, dt)
end

-- 성능 측정 및 비교
local function benchmark_particle_update()
    local particle_count = 100000
    local particles = create_particle_system(particle_count)

    -- 개별 처리 방식 측정
    local start = os.clock()
    for frame = 1, 100 do
        update_particles_individually(particles, 0.016)
    end
    local individual_time = os.clock() - start

    -- 배치 처리 방식 측정
    start = os.clock()
    for frame = 1, 100 do
        update_particles_batch(particles, 0.016)
    end
    local batch_time = os.clock() - start

    print(string.format("Individual: %.4fs, Batch: %.4fs, Speedup: %.2fx", 
          individual_time, batch_time, individual_time / batch_time))
end

 

 

개별 처리 vs 배치 처리 성능 비교 그래프
개별 처리 vs 배치 처리 성능 비교 그래프


실제 라이브러리 바인딩 프로젝트

실무에서 자주 사용되는 이미지 처리 라이브러리 바인딩 예제를 통해 완전한 프로젝트 구조를 살펴보겠습니다.

이미지 처리 라이브러리 바인딩

// imagelib.c - C 라이브러리 구현
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int width;
    int height;
    int channels;
    unsigned char *data;
} Image;

// 이미지 생성
static int lua_create_image(lua_State *L) {
    int width = luaL_checkinteger(L, 1);
    int height = luaL_checkinteger(L, 2);
    int channels = luaL_optinteger(L, 3, 3);  // 기본값 3 (RGB)

    if (width <= 0 || height <= 0 || channels <= 0) {
        return luaL_error(L, "Invalid image dimensions");
    }

    Image *img = (Image*)lua_newuserdata(L, sizeof(Image));
    img->width = width;
    img->height = height;
    img->channels = channels;

    size_t data_size = width * height * channels;
    img->data = malloc(data_size);
    if (!img->data) {
        return luaL_error(L, "Memory allocation failed");
    }

    memset(img->data, 0, data_size);  // 0으로 초기화

    // 메타테이블 설정
    luaL_getmetatable(L, "Image");
    lua_setmetatable(L, -2);

    return 1;
}

// 픽셀 값 설정
static int lua_set_pixel(lua_State *L) {
    Image *img = (Image*)luaL_checkudata(L, 1, "Image");
    int x = luaL_checkinteger(L, 2);
    int y = luaL_checkinteger(L, 3);

    if (x < 0 || x >= img->width || y < 0 || y >= img->height) {
        return luaL_error(L, "Pixel coordinates out of bounds");
    }

    int base_idx = (y * img->width + x) * img->channels;

    for (int c = 0; c < img->channels; c++) {
        double value = luaL_checknumber(L, 4 + c);
        img->data[base_idx + c] = (unsigned char)(value * 255.0);
    }

    return 0;
}

// 가우시안 블러 필터 적용
static int lua_gaussian_blur(lua_State *L) {
    Image *img = (Image*)luaL_checkudata(L, 1, "Image");
    double sigma = luaL_checknumber(L, 2);

    // 간단한 가우시안 블러 구현
    int kernel_size = (int)(sigma * 6) | 1;  // 홀수로 만들기
    int half_kernel = kernel_size / 2;

    // 가우시안 커널 생성
    double *kernel = malloc(kernel_size * sizeof(double));
    double sum = 0.0;

    for (int i = 0; i < kernel_size; i++) {
        int x = i - half_kernel;
        kernel[i] = exp(-(x * x) / (2.0 * sigma * sigma));
        sum += kernel[i];
    }

    // 정규화
    for (int i = 0; i < kernel_size; i++) {
        kernel[i] /= sum;
    }

    // 임시 버퍼 할당
    size_t data_size = img->width * img->height * img->channels;
    unsigned char *temp = malloc(data_size);
    memcpy(temp, img->data, data_size);

    // 수평 블러
    for (int y = 0; y < img->height; y++) {
        for (int x = 0; x < img->width; x++) {
            for (int c = 0; c < img->channels; c++) {
                double sum = 0.0;
                for (int k = 0; k < kernel_size; k++) {
                    int sample_x = x + k - half_kernel;
                    if (sample_x >= 0 && sample_x < img->width) {
                        int idx = (y * img->width + sample_x) * img->channels + c;
                        sum += temp[idx] * kernel[k];
                    }
                }
                int target_idx = (y * img->width + x) * img->channels + c;
                img->data[target_idx] = (unsigned char)sum;
            }
        }
    }

    free(kernel);
    free(temp);

    return 0;
}

// 가비지 컬렉션 메타메서드
static int lua_image_gc(lua_State *L) {
    Image *img = (Image*)luaL_checkudata(L, 1, "Image");
    if (img->data) {
        free(img->data);
        img->data = NULL;
    }
    return 0;
}

// 라이브러리 함수 테이블
static const luaL_Reg imagelib_functions[] = {
    {"create", lua_create_image},
    {NULL, NULL}
};

// 이미지 메서드 테이블
static const luaL_Reg image_methods[] = {
    {"set_pixel", lua_set_pixel},
    {"gaussian_blur", lua_gaussian_blur},
    {"__gc", lua_image_gc},
    {NULL, NULL}
};

// 라이브러리 초기화
int luaopen_imagelib(lua_State *L) {
    // 이미지 메타테이블 생성
    luaL_newmetatable(L, "Image");
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
    luaL_setfuncs(L, image_methods, 0);

    // 라이브러리 테이블 생성
    luaL_newlib(L, imagelib_functions);

    return 1;
}

루아에서 라이브러리 사용

-- imagelib 사용 예제
local imagelib = require("imagelib")

-- 이미지 생성 및 처리
local function create_gradient_image()
    local width, height = 256, 256
    local img = imagelib.create(width, height, 3)  -- RGB 이미지

    -- 그라디언트 생성
    for y = 0, height - 1 do
        for x = 0, width - 1 do
            local r = x / (width - 1)   -- 빨간색 그라디언트
            local g = y / (height - 1)  -- 초록색 그라디언트
            local b = 0.5               -- 파란색 고정값

            img:set_pixel(x, y, r, g, b)
        end
    end

    -- 가우시안 블러 적용
    img:gaussian_blur(2.0)

    return img
end

-- 성능 측정
local function benchmark_image_processing()
    local start_time = os.clock()

    local images = {}
    for i = 1, 10 do
        images[i] = create_gradient_image()
    end

    local end_time = os.clock()
    local elapsed = end_time - start_time

    print(string.format("Created and processed %d images in %.4f seconds", 
          #images, elapsed))
    print(string.format("Average time per image: %.4f seconds", 
          elapsed / #images))
end

benchmark_image_processing()

메모리 관리와 안전성

루아-C 연동에서 가장 중요한 측면 중 하나는 안전한 메모리 관리입니다.

C에서 할당한 메모리는 반드시 해제해야 하며, 루아의 가비지 컬렉터와 협력해야 합니다.

안전한 메모리 관리 패턴

// 안전한 버퍼 관리 구조체
typedef struct {
    char *data;
    size_t size;
    size_t capacity;
    int is_owned;  // 메모리 소유권 플래그
} SafeBuffer;

static int lua_create_buffer(lua_State *L) {
    size_t initial_capacity = luaL_optinteger(L, 1, 1024);

    SafeBuffer *buf = (SafeBuffer*)lua_newuserdata(L, sizeof(SafeBuffer));
    buf->data = malloc(initial_capacity);
    if (!buf->data) {
        return luaL_error(L, "Memory allocation failed");
    }

    buf->size = 0;
    buf->capacity = initial_capacity;
    buf->is_owned = 1;  // 이 구조체가 메모리를 소유함

    // 메타테이블 설정으로 가비지 컬렉션 처리
    luaL_getmetatable(L, "SafeBuffer");
    lua_setmetatable(L, -2);

    return 1;
}

static int lua_buffer_gc(lua_State *L) {
    SafeBuffer *buf = (SafeBuffer*)luaL_checkudata(L, 1, "SafeBuffer");
    if (buf->data && buf->is_owned) {
        free(buf->data);
        buf->data = NULL;
        buf->is_owned = 0;
    }
    return 0;
}

// 예외 안전성을 위한 래퍼 함수
static int safe_lua_operation(lua_State *L) {
    int result = 0;
    char *temp_buffer = NULL;

    // 루아 예외 처리를 위한 pcall 사용
    lua_pushcfunction(L, lua_actual_operation);
    lua_pushvalue(L, 1);  // 첫 번째 인자 복사

    if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
        // 에러 발생 시 정리 작업
        if (temp_buffer) {
            free(temp_buffer);
        }
        lua_error(L);  // 에러 재발생
    }

    return 1;  // 성공적으로 완료
}

리소스 자동 정리 (RAII 패턴)

-- 루아에서 RAII 패턴 구현
local ResourceManager = {}
ResourceManager.__index = ResourceManager

function ResourceManager:new()
    local rm = {
        resources = {},
        cleanup_functions = {}
    }
    return setmetatable(rm, self)
end

function ResourceManager:add_resource(resource, cleanup_func)
    table.insert(self.resources, resource)
    table.insert(self.cleanup_functions, cleanup_func)
end

function ResourceManager:cleanup()
    for i = #self.cleanup_functions, 1, -1 do
        local success, err = pcall(self.cleanup_functions[i], self.resources[i])
        if not success then
            print("Warning: Resource cleanup failed:", err)
        end
    end
    self.resources = {}
    self.cleanup_functions = {}
end

-- 자동 정리를 위한 메타메서드
ResourceManager.__gc = ResourceManager.cleanup

-- 사용 예제
local function process_large_file(filename)
    local rm = ResourceManager:new()

    -- 파일 리소스 관리
    local file = io.open(filename, "rb")
    if not file then
        error("Cannot open file: " .. filename)
    end
    rm:add_resource(file, function(f) f:close() end)

    -- C 라이브러리 버퍼 관리
    local buffer = create_c_buffer(1024 * 1024)  -- 1MB 버퍼
    rm:add_resource(buffer, function(buf) destroy_c_buffer(buf) end)

    -- 실제 처리 작업
    local data = file:read("*all")
    local processed = process_data_with_c(buffer, data)

    -- 함수 종료 시 자동으로 모든 리소스 정리됨
    return processed
end

동적 라이브러리 로딩과 배포

실제 프로덕션 환경에서는 동적 라이브러리(.so, .dll, .dylib)를 효율적으로 로딩하고 배포하는 전략이 필요합니다.

크로스 플랫폼 라이브러리 로딩

local ffi = require("ffi")

-- 플랫폼별 라이브러리 확장자 검출
local function get_library_extension()
    if ffi.os == "Windows" then
        return ".dll"
    elseif ffi.os == "OSX" then
        return ".dylib"
    else
        return ".so"
    end
end

-- 안전한 라이브러리 로딩
local function load_library_safe(lib_name, fallback_paths)
    local extension = get_library_extension()
    local full_name = lib_name .. extension

    -- 표준 경로에서 시도
    local success, lib = pcall(ffi.load, full_name)
    if success then
        return lib
    end

    -- 대체 경로들에서 시도
    for _, path in ipairs(fallback_paths or {}) do
        local full_path = path .. "/" .. full_name
        success, lib = pcall(ffi.load, full_path)
        if success then
            return lib
        end
    end

    error(string.format("Failed to load library: %s", lib_name))
end

-- 사용 예제
local mylib = load_library_safe("mymath", {
    "./lib",
    "/usr/local/lib",
    "/opt/myapp/lib"
})

버전 호환성 관리

-- 라이브러리 버전 체크 시스템
local function check_library_version(lib, min_version)
    if not lib.get_version then
        print("Warning: Library version check not supported")
        return true
    end

    local current_version = lib.get_version()

    -- 간단한 버전 비교 (major.minor.patch 형식)
    local function parse_version(version_str)
        local major, minor, patch = version_str:match("(%d+)%.(%d+)%.(%d+)")
        return {
            major = tonumber(major) or 0,
            minor = tonumber(minor) or 0,
            patch = tonumber(patch) or 0
        }
    end

    local current = parse_version(ffi.string(current_version))
    local minimum = parse_version(min_version)

    if current.major > minimum.major then
        return true
    elseif current.major == minimum.major then
        if current.minor > minimum.minor then
            return true
        elseif current.minor == minimum.minor then
            return current.patch >= minimum.patch
        end
    end

    error(string.format("Library version %s is older than required %s", 
          ffi.string(current_version), min_version))
end

디버깅과 프로파일링

루아-C 연동 코드의 디버깅과 성능 프로파일링은 일반적인 루아 코드보다 더 복잡합니다.

메모리 누수 검출

// 메모리 디버깅을 위한 래퍼 함수들
#ifdef DEBUG_MEMORY
static size_t total_allocated = 0;
static int allocation_count = 0;

void* debug_malloc(size_t size, const char* file, int line) {
    void* ptr = malloc(size + sizeof(size_t));
    if (ptr) {
        *(size_t*)ptr = size;
        total_allocated += size;
        allocation_count++;
        printf("MALLOC: %zu bytes at %p (%s:%d) - Total: %zu\n", 
               size, (char*)ptr + sizeof(size_t), file, line, total_allocated);
        return (char*)ptr + sizeof(size_t);
    }
    return NULL;
}

void debug_free(void* ptr, const char* file, int line) {
    if (ptr) {
        void* real_ptr = (char*)ptr - sizeof(size_t);
        size_t size = *(size_t*)real_ptr;
        total_allocated -= size;
        allocation_count--;
        printf("FREE: %zu bytes at %p (%s:%d) - Total: %zu\n", 
               size, ptr, file, line, total_allocated);
        free(real_ptr);
    }
}

#define MALLOC(size) debug_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) debug_free(ptr, __FILE__, __LINE__)
#else
#define MALLOC(size) malloc(size)
#define FREE(ptr) free(ptr)
#endif

// 메모리 누수 체크를 위한 루아 함수
static int lua_check_memory_leaks(lua_State *L) {
#ifdef DEBUG_MEMORY
    lua_pushinteger(L, total_allocated);
    lua_pushinteger(L, allocation_count);
    return 2;
#else
    lua_pushinteger(L, 0);
    lua_pushinteger(L, 0);
    return 2;
#endif
}

성능 프로파일링 도구

-- 고정밀 타이밍 측정
local function precise_timer()
    if jit and jit.os == "Windows" then
        -- Windows에서 고정밀 타이머 사용
        local ffi = require("ffi")
        ffi.cdef[[
            int QueryPerformanceCounter(int64_t *lpPerformanceCount);
            int QueryPerformanceFrequency(int64_t *lpFrequency);
        ]]

        local freq = ffi.new("int64_t[1]")
        local count = ffi.new("int64_t[1]")
        ffi.C.QueryPerformanceFrequency(freq)

        return function()
            ffi.C.QueryPerformanceCounter(count)
            return tonumber(count[0]) / tonumber(freq[0])
        end
    else
        -- POSIX 시스템에서 clock_gettime 사용
        return function()
            return os.clock()
        end
    end
end

-- 함수 실행 시간 측정 데코레이터
local function profile_function(func, name)
    local timer = precise_timer()
    local total_time = 0
    local call_count = 0

    return function(...)
        local start_time = timer()
        local results = {func(...)}
        local end_time = timer()

        local elapsed = end_time - start_time
        total_time = total_time + elapsed
        call_count = call_count + 1

        if call_count % 1000 == 0 then
            print(string.format("%s: %d calls, %.4fs total, %.6fs avg", 
                  name or "function", call_count, total_time, total_time / call_count))
        end

        return table.unpack(results)
    end
end

-- 사용 예제
local fast_multiply = profile_function(mylib.fast_multiply, "fast_multiply")
local reverse_string = profile_function(mylib.reverse_string, "reverse_string")

-- 성능 테스트 실행
for i = 1, 10000 do
    fast_multiply(i, i + 1)
    reverse_string("Hello World " .. i)
end

실전 응용: 게임 엔진 통합

마지막으로 실제 게임 개발에서 루아-C 연동을 어떻게 활용하는지 살펴보겠습니다.

게임 엔진 스크립팅 시스템

// game_engine.c - 게임 엔진 핵심 부분
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

typedef struct {
    float x, y, z;
} Vector3;

typedef struct {
    int id;
    Vector3 position;
    Vector3 velocity;
    float health;
    int active;
} GameObject;

static GameObject game_objects[1000];
static int object_count = 0;

// 게임 오브젝트 생성
static int lua_create_game_object(lua_State *L) {
    if (object_count >= 1000) {
        return luaL_error(L, "Maximum game objects reached");
    }

    GameObject *obj = &game_objects[object_count];
    obj->id = object_count;
    obj->position.x = luaL_optnumber(L, 1, 0.0f);
    obj->position.y = luaL_optnumber(L, 2, 0.0f);
    obj->position.z = luaL_optnumber(L, 3, 0.0f);
    obj->velocity.x = obj->velocity.y = obj->velocity.z = 0.0f;
    obj->health = luaL_optnumber(L, 4, 100.0f);
    obj->active = 1;

    lua_pushinteger(L, object_count);
    object_count++;

    return 1;
}

// 게임 오브젝트 이동
static int lua_move_object(lua_State *L) {
    int id = luaL_checkinteger(L, 1);
    float dx = luaL_checknumber(L, 2);
    float dy = luaL_checknumber(L, 3);
    float dz = luaL_optnumber(L, 4, 0.0f);

    if (id < 0 || id >= object_count || !game_objects[id].active) {
        return luaL_error(L, "Invalid game object ID");
    }

    GameObject *obj = &game_objects[id];
    obj->position.x += dx;
    obj->position.y += dy;
    obj->position.z += dz;

    return 0;
}

// 충돌 검사 (C에서 고성능으로 처리)
static int lua_check_collisions(lua_State *L) {
    int collisions = 0;
    lua_newtable(L);  // 충돌 결과 테이블

    for (int i = 0; i < object_count; i++) {
        if (!game_objects[i].active) continue;

        for (int j = i + 1; j < object_count; j++) {
            if (!game_objects[j].active) continue;

            GameObject *obj1 = &game_objects[i];
            GameObject *obj2 = &game_objects[j];

            // 간단한 거리 기반 충돌 검사
            float dx = obj1->position.x - obj2->position.x;
            float dy = obj1->position.y - obj2->position.y;
            float dz = obj1->position.z - obj2->position.z;
            float distance_sq = dx*dx + dy*dy + dz*dz;

            if (distance_sq < 4.0f) {  // 충돌 거리 임계값
                // 충돌 정보를 테이블에 추가
                lua_pushinteger(L, collisions + 1);
                lua_newtable(L);

                lua_pushstring(L, "obj1");
                lua_pushinteger(L, obj1->id);
                lua_settable(L, -3);

                lua_pushstring(L, "obj2");
                lua_pushinteger(L, obj2->id);
                lua_settable(L, -3);

                lua_pushstring(L, "distance");
                lua_pushnumber(L, sqrt(distance_sq));
                lua_settable(L, -3);

                lua_settable(L, -3);  // 충돌 결과 테이블에 추가
                collisions++;
            }
        }
    }

    return 1;  // 충돌 결과 테이블 반환
}

// 게임 엔진 함수 등록
static const luaL_Reg game_engine_lib[] = {
    {"create_object", lua_create_game_object},
    {"move_object", lua_move_object},
    {"check_collisions", lua_check_collisions},
    {NULL, NULL}
};

int luaopen_game_engine(lua_State *L) {
    luaL_newlib(L, game_engine_lib);
    return 1;
}

게임 로직 스크립트

-- game_logic.lua - 루아로 작성된 게임 로직
local engine = require("game_engine")

-- 게임 상태 관리
local GameState = {
    players = {},
    enemies = {},
    projectiles = {},
    score = 0,
    level = 1
}

-- 플레이어 생성
function GameState:create_player(x, y)
    local player_id = engine.create_object(x, y, 0, 100)
    table.insert(self.players, {
        id = player_id,
        type = "player",
        last_shot = 0
    })
    return player_id
end

-- 적 생성
function GameState:spawn_enemy()
    local x = math.random(-50, 50)
    local y = math.random(30, 50)
    local enemy_id = engine.create_object(x, y, 0, 50)

    table.insert(self.enemies, {
        id = enemy_id,
        type = "enemy",
        ai_state = "patrol",
        target_x = math.random(-30, 30),
        speed = 0.5 + math.random() * 0.5
    })

    return enemy_id
end

-- AI 업데이트 (루아에서 복잡한 로직 처리)
function GameState:update_enemy_ai(enemy, dt)
    if enemy.ai_state == "patrol" then
        -- 목표 지점으로 이동
        local dx = enemy.target_x - self:get_object_x(enemy.id)
        if math.abs(dx) < 0.5 then
            enemy.target_x = math.random(-30, 30)
        end

        local move_x = dx > 0 and enemy.speed * dt or -enemy.speed * dt
        engine.move_object(enemy.id, move_x, -enemy.speed * dt * 0.5, 0)

        -- 플레이어 감지 시 추적 모드로 전환
        for _, player in ipairs(self.players) do
            local distance = self:get_distance(enemy.id, player.id)
            if distance < 10 then
                enemy.ai_state = "chase"
                enemy.target_id = player.id
                break
            end
        end

    elseif enemy.ai_state == "chase" then
        -- 플레이어 추적
        if enemy.target_id and self:is_object_active(enemy.target_id) then
            local target_x = self:get_object_x(enemy.target_id)
            local current_x = self:get_object_x(enemy.id)
            local dx = target_x - current_x

            local move_x = dx > 0 and enemy.speed * dt * 1.5 or -enemy.speed * dt * 1.5
            engine.move_object(enemy.id, move_x, -enemy.speed * dt, 0)

            -- 너무 멀어지면 순찰 모드로 복귀
            local distance = self:get_distance(enemy.id, enemy.target_id)
            if distance > 20 then
                enemy.ai_state = "patrol"
                enemy.target_id = nil
            end
        else
            enemy.ai_state = "patrol"
        end
    end
end

-- 게임 메인 루프
function GameState:update(dt)
    -- 적 AI 업데이트
    for _, enemy in ipairs(self.enemies) do
        if self:is_object_active(enemy.id) then
            self:update_enemy_ai(enemy, dt)
        end
    end

    -- 충돌 검사 (C에서 고성능 처리)
    local collisions = engine.check_collisions()

    -- 충돌 처리 (루아에서 게임 로직)
    for _, collision in ipairs(collisions) do
        self:handle_collision(collision.obj1, collision.obj2, collision.distance)
    end

    -- 레벨 진행 체크
    if #self.enemies == 0 then
        self:next_level()
    end
end

-- 충돌 처리
function GameState:handle_collision(id1, id2, distance)
    local obj1_type = self:get_object_type(id1)
    local obj2_type = self:get_object_type(id2)

    if (obj1_type == "player" and obj2_type == "enemy") or 
       (obj1_type == "enemy" and obj2_type == "player") then
        -- 플레이어-적 충돌
        self:damage_object(id1, 25)
        self:damage_object(id2, 25)

    elseif (obj1_type == "projectile" and obj2_type == "enemy") or
           (obj1_type == "enemy" and obj2_type == "projectile") then
        -- 투사체-적 충돌
        local enemy_id = obj1_type == "enemy" and id1 or id2
        local projectile_id = obj1_type == "projectile" and id1 or id2

        self:damage_object(enemy_id, 50)
        self:destroy_object(projectile_id)
        self.score = self.score + 10
    end
end

-- 게임 상태 출력
function GameState:print_status()
    print(string.format("Level: %d, Score: %d, Players: %d, Enemies: %d", 
          self.level, self.score, #self.players, #self.enemies))
end

return GameState

마무리 및 베스트 프랙티스

루아와 C의 성공적인 연동을 위해서는 다음과 같은 베스트 프랙티스를 따라야 합니다.

핵심 가이드라인

성능을 위한 설계: 빈번히 호출되는 함수는 C로, 복잡한 로직은 루아로 분리합니다.

메모리 안전성: 모든 C에서 할당한 메모리는 반드시 해제하고, 루아의 가비지 컬렉터와 협력합니다.

에러 처리: C 함수에서는 적절한 에러 메시지와 함께 luaL_error를 사용하고, 루아에서는 pcall로 안전하게 호출합니다.

버전 호환성: 라이브러리 버전을 체크하고, 호환성 문제를 사전에 방지합니다.

디버깅 지원: 개발 단계에서는 메모리 디버깅과 성능 프로파일링 도구를 적극 활용합니다.

 

루아와 C의 연동은 단순히 성능 향상을 위한 기술이 아닙니다.

각 언어의 장점을 최대화하면서 복잡한 소프트웨어 시스템을 효율적으로 구축할 수 있는 강력한 아키텍처 패턴입니다.

이번 포스트에서 다룬 기법들을 실제 프로젝트에 적용해보시고, 이전 글 - 루아 에러 처리와 디버깅

함께 읽어보시면 더욱 완성도 높은 루아 프로그래밍이 가능할 것입니다.

다음 시리즈에서는 루아의 고급 메타프로그래밍 기법에 대해 자세히 알아보겠습니다.

728x90
반응형