C API 실전 — 모듈 작성, userdata, 스레드, 이진 청크
C API 실전 — 모듈 작성, userdata, 스레드, 이진 청크 (C API in Practice — Modules, userdata, Threads, Binary Chunks)
이 절은 루아 C API를 실제로 활용하는 고급 주제를 다뤄요. 모듈을 어떻게 C로 작성하고 등록하는지, userdata로 임의의 C 데이터를 어떻게 노출하는지, 스레드(코루틴)를 C에서 어떻게 다루는지, 그리고 바이너리(컴파일된) 청크를 어떻게 저장·로드하는지를 설명해요. 이 내용은 루아 5.4 Reference Manual 4.x의 실전 적용이에요.
C 모듈 작성하기 (Writing C Modules)
C로 루아 모듈을 작성하려면, 라이브러리를 여는 함수(보통 luaopen_<모듈명>)를 정의하고 luaL_Reg 배열로 함수들을 등록해요.
#include <lua.h>
#include <lauxlib.h>
static int l_square(lua_State *L) {
lua_Number x = luaL_checknumber(L, 1);
lua_pushnumber(L, x * x);
return 1;
}
static const luaL_Reg mylib[] = {
{"square", l_square},
{NULL, NULL} /* 끝 */
};
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
컴파일하고 공유 라이브러리(mylib.so/.dll)로 만든 뒤, 루아에서 require("mylib")로 로드해요. package.cpath에 경로가 있어야 해요.
userdata로 C 데이터 노출 (Exposing C Data via userdata)
lua_newuserdatauv(L, size, nuvalue)로 C 구조체를 담을 메모리를 할당하고, 그 포인터를 lua_touserdata로 얻어 구조체를 저장해요. 대개 __index 메타테이블로 메서드를 제공하고, __gc로 메모리(또는 외부 자원)를 정리해요.
typedef struct { double x, y; } Point;
static int point_new(lua_State *L) {
Point *p = (Point *)lua_newuserdatauv(L, sizeof(Point), 0);
p->x = luaL_checknumber(L, 1);
p->y = luaL_checknumber(L, 2);
luaL_getmetatable(L, "Point");
lua_setmetatable(L, -2);
return 1;
}
static int point_gc(lua_State *L) {
Point *p = (Point *)lua_touserdata(L, 1);
/* 필요한 정리 수행 */
(void)p;
return 0;
}
메타테이블 등록(luaL_newmetatable)과 메서드 설치(luaL_setfuncs)를 결합해 객체 메서드를 제공해요.
스레드와 코루틴 (Threads and Coroutines in C)
C API에서 lua_newthread(L)로 새 스레드를 만들어요. 스레드는 코루틴을 나타내고, lua_resume으로 재개하며 lua_yield로 양보를 지원해요. C 함수가 양보하려면 "연속 호출"(lua_callk, lua_yieldk)을 사용해야 해요.
lua_resume(L, from, nargs, nresults)— 코루틴 재개.lua_yield(L, nresults)/lua_yieldk— 양보.lua_isyieldable(L)— 현재 양보 가능 여부 (메인 스레드나 비-연속 C 함수 안이면 거짓).lua_status(L)—LUA_OK(정상 종료),LUA_YIELD(양보 상태) 등을 반환.
/* 양보 가능한 C 함수 예시 */
static int yielding(lua_State *L) {
return lua_yield(L, lua_gettop(L));
}
바이너리 청크 (Binary Chunks)
lua_dump는 함수의 바이너리(컴파일된) 표현을 만들어요. 이 바이너리를 파일에 저장하면, 나중에 lua_load로 다시 읽어 실행할 수 있어요 (컴파일 생략).
int dump_function(lua_State *L) {
/* 스택 맨 위에 함수가 있다고 가정 */
luaL_Buffer b;
luaL_buffinit(L, &b);
lua_dump(L, luaL_BufferWriter, &b, 1); /* strip=1: 디버그 정보 제거 */
luaL_pushresult(&b);
return 1;
}
lua_load는 문자열/리더에서 컴파일하고, mode 인자로 텍스트("t")·바이너리("b")·둘 다("bt")를 선택해요. 바이너리 청크는 플랫폼·버전 의존적이므로, 배포보다는 캐시 목적에 적합해요.
메모리 할당자 (Custom Allocators)
lua_newstate(allocf, ud)에 사용자 정의 할당자 lua_Alloc을 제공할 수 있어요. lua_setallocf/lua_getallocf로 실행 중에도 바꿀 수 있어요. 할당자는 ptr, osize, nsize 인자를 받아 메모리를 재할당해요. 사용자 정의 할당자는 통계·제한·추적을 구현할 때 유용해요.
static void *my_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
/* nsize==0 이면 해제, ptr==NULL 이면 할당 */
if (nsize == 0) { free(ptr); return NULL; }
return realloc(ptr, nsize);
}
lua_State *L = lua_newstate(my_alloc, NULL);
확장 공간과 전역 설정 (Extra Space and Global Settings)
lua_getextraspace(L)— 상태에 연결된 작은 추가 메모리 블록(설정 데이터 보관 등).lua_newstate의ud— 사용자 데이터 포인터(전역 컨텍스트).- 레지스트리(
LUA_REGISTRYINDEX) — C 코드가 전역으로 값/참조를 보관하는 공간.
실전 체크리스트
- C 함수는 항상
luaL_check*계열로 인자를 검증해요. - userdata에는 항상 메타테이블을 설정해 타입 안전을 확보하고,
luaL_checkudata로 검사해요. - 메모리를 오래 보관하려면
luaL_ref로 참조를, 놓을 때는luaL_unref로 해제해요. - C 모듈은 버전 호환성 확인을 위해 진입점에서
luaL_checkversion(L)을 호출하는 걸 고려해요.
본문
빌드 및 링크
C 모듈은 루아 헤더(lua.h, lauxlib.h, lualib.h)를 포함하고, 루아 라이브러리와 링크하거나 플러그인으로 동적 로드해요. 플랫폼별 컴파일 명령은 루아 배포판의 Makefile/문서를 참조해요.
임베딩 vs 모듈
호스트가 루아를 내장(embed)할 때는 lua_State를 만들고 luaL_openlibs로 라이브러리를 연 뒤, 애플리케이션 전용 함수를 등록해요. 모듈은 require로 로드되는 반면, 임베딩은 상태 생성 시 직접 등록해요.