루아(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;
}
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
실제 라이브러리 바인딩 프로젝트
실무에서 자주 사용되는 이미지 처리 라이브러리 바인딩 예제를 통해 완전한 프로젝트 구조를 살펴보겠습니다.
이미지 처리 라이브러리 바인딩
// 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의 연동은 단순히 성능 향상을 위한 기술이 아닙니다.
각 언어의 장점을 최대화하면서 복잡한 소프트웨어 시스템을 효율적으로 구축할 수 있는 강력한 아키텍처 패턴입니다.
이번 포스트에서 다룬 기법들을 실제 프로젝트에 적용해보시고, 이전 글 - 루아 에러 처리와 디버깅과
함께 읽어보시면 더욱 완성도 높은 루아 프로그래밍이 가능할 것입니다.
다음 시리즈에서는 루아의 고급 메타프로그래밍 기법에 대해 자세히 알아보겠습니다.
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #8: OpenResty로 고성능 웹 서버 구축하기 - Nginx + Lua의 완벽한 조합 (0) | 2025.06.13 |
---|---|
루아 입문 시리즈 #7: 코루틴과 비동기 프로그래밍 - 협력적 멀티태스킹의 완전 정복 (0) | 2025.06.13 |
루아 입문 시리즈 #5: 루아 에러 처리와 디버깅 완벽 가이드 - 안정적인 Lua 애플리케이션 개발을 위한 실전 기법 (0) | 2025.06.13 |
루아 입문 시리즈 #4: 루아 모듈과 패키지 시스템 완벽 가이드 (0) | 2025.06.11 |
루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지 (1) | 2025.05.16 |