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_newstateud — 사용자 데이터 포인터(전역 컨텍스트).
  • 레지스트리(LUA_REGISTRYINDEX) — C 코드가 전역으로 값/참조를 보관하는 공간.

실전 체크리스트

  • C 함수는 항상 luaL_check* 계열로 인자를 검증해요.
  • userdata에는 항상 메타테이블을 설정해 타입 안전을 확보하고, luaL_checkudata로 검사해요.
  • 메모리를 오래 보관하려면 luaL_ref로 참조를, 놓을 때는 luaL_unref로 해제해요.
  • C 모듈은 버전 호환성 확인을 위해 진입점에서 luaL_checkversion(L)을 호출하는 걸 고려해요.

출처: C API 실전 — 모듈·userdata·스레드·이진 청크 (C API in Practice)

본문

빌드 및 링크

C 모듈은 루아 헤더(lua.h, lauxlib.h, lualib.h)를 포함하고, 루아 라이브러리와 링크하거나 플러그인으로 동적 로드해요. 플랫폼별 컴파일 명령은 루아 배포판의 Makefile/문서를 참조해요.

임베딩 vs 모듈

호스트가 루아를 내장(embed)할 때는 lua_State를 만들고 luaL_openlibs로 라이브러리를 연 뒤, 애플리케이션 전용 함수를 등록해요. 모듈은 require로 로드되는 반면, 임베딩은 상태 생성 시 직접 등록해요.

더 알아보기