Lua 모듈
Lua 모듈
섹션 제목: “Lua 모듈”런타임 모듈은 새로운 기능으로 Lua 환경을 확장합니다. 모듈은 결정론적 유틸리티, I/O 작업, 또는 외부 시스템으로 yield하는 비동기 명령을 제공할 수 있습니다.
Lua 런타임 구현은 향후 버전에서 변경될 수 있습니다.
모듈 정의
섹션 제목: “모듈 정의”모든 모듈은 luaapi.ModuleDef를 사용합니다:
var Module = &luaapi.ModuleDef{ Name: "mymodule", Description: "내 커스텀 모듈", Class: []string{luaapi.ClassDeterministic}, Types: ModuleTypes, // 도구를 위한 타입 정의 Build: func() (*lua.LTable, []luaapi.YieldType) { mod := lua.CreateTable(0, 2) mod.RawSetString("hello", lua.LGoFunc(helloFunc)) mod.RawSetString("greet", lua.LGoFunc(greetFunc)) mod.Immutable = true return mod, nil },}Build 함수는 반환합니다:
- 내보낸 함수가 있는 모듈 테이블
- 비동기 작업을 위한 yield 타입 목록 (또는 nil)
모듈 테이블은 한 번 빌드되고 모든 Lua 상태에서 재사용을 위해 캐시됩니다.
모듈 분류
섹션 제목: “모듈 분류”Class 필드는 모듈을 사용할 수 있는 위치를 결정합니다:
| 클래스 | 설명 |
|---|---|
ClassDeterministic | 같은 입력은 항상 같은 출력 생성 |
ClassNondeterministic | 출력이 다름 (시간, 랜덤) |
ClassIO | 외부 I/O 작업 |
ClassNetwork | 네트워크 작업 |
ClassStorage | 데이터 지속성 |
ClassWorkflow | 워크플로우 안전 작업 |
ClassDeterministic만 태그된 모듈은 워크플로우 안전합니다. I/O나 네트워크 클래스를 추가하면 모듈이 함수와 프로세스로 제한됩니다.
함수 노출
섹션 제목: “함수 노출”함수는 func(l *lua.LState) int 시그니처를 가지며 반환 값은 스택에 푸시된 값의 수입니다:
func greetFunc(l *lua.LState) int { name := l.CheckString(1) // 필수 인자 greeting := l.OptString(2, "Hello") // 기본값이 있는 선택적
l.Push(lua.LString(greeting + ", " + name + "!")) return 1}| 메서드 | 설명 |
|---|---|
l.CheckString(n) | 위치 n의 필수 문자열 |
l.CheckInt(n) | 필수 정수 |
l.CheckNumber(n) | 필수 숫자 |
l.CheckTable(n) | 필수 테이블 |
l.OptString(n, def) | 기본값이 있는 선택적 문자열 |
l.OptInt(n, def) | 기본값이 있는 선택적 정수 |
테이블
섹션 제목: “테이블”Go와 Lua 사이에 전달되는 테이블은 기본적으로 가변입니다. 모듈 내보내기 테이블은 불변으로 표시해야 합니다:
mod := lua.CreateTable(0, 5)mod.RawSetString("func1", lua.LGoFunc(func1))mod.Immutable = true // Lua가 내보내기를 수정하는 것 방지데이터 테이블은 일반 사용을 위해 가변으로 유지됩니다:
result := l.CreateTable(0, 3)result.RawSetString("name", lua.LString("value"))result.RawSetString("count", lua.LNumber(42))l.Push(result)타입 시스템
섹션 제목: “타입 시스템”모듈은 두 개의 별도이지만 상호 보완적인 타이핑 메커니즘을 사용합니다.
타입 정의 (도구)
섹션 제목: “타입 정의 (도구)”Types 필드는 IDE 지원 및 문서화를 위한 타입 시그니처를 제공합니다:
func ModuleTypes() *types.TypeManifest { m := types.NewManifest("mymodule")
objectType := &types.InterfaceType{ Name: "mymodule.Object", Methods: map[string]*types.FunctionType{ "get_value": types.NewFunction(nil, []types.Type{types.String}), "set_value": types.NewFunction([]types.Type{types.String}, nil), }, }
m.DefineType("Object", objectType) m.SetExport(moduleType) return m}사용 가능한 타입 구조:
| 타입 | 설명 |
|---|---|
types.String | 문자열 프리미티브 |
types.Number | 숫자 값 |
types.Boolean | 불리언 값 |
types.Any | 모든 Lua 값 |
types.LuaError | 에러 타입 |
types.Optional(t) | 타입 t의 선택적 값 |
types.InterfaceType | 메서드가 있는 객체 |
types.FunctionType | 파라미터/반환이 있는 함수 시그니처 |
types.RecordType | 필드가 있는 구조체 유사 타입 |
types.TableType | 키/값 타입이 있는 테이블 |
함수 시그니처는 가변 파라미터를 지원합니다:
// (string, ...any) -> (string, error?)types.FunctionType{ Params: []types.Type{types.String}, Variadic: types.Any, Returns: []types.Type{types.String, types.Optional(types.LuaError)},}전체 타입 시스템은 go-lua의 types 패키지를 참조하세요.
UserData 바인딩 (런타임)
섹션 제목: “UserData 바인딩 (런타임)”RegisterTypeMethods는 실제 Go-to-Lua 바인딩을 생성합니다:
func init() { value.RegisterTypeMethods(nil, "mymodule.Object", map[string]lua.LGoFunc{ "__tostring": objectToString, // 메타메서드 }, map[string]lua.LGoFunc{ "get_value": objectGetValue, // 일반 메서드 "set_value": objectSetValue, }, )}메타테이블은 불변이고 스레드 안전 재사용을 위해 전역적으로 캐시됩니다.
| 시스템 | 목적 | 정의 |
|---|---|---|
| 타입 정의 | IDE, 문서, 타입 검사 | 시그니처 |
| UserData 바인딩 | 런타임 메서드 호출 | 실행 가능한 함수 |
비동기 작업
섹션 제목: “비동기 작업”외부 시스템을 기다리는 작업의 경우 결과 대신 yield를 반환합니다. yield는 Go 핸들러로 디스패치되고 핸들러가 완료되면 프로세스가 재개됩니다.
Yield 정의
섹션 제목: “Yield 정의”모듈의 Build 함수에서 yield 타입을 선언합니다:
Build: func() (*lua.LTable, []luaapi.YieldType) { mod := lua.CreateTable(0, 1) mod.RawSetString("fetch", lua.LGoFunc(fetchFunc)) mod.Immutable = true
yields := []luaapi.YieldType{ {Sample: &FetchYield{}, CmdID: myapi.FetchCommand}, }
return mod, yields}Yield 생성
섹션 제목: “Yield 생성”일반 반환 값 대신 yield를 시그널하려면 -1을 반환합니다:
func fetchFunc(l *lua.LState) int { url := l.CheckString(1)
yield := AcquireFetchYield() yield.URL = url
l.Push(yield) return -1 // 스택 카운트가 아닌 yield 시그널}Yield 구현
섹션 제목: “Yield 구현”Yield는 Lua 값과 디스패처 명령을 브릿지합니다:
type FetchYield struct { *myapi.FetchCmd}
func (y *FetchYield) String() string { return "<fetch_yield>" }func (y *FetchYield) Type() lua.LValueType { return lua.LTUserData }func (y *FetchYield) CmdID() dispatcher.CommandID { return myapi.FetchCommand }func (y *FetchYield) ToCommand() dispatcher.Command { return y.FetchCmd }func (y *FetchYield) Release() { releaseFetchYield(y) }
func (y *FetchYield) HandleResult(l *lua.LState, data any, err error) []lua.LValue { if err != nil { return []lua.LValue{lua.LNil, lua.NewLuaError(l, err.Error())} } resp := data.(*myapi.FetchResponse) return []lua.LValue{lua.LString(resp.Body), lua.LNil}}디스패처는 명령을 핸들러로 라우팅합니다. 핸들러 구현은 명령 디스패치를 참조하세요.
에러 처리
섹션 제목: “에러 처리”구조화된 에러를 사용하여 두 번째 값으로 에러를 반환합니다:
func myFunc(l *lua.LState) int { result, err := doSomething() if err != nil { lerr := lua.NewLuaError(l, err.Error()). WithKind(lua.Internal). WithRetryable(true) l.Push(lua.LNil) l.Push(lerr) return 2 }
l.Push(lua.LString(result)) l.Push(lua.LNil) return 2}민감한 작업을 수행하기 전에 권한을 확인합니다:
func myFunc(l *lua.LState) int { ctx := l.Context()
if !security.IsAllowed(ctx, "mymodule.action", resource, nil) { l.Push(lua.LNil) l.Push(lua.NewLuaError(l, "permission denied").WithKind(lua.PermissionDenied)) return 2 }
// 작업 진행}테스팅
섹션 제목: “테스팅”기본 모듈 테스트는 구조와 동기 함수를 검증합니다:
func TestModule(t *testing.T) { l := lua.NewState() defer l.Close()
mod, _ := Module.Build() l.SetGlobal("mymodule", mod)
err := l.DoString(` local m = mymodule assert(m.hello() == "Hello, World!") `) if err != nil { t.Fatal(err) }}Yield가 있는 모듈 테스팅
섹션 제목: “Yield가 있는 모듈 테스팅”yield 함수를 사용하는 Lua 코드를 테스트하려면 필요한 디스패처가 있는 최소 스케줄러를 생성합니다:
type testScheduler struct { *actor.Scheduler clock *clock.Dispatcher mu sync.Mutex pending map[string]chan *runtime.Result}
func newTestScheduler() *testScheduler { ts := &testScheduler{pending: make(map[string]chan *runtime.Result)} reg := scheduler.NewRegistry()
// 모듈이 사용하는 yield를 위한 디스패처 등록 clockSvc := clock.NewDispatcher() clockSvc.RegisterAll(func(id dispatcher.CommandID, h dispatcher.Handler) { reg.Register(id, h) }) ts.clock = clockSvc
ts.Scheduler = actor.NewScheduler(reg, actor.WithWorkers(4), actor.WithLifecycle(ts)) return ts}
func (ts *testScheduler) OnComplete(_ context.Context, p pid.PID, result *runtime.Result) { ts.mu.Lock() ch, ok := ts.pending[p.UniqID] delete(ts.pending, p.UniqID) ts.mu.Unlock() if ok { ch <- result }}
func (ts *testScheduler) Execute(ctx context.Context, p pid.PID, proc process.Process, method string, input payload.Payloads) (*runtime.Result, error) { resultCh := make(chan *runtime.Result, 1) ts.mu.Lock() ts.pending[p.UniqID] = resultCh ts.mu.Unlock()
_, err := ts.Scheduler.Submit(ctx, p, proc, method, input) if err != nil { return nil, err }
select { case result := <-resultCh: return result, nil case <-ctx.Done(): return nil, ctx.Err() }}테스트하는 모듈로 Lua 스크립트에서 프로세스를 생성합니다:
func bindMyModule(l *lua.LState) { tbl, _ := mymodule.Module.Build() l.SetGlobal(mymodule.Module.Name, tbl)}
func newLuaProcess(script string) *engine.Process { proto, _ := lua.CompileString(script, "test.lua") return engine.NewProcess( engine.WithProto(proto), engine.WithModuleBinder(bindMyModule), )}
func TestMyModuleYields(t *testing.T) { sched := newTestScheduler() sched.Start() defer sched.Stop()
script := ` local result = mymodule.fetch("http://example.com") return result.status `
ctx, _ := ctxapi.OpenFrameContext(context.Background()) proc := newLuaProcess(script)
result, err := sched.Execute(ctx, pid.PID{UniqID: "test"}, proc, "", nil) if err != nil { t.Fatal(err) } // 결과에 대해 Assert}완전한 예제는 runtime/lua/modules/time/integration_test.go를 참조하세요.