1
0

Compare commits

..

39 Commits

Author SHA1 Message Date
f47d36eb8b update docs 2025-06-03 17:01:56 -05:00
516d66c2f2 maps always return any 2025-06-03 16:56:58 -05:00
4e59fc6e5c add map array support to pushvalue 2025-06-02 21:04:06 -05:00
c22638b51f update readme 2025-05-31 17:54:08 -05:00
202664f635 update documentation 2025-05-31 17:49:25 -05:00
f4bfff470f massive rewrite
fix go func mallocs
add helper utils
2025-05-31 17:42:58 -05:00
fc7958312d add metatable methods 2025-05-24 07:32:24 -05:00
83134f3dbc fix error text test 2025-05-22 11:39:43 -05:00
3817f80dc4 require explicit cleanup 2025-05-22 11:11:16 -05:00
7a9dfd47d3 ensure funcs get unique pointer 2025-05-22 10:31:00 -05:00
6a2eb9df63 improve error reporting 2025-05-22 08:54:59 -05:00
2bdf7cfd4a drop vendor directory, use system-provided libs on windows 2025-05-08 17:51:47 -05:00
231ffb0d29 add int64 type cast 2025-05-03 12:47:07 -05:00
6b9e2a0e20 op 1 2025-04-04 21:32:17 -05:00
e58f9a6028 add bench profiling 2025-04-04 21:32:17 -05:00
a2b4b1c927 add type utilities 2025-03-29 09:08:02 -05:00
44337fffe3 add flag for std libs 2025-03-29 08:32:45 -05:00
0756cabcaa remove sandbox docs 2025-03-28 20:31:29 -05:00
656ac1a703 remove sandbox 2025-03-28 20:27:51 -05:00
5774808064 update docs 2025-03-27 22:05:09 -05:00
875abee366 optimize sandbox 2025-03-27 21:58:56 -05:00
4ad87f81f3 move sandbox benches 2025-03-27 21:51:31 -05:00
9e5092acdb optimize wrapper 2025-03-27 21:46:23 -05:00
b83f77d7a6 optimize functions 2025-03-27 21:45:39 -05:00
29679349ef optimize table 2025-03-27 21:42:58 -05:00
fed0c2ad34 optimize bytecode 2025-03-27 21:41:06 -05:00
faab0a2d08 sandbox 2 2025-03-27 21:31:41 -05:00
f106dfd9ea sandbox 1 2025-03-27 18:25:09 -05:00
936e4ccdc2 remove unregistration change 2025-03-27 14:04:17 -05:00
075b45768f more robust unregistergofunction 2025-03-27 13:56:02 -05:00
13686b3e66 Use pkg-config on *nix 2025-03-12 11:50:37 -05:00
98ca857d73 interface{} to any 2025-03-07 07:25:34 -06:00
143b9333c6 Wrapper rewrite 2025-02-26 07:00:01 -06:00
865ac8859f added table pooling and some micro-ops 2025-02-13 07:11:13 -06:00
4dc266201f BIG changes; no "safe" mode, function updates, etc 2025-02-12 19:17:11 -06:00
7c79616cac Small update to DoString for stack discipline 2025-02-08 09:57:59 -06:00
146b0a51db Add Call and IsNil 2025-02-03 19:09:30 -06:00
c74ad4bbc9 Add IsNumber 2025-02-03 18:55:34 -06:00
229884ba97 Add IsString and Next helpers 2025-02-03 18:52:26 -06:00
37 changed files with 4337 additions and 2425 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@
go.work
.idea
bench/profile_results

82
API.md Normal file
View File

@ -0,0 +1,82 @@
# API Quick Reference
## Core State
- New(openLibs ...bool) *State
- Close()
- Cleanup()
## Stack
- GetTop() int
- SetTop(index int)
- Pop(n int)
- PushCopy(index int)
- Remove(index int)
## Type Checks
- GetType(index int) LuaType
- IsNil/IsBoolean/IsNumber/IsString/IsTable/IsFunction(index int) bool
## Values
- ToString/ToNumber/ToBoolean(index int) T
- ToValue(index int) (any, error)
- ToTable(index int) (any, error)
- PushNil/PushBoolean/PushNumber/PushString/PushValue()
## Tables
- NewTable()
- CreateTable(narr, nrec int)
- GetTable/SetTable(index int)
- GetField/SetField(index int, key string)
- GetFieldString/Number/Bool/Table(index int, key string, default T) T
- GetTableLength(index int) int
- Next(index int) bool
- ForEachTableKV/ForEachArray(index int, fn func)
- NewTableBuilder() *TableBuilder
## Functions
- RegisterGoFunction(name string, fn GoFunction) error
- UnregisterGoFunction(name string)
- PushGoFunction(fn GoFunction) error
- Call(nargs, nresults int) error
- CallGlobal(name string, args ...any) ([]any, error)
## Globals
- GetGlobal/SetGlobal(name string)
## Execution
- LoadString/LoadFile(source string) error
- DoString/DoFile(source string) error
- Execute(code string) (int, error)
- ExecuteWithResult(code string) (any, error)
- BatchExecute(statements []string) error
## Bytecode
- CompileBytecode(code, name string) ([]byte, error)
- LoadBytecode(bytecode []byte, name string) error
- RunBytecode() error
- RunBytecodeWithResults(nresults int) error
- LoadAndRunBytecode(bytecode []byte, name string) error
- LoadAndRunBytecodeWithResults(bytecode []byte, name string, nresults int) error
- CompileAndRun(code, name string) error
## Validation
- CheckArgs(specs ...ArgSpec) error
- CheckMinArgs/CheckExactArgs(n int) error
- SafeToString/Number/Table(index int) (T, error)
## Error Handling
- PushError(format string, args ...any) int
- GetStackTrace() string
- GetErrorInfo(context string) *LuaError
- CreateLuaError(code int, context string) *LuaError
## Package
- SetPackagePath/AddPackagePath(path string) error
## Metatable
- SetMetatable(index int)
- GetMetatable(index int) bool
## Constants
- TypeNil/Boolean/Number/String/Table/Function/UserData/Thread
- LUA_MINSTACK/MAXSTACK/REGISTRYINDEX/GLOBALSINDEX

392
DOCS.md
View File

@ -1,19 +1,11 @@
# LuaJIT Go Wrapper API Documentation
# API Documentation
## State Management
### NewSafe() *State
Creates a new Lua state with stack safety enabled.
```go
L := luajit.NewSafe()
defer L.Close()
defer L.Cleanup()
```
### New() *State
Creates a new Lua state without stack safety checks.
New creates a new Lua state with optional standard libraries; true if not specified
```go
L := luajit.New()
L := luajit.New() // or luajit.New(false)
defer L.Close()
defer L.Cleanup()
```
@ -38,6 +30,18 @@ Returns the index of the top element in the stack.
top := L.GetTop() // 0 for empty stack
```
### SetTop(index int)
Sets the stack top to a specific index.
```go
L.SetTop(2) // Truncate stack to 2 elements
```
### PushCopy(index int)
Pushes a copy of the value at the given index onto the stack.
```go
L.PushCopy(-1) // Duplicate the top element
```
### Pop(n int)
Removes n elements from the stack.
```go
@ -52,14 +56,6 @@ L.Remove(-1) // Remove top element
L.Remove(1) // Remove first element
```
### checkStack(n int) error
Internal function that ensures there's enough space for n new elements.
```go
if err := L.checkStack(2); err != nil {
return err
}
```
## Type Checks
### GetType(index int) LuaType
@ -70,9 +66,12 @@ if L.GetType(-1) == TypeString {
}
```
### IsFunction(index int) bool
### IsNil(index int) bool
### IsBoolean(index int) bool
### IsNumber(index int) bool
### IsString(index int) bool
### IsTable(index int) bool
### IsUserData(index int) bool
### IsFunction(index int) bool
Type checking functions for specific Lua types.
```go
if L.IsTable(-1) {
@ -100,8 +99,8 @@ Converts the value to a boolean.
bool := L.ToBoolean(-1)
```
### ToValue(index int) (interface{}, error)
Converts any Lua value to its Go equivalent.
### ToValue(index int) (any, error)
Converts any Lua value to its Go equivalent with automatic type detection.
```go
val, err := L.ToValue(-1)
if err != nil {
@ -109,8 +108,8 @@ if err != nil {
}
```
### ToTable(index int) (map[string]interface{}, error)
Converts a Lua table to a Go map.
### ToTable(index int) (any, error)
Converts a Lua table to optimal Go type; arrays or `map[string]any`.
```go
table, err := L.ToTable(-1)
if err != nil {
@ -118,6 +117,12 @@ if err != nil {
}
```
### GetTableLength(index int) int
Returns the length of a table at the given index.
```go
length := L.GetTableLength(-1)
```
## Value Pushing
### PushNil()
@ -132,32 +137,111 @@ L.PushBoolean(true)
L.PushNil()
```
### PushValue(v interface{}) error
Pushes any Go value onto the stack.
### PushValue(v any) error
Pushes any Go value onto the stack with comprehensive type support.
```go
err := L.PushValue(myValue)
```
### PushTable(table map[string]interface{}) error
Pushes a Go map as a Lua table.
```go
data := map[string]interface{}{
// Supports: primitives, slices, maps with various type combinations
err := L.PushValue(map[string]any{
"key": "value",
"numbers": []float64{1, 2, 3},
}
err := L.PushTable(data)
})
```
## Function Registration
## Table Operations
### RegisterGoFunction(name string, fn GoFunction) error
Registers a Go function that can be called from Lua.
### CreateTable(narr, nrec int)
Creates a new table with pre-allocated space.
```go
adder := func(s *State) int {
L.CreateTable(10, 5) // Space for 10 array elements, 5 records
```
### NewTable()
Creates a new empty table and pushes it onto the stack.
```go
L.NewTable()
```
### GetTable(index int)
Gets a table field (t[k]) where t is at the given index and k is at the top of the stack.
```go
L.PushString("key")
L.GetTable(-2) // Gets table["key"]
```
### SetTable(index int)
Sets a table field (t[k] = v) where t is at the given index, k is at -2, and v is at -1.
```go
L.PushString("key")
L.PushString("value")
L.SetTable(-3) // table["key"] = "value"
```
### GetField(index int, key string)
Gets a table field t[k] and pushes it onto the stack.
```go
L.GetField(-1, "name") // gets table.name
```
### SetField(index int, key string)
Sets a table field t[k] = v, where v is the value at the top of the stack.
```go
L.PushString("value")
L.SetField(-2, "key") // table.key = "value"
```
### Next(index int) bool
Pops a key from the stack and pushes the next key-value pair from the table.
```go
L.PushNil() // Start iteration
for L.Next(-2) {
// Stack now has key at -2 and value at -1
key := L.ToString(-2)
value := L.ToString(-1)
L.Pop(1) // Remove value, keep key for next iteration
}
```
### GetFieldString/Number/Bool/Table(index int, key string, default T) T
Get typed fields from tables with default values.
```go
name := L.GetFieldString(-1, "name", "unknown")
age := L.GetFieldNumber(-1, "age", 0)
active := L.GetFieldBool(-1, "active", false)
config, ok := L.GetFieldTable(-1, "config")
```
### ForEachTableKV(index int, fn func(key, value string) bool)
### ForEachArray(index int, fn func(i int, state *State) bool)
Convenient iteration helpers.
```go
L.ForEachTableKV(-1, func(key, value string) bool {
fmt.Printf("%s: %s\n", key, value)
return true // continue iteration
})
```
## Function Registration and Calling
### GoFunction
Type definition for Go functions callable from Lua.
```go
type GoFunction func(*State) int
```
### PushGoFunction(fn GoFunction) error
Wraps a Go function and pushes it onto the Lua stack.
```go
adder := func(s *luajit.State) int {
sum := s.ToNumber(1) + s.ToNumber(2)
s.PushNumber(sum)
return 1
}
err := L.PushGoFunction(adder)
```
### RegisterGoFunction(name string, fn GoFunction) error
Registers a Go function as a global Lua function.
```go
err := L.RegisterGoFunction("add", adder)
```
@ -167,23 +251,51 @@ Removes a previously registered function.
L.UnregisterGoFunction("add")
```
## Package Management
### SetPackagePath(path string) error
Sets the Lua package.path variable.
### Call(nargs, nresults int) error
Calls a function with the given number of arguments and results.
```go
err := L.SetPackagePath("./?.lua;/usr/local/share/lua/5.1/?.lua")
L.GetGlobal("myfunction")
L.PushNumber(1)
L.PushNumber(2)
err := L.Call(2, 1) // Call with 2 args, expect 1 result
```
### AddPackagePath(path string) error
Adds a path to the existing package.path.
### CallGlobal(name string, args ...any) ([]any, error)
Calls a global function with arguments and returns all results.
```go
err := L.AddPackagePath("./modules/?.lua")
results, err := L.CallGlobal("myfunction", 1, 2, "hello")
```
## Global Operations
### GetGlobal(name string)
Gets a global variable and pushes it onto the stack.
```go
L.GetGlobal("myGlobal")
```
### SetGlobal(name string)
Sets a global variable from the value at the top of the stack.
```go
L.PushNumber(42)
L.SetGlobal("answer") // answer = 42
```
## Code Execution
### DoString(str string) error
### LoadString(code string) error
Loads a Lua chunk from a string without executing it.
```go
err := L.LoadString("return 42")
```
### LoadFile(filename string) error
Loads a Lua chunk from a file without executing it.
```go
err := L.LoadFile("script.lua")
```
### DoString(code string) error
Executes a string of Lua code.
```go
err := L.DoString(`
@ -199,52 +311,185 @@ Executes a Lua file.
err := L.DoFile("script.lua")
```
## Table Operations
### GetField(index int, key string)
Gets a field from a table at the given index.
### Execute(code string) (int, error)
Executes a Lua string and returns the number of results left on the stack.
```go
L.GetField(-1, "name") // gets table.name
nresults, err := L.Execute("return 1, 2, 3")
// nresults would be 3
```
### SetField(index int, key string)
Sets a field in a table at the given index.
### ExecuteWithResult(code string) (any, error)
Executes a Lua string and returns the first result.
```go
L.PushString("value")
L.SetField(-2, "key") // table.key = "value"
result, err := L.ExecuteWithResult("return 'hello'")
// result would be "hello"
```
### GetGlobal(name string)
Gets a global variable.
### BatchExecute(statements []string) error
Executes multiple statements as a single batch.
```go
L.GetGlobal("myGlobal")
err := L.BatchExecute([]string{
"x = 10",
"y = 20",
"result = x + y",
})
```
### SetGlobal(name string)
Sets a global variable from the value at the top of the stack.
## Bytecode Operations
### CompileBytecode(code string, name string) ([]byte, error)
Compiles a Lua chunk to bytecode without executing it.
```go
L.PushNumber(42)
L.SetGlobal("answer") // answer = 42
bytecode, err := L.CompileBytecode("return 42", "test")
```
### LoadBytecode(bytecode []byte, name string) error
Loads precompiled bytecode without executing it.
```go
err := L.LoadBytecode(bytecode, "test")
```
### RunBytecode() error
Executes previously loaded bytecode with 0 results.
```go
err := L.RunBytecode()
```
### RunBytecodeWithResults(nresults int) error
Executes bytecode and keeps nresults on the stack.
```go
err := L.RunBytecodeWithResults(1)
```
### LoadAndRunBytecode(bytecode []byte, name string) error
Loads and executes bytecode.
```go
err := L.LoadAndRunBytecode(bytecode, "test")
```
### LoadAndRunBytecodeWithResults(bytecode []byte, name string, nresults int) error
Loads and executes bytecode, preserving results.
```go
err := L.LoadAndRunBytecodeWithResults(bytecode, "test", 1)
```
### CompileAndRun(code string, name string) error
Compiles and immediately executes Lua code.
```go
err := L.CompileAndRun("answer = 42", "test")
```
## Package Path Operations
### SetPackagePath(path string) error
Sets the Lua package.path.
```go
err := L.SetPackagePath("./?.lua;/usr/local/share/lua/5.1/?.lua")
```
### AddPackagePath(path string) error
Adds a path to package.path.
```go
err := L.AddPackagePath("./modules/?.lua")
```
## Metatable Operations
### SetMetatable(index int)
Sets the metatable for the value at the given index.
```go
L.SetMetatable(-1)
```
### GetMetatable(index int) bool
Gets the metatable for the value at the given index.
```go
if L.GetMetatable(-1) {
// Metatable is now on stack
L.Pop(1)
}
```
## Error Handling
### LuaError
Error type containing both an error code and message.
Enhanced error type with detailed context information.
```go
type LuaError struct {
Code int
Message string
Code int
Message string
File string
Line int
StackTrace string
Context string
}
```
### getStackTrace() string
### GetStackTrace() string
Gets the current Lua stack trace.
```go
trace := L.getStackTrace()
trace := L.GetStackTrace()
fmt.Println(trace)
```
### GetErrorInfo(context string) *LuaError
Extracts detailed error information from the Lua stack.
```go
err := L.GetErrorInfo("MyFunction")
```
### CreateLuaError(code int, context string) *LuaError
Creates a LuaError with full context information.
```go
err := L.CreateLuaError(status, "DoString")
```
### PushError(format string, args ...any) int
Pushes an error string and returns -1.
```go
return s.PushError("invalid argument: %v", arg)
```
## Validation
### CheckArgs(specs ...ArgSpec) error
Validates function arguments against specifications.
```go
err := s.CheckArgs(
ArgSpec{Name: "name", Type: "string", Required: true, Check: CheckString},
ArgSpec{Name: "age", Type: "number", Required: false, Check: CheckNumber},
)
```
### CheckMinArgs/CheckExactArgs(n int) error
Argument count validation.
```go
if err := s.CheckMinArgs(2); err != nil {
return s.PushError(err.Error())
}
```
### SafeToString/Number/Table(index int) (T, error)
Safe value conversion with error handling.
```go
str, err := s.SafeToString(1)
if err != nil {
return s.PushError(err.Error())
}
```
## Table Building
### NewTableBuilder() *TableBuilder
Creates a new table builder for fluent table construction.
```go
L.NewTableBuilder().
SetString("name", "John").
SetNumber("age", 30).
SetBool("active", true).
Build()
```
## Thread Safety Notes
- The function registry is thread-safe
@ -256,15 +501,14 @@ fmt.Println(trace)
Always pair state creation with cleanup:
```go
L := luajit.NewSafe()
L := luajit.New()
defer L.Close()
defer L.Cleanup()
```
Stack management in unsafe mode requires manual attention:
Stack management requires manual attention:
```go
L := luajit.New()
L.PushString("hello")
// ... use the string
L.Pop(1) // Clean up when done
```
```

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Sky
Copyright (c) 2025 Sharkk, Skylear Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

284
README.md
View File

@ -1,6 +1,6 @@
# LuaJIT Go Wrapper
Hey there! This is a Go wrapper for LuaJIT that makes it easy to embed Lua in your Go applications. We've focused on making it both safe and fast, while keeping the API clean and intuitive.
This is a Go wrapper for LuaJIT that makes it easy to embed Lua in your Go applications. We've focused on making it both performant and developer-friendly, with an API that feels natural to use.
## What's This For?
@ -18,55 +18,15 @@ First, grab the package:
go get git.sharkk.net/Sky/LuaJIT-to-Go
```
You'll need LuaJIT's development files, but don't worry - we include libraries for Windows and Linux in the vendor directory.
Here's the simplest thing you can do:
```go
L := luajit.NewSafe()
L := luajit.New() // pass false to not load standard libs
defer L.Close()
defer L.Cleanup()
err := L.DoString(`print("Hey from Lua!")`)
```
## Stack Safety: Choose Your Adventure
One of the key decisions you'll make is whether to use stack-safe mode. Here's what that means:
### Stack-Safe Mode (NewSafe())
```go
L := luajit.NewSafe()
```
Think of this as driving with guardrails. It's perfect when:
- You're new to Lua or embedding scripting languages
- You're writing a server or long-running application
- You want to handle untrusted Lua code
- You'd rather have slightly slower code than mysterious crashes
The safe mode will:
- Prevent stack overflows
- Check types more thoroughly
- Clean up after messy Lua code
- Give you better error messages
### Non-Stack-Safe Mode (New())
```go
L := luajit.New()
```
This is like taking off the training wheels. Use it when:
- You know exactly how your Lua code behaves
- You've profiled your application and need more speed
- You're doing lots of rapid, simple Lua calls
- You're writing performance-critical code
The unsafe mode:
- Skips most safety checks
- Runs noticeably faster
- Gives you direct control over the stack
- Can crash spectacularly if you make a mistake
Most applications should start with stack-safe mode and only switch to unsafe mode if profiling shows it's necessary.
## Working with Bytecode
Need even more performance? You can compile your Lua code to bytecode and reuse it:
@ -74,53 +34,45 @@ Need even more performance? You can compile your Lua code to bytecode and reuse
```go
// Compile once
bytecode, err := L.CompileBytecode(`
local function calculate(x)
return x * x + x + 1
end
return calculate(10)
local function calculate(x)
return x * x + x + 1
end
return calculate(10)
`, "calc")
// Execute many times
for i := 0; i < 1000; i++ {
err := L.LoadBytecode(bytecode, "calc")
err := L.LoadAndRunBytecode(bytecode, "calc")
}
// Or do both at once
err := L.CompileAndLoad(`return "hello"`, "greeting")
err := L.CompileAndRun(`return "hello"`, "greeting")
```
### When to Use Bytecode
Bytecode execution is consistently faster than direct execution:
- Simple operations: 20-60% faster
- String operations: Up to 60% speedup
- Loop-heavy code: 10-15% improvement
- Table operations: 10-15% faster
Some benchmark results on a typical system:
```
Operation Direct Exec Bytecode Exec
----------------------------------------
Simple Math 1.5M ops/sec 2.4M ops/sec
String Ops 370K ops/sec 600K ops/sec
Table Creation 127K ops/sec 146K ops/sec
Benchmark Ops/sec Comparison
----------------------------------------------------------------------------
BenchmarkSimpleDoString 2,561,012 Base
BenchmarkSimplePrecompiledBytecode 3,828,841 +49.5% faster
BenchmarkFunctionCallDoString 2,021,098 Base
BenchmarkFunctionCallPrecompiled 3,482,074 +72.3% faster
BenchmarkLoopDoString 188,119 Base
BenchmarkLoopPrecompiled 211,081 +12.2% faster
BenchmarkTableOperationsDoString 84,086 Base
BenchmarkTableOperationsPrecompiled 93,655 +11.4% faster
BenchmarkComplexScript 33,133 Base
BenchmarkComplexScriptPrecompiled 41,044 +23.9% faster
```
Use bytecode when you:
- Have code that runs frequently
- Need maximum performance
- Want to precompile your Lua code
- Are distributing Lua code to many instances
## Registering Go Functions
Want to call Go code from Lua? Easy:
Want to call Go code from Lua? It's straightforward:
```go
// This function adds two numbers and returns the result
adder := func(s *luajit.State) int {
sum := s.ToNumber(1) + s.ToNumber(2)
s.PushNumber(sum)
return 1 // we're returning one value
sum := s.ToNumber(1) + s.ToNumber(2)
s.PushNumber(sum)
return 1 // we're returning one value
}
L.RegisterGoFunction("add", adder)
@ -131,45 +83,197 @@ Now in Lua:
result = add(40, 2) -- result = 42
```
### Function Validation
Validate arguments easily:
```go
calculator := func(s *luajit.State) int {
if err := s.CheckArgs(
luajit.ArgSpec{Name: "x", Type: "number", Required: true, Check: luajit.CheckNumber},
luajit.ArgSpec{Name: "y", Type: "number", Required: true, Check: luajit.CheckNumber},
); err != nil {
return s.PushError(err.Error())
}
result := s.ToNumber(1) + s.ToNumber(2)
s.PushNumber(result)
return 1
}
```
## Working with Tables
Lua tables are pretty powerful - they're like a mix of Go's maps and slices. We make it easy to work with them:
Lua tables are powerful - they're like a mix of Go's maps and slices. We make it easy to work with them:
```go
// Go → Lua
stuff := map[string]interface{}{
"name": "Arthur Dent",
"age": 30,
"items": []float64{1, 2, 3},
stuff := map[string]any{
"name": "Arthur Dent",
"age": 30,
"items": []float64{1, 2, 3},
}
L.PushTable(stuff)
L.PushValue(stuff) // Handles all Go types automatically
// Lua → Go
// Lua → Go with automatic type detection
L.GetGlobal("some_table")
result, err := L.ToTable(-1)
result, err := L.ToTable(-1) // Returns optimal Go type (typed array, or map[string]any)
```
### Table Builder
Build tables fluently:
```go
L.NewTableBuilder().
SetString("name", "John").
SetNumber("age", 30).
SetBool("active", true).
SetArray("scores", []any{95, 87, 92}).
Build()
```
### Table Field Access
Get fields with defaults:
```go
L.GetGlobal("config")
host := L.GetFieldString(-1, "host", "localhost")
port := L.GetFieldNumber(-1, "port", 8080)
debug := L.GetFieldBool(-1, "debug", false)
```
## Error Handling
We try to give you useful errors instead of mysterious panics:
We provide useful errors instead of mysterious panics:
```go
if err := L.DoString("this isn't valid Lua!"); err != nil {
if luaErr, ok := err.(*luajit.LuaError); ok {
fmt.Printf("Oops: %s\n", luaErr.Message)
}
if luaErr, ok := err.(*luajit.LuaError); ok {
fmt.Printf("Error in %s:%d - %s\n", luaErr.File, luaErr.Line, luaErr.Message)
fmt.Printf("Stack trace:\n%s\n", luaErr.StackTrace)
}
}
```
## A Few Tips
## Memory Management
- Always use those `defer L.Close()` and `defer L.Cleanup()` calls - they prevent memory leaks
The wrapper uses bytecode buffer pooling to reduce allocations:
```go
// Bytecode buffers are pooled and reused internally
for i := 0; i < 1000; i++ {
bytecode, _ := L.CompileBytecode(code, "test")
// Buffer automatically returned to pool
}
```
Function pointers are managed safely:
```go
// Functions are registered in a thread-safe registry
L.RegisterGoFunction("myFunc", myGoFunc)
defer L.Cleanup() // Cleans up all registered functions
```
## Best Practices
### State Management
- Always use `defer L.Close()` and `defer L.Cleanup()` to prevent memory leaks
- Each Lua state should stick to one goroutine
- For concurrent stuff, create multiple states
- For concurrent operations, create multiple states
- You can share functions between states safely
- Keep an eye on your stack in unsafe mode - it won't clean up after itself
- Start with stack-safe mode and measure before optimizing
- Keep an eye on your stack management - pop as many items as you push
### Bytecode Optimization
- Use bytecode for frequently executed code paths
- Consider compiling critical Lua code to bytecode at startup
- For small scripts (< 1024 bytes), direct execution might be faster due to compilation overhead
### Type Conversion
- Use `ToTable()` for automagic type detection and optimized Go arrays/maps
- Use `PushValue()` for automagic Go-to-Lua conversion
- Leverage typed field accessors for config-style tables
## Advanced Features
### Bytecode Serialization
You can serialize bytecode for distribution or caching:
```go
// Compile once
bytecode, _ := L.CompileBytecode(complexScript, "module")
// Save to file
ioutil.WriteFile("module.luac", bytecode, 0644)
// Later, load from file
bytecode, _ := ioutil.ReadFile("module.luac")
L.LoadAndRunBytecode(bytecode, "module")
```
### Closures and Upvalues
Bytecode properly preserves closures and upvalues:
```go
code := `
local counter = 0
return function()
counter = counter + 1
return counter
end
`
bytecode, _ := L.CompileBytecode(code, "counter")
L.LoadAndRunBytecodeWithResults(bytecode, "counter", 1)
L.SetGlobal("increment")
// Later...
results, _ := L.CallGlobal("increment") // Returns []any{1}
results, _ = L.CallGlobal("increment") // Returns []any{2}
```
### Batch Execution
Execute multiple statements efficiently:
```go
statements := []string{
"x = 10",
"y = 20",
"result = x + y",
}
err := L.BatchExecute(statements)
```
### Package Path Management
Manage Lua module paths:
```go
L.SetPackagePath("./?.lua;./modules/?.lua")
L.AddPackagePath("./vendor/?.lua")
```
### Type Conversion System
The wrapper includes a comprehensive type conversion system:
```go
// Get typed values with automatic conversion
value, ok := luajit.GetTypedValue[int](L, -1)
global, ok := luajit.GetGlobalTyped[[]string](L, "myArray")
// Convert between compatible types
result, ok := luajit.ConvertValue[map[string]int](someMap)
```
## Performance Tips
- Use bytecode for repeated execution
- Prefer `CallGlobal()` for simple function calls
- Use typed field accessors for configuration parsing
- Leverage automatic type detection in `ToTable()`
- Pool your Lua states for high-throughput scenarios
## Need Help?
@ -177,4 +281,4 @@ Check out the tests in the repository - they're full of examples. If you're stuc
## License
MIT Licensed - do whatever you want with it!
MIT Licensed - do whatever you want with it!

171
bench/bench_profile.go Normal file
View File

@ -0,0 +1,171 @@
package luajit_bench
import (
"flag"
"fmt"
"os"
"runtime"
"runtime/pprof"
"testing"
)
// Profiling flags
var (
cpuProfile = flag.String("cpuprofile", "", "write cpu profile to `file`")
memProfile = flag.String("memprofile", "", "write memory profile to `file`")
memProfileGC = flag.Bool("memprofilegc", false, "force GC before writing memory profile")
blockProfile = flag.String("blockprofile", "", "write block profile to `file`")
mutexProfile = flag.String("mutexprofile", "", "write mutex profile to `file`")
)
// setupTestMain configures profiling for benchmarks
func setupTestMain() {
// Make sure the flags are parsed
if !flag.Parsed() {
flag.Parse()
}
// CPU profiling
if *cpuProfile != "" {
f, err := os.Create(*cpuProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create CPU profile: %v\n", err)
os.Exit(1)
}
if err := pprof.StartCPUProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start CPU profile: %v\n", err)
os.Exit(1)
}
fmt.Println("CPU profiling enabled")
}
// Block profiling (goroutine blocking)
if *blockProfile != "" {
runtime.SetBlockProfileRate(1)
fmt.Println("Block profiling enabled")
}
// Mutex profiling (lock contention)
if *mutexProfile != "" {
runtime.SetMutexProfileFraction(1)
fmt.Println("Mutex profiling enabled")
}
}
// teardownTestMain completes profiling and writes output files
func teardownTestMain() {
// Stop CPU profile
if *cpuProfile != "" {
pprof.StopCPUProfile()
fmt.Println("CPU profile written to", *cpuProfile)
}
// Write memory profile
if *memProfile != "" {
f, err := os.Create(*memProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create memory profile: %v\n", err)
os.Exit(1)
}
defer f.Close()
// Force garbage collection before writing memory profile if requested
if *memProfileGC {
runtime.GC()
}
if err := pprof.WriteHeapProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write memory profile: %v\n", err)
os.Exit(1)
}
fmt.Println("Memory profile written to", *memProfile)
}
// Write block profile
if *blockProfile != "" {
f, err := os.Create(*blockProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create block profile: %v\n", err)
os.Exit(1)
}
defer f.Close()
if err := pprof.Lookup("block").WriteTo(f, 0); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write block profile: %v\n", err)
os.Exit(1)
}
fmt.Println("Block profile written to", *blockProfile)
}
// Write mutex profile
if *mutexProfile != "" {
f, err := os.Create(*mutexProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create mutex profile: %v\n", err)
os.Exit(1)
}
defer f.Close()
if err := pprof.Lookup("mutex").WriteTo(f, 0); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write mutex profile: %v\n", err)
os.Exit(1)
}
fmt.Println("Mutex profile written to", *mutexProfile)
}
}
// TestMain is the entry point for all tests in this package
func TestMain(m *testing.M) {
setupTestMain()
code := m.Run()
teardownTestMain()
os.Exit(code)
}
// MemStats captures a snapshot of memory statistics
type MemStats struct {
Alloc uint64
TotalAlloc uint64
Sys uint64
Mallocs uint64
Frees uint64
HeapAlloc uint64
}
// CaptureMemStats returns current memory statistics
func CaptureMemStats() MemStats {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return MemStats{
Alloc: m.Alloc,
TotalAlloc: m.TotalAlloc,
Sys: m.Sys,
Mallocs: m.Mallocs,
Frees: m.Frees,
HeapAlloc: m.HeapAlloc,
}
}
// TrackMemoryUsage runs fn and reports memory usage before and after
func TrackMemoryUsage(b *testing.B, name string, fn func()) {
b.Helper()
// Force GC before measurement
runtime.GC()
// Capture memory stats before
before := CaptureMemStats()
// Run the function
fn()
// Force GC after measurement to get accurate stats
runtime.GC()
// Capture memory stats after
after := CaptureMemStats()
// Report stats
b.ReportMetric(float64(after.Mallocs-before.Mallocs), name+"-mallocs")
b.ReportMetric(float64(after.TotalAlloc-before.TotalAlloc)/float64(b.N), name+"-bytes/op")
}

472
bench/bench_test.go Normal file
View File

@ -0,0 +1,472 @@
package luajit_bench
import (
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
// BenchmarkSimpleDoString benchmarks direct execution of a simple expression
func BenchmarkSimpleDoString(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := "local x = 1 + 1"
b.ResetTimer()
TrackMemoryUsage(b, "dostring", func() {
for i := 0; i < b.N; i++ {
if err := state.DoString(code); err != nil {
b.Fatalf("DoString failed: %v", err)
}
}
})
}
// BenchmarkSimpleCompileAndRun benchmarks compile and run of a simple expression
func BenchmarkSimpleCompileAndRun(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := "local x = 1 + 1"
b.ResetTimer()
TrackMemoryUsage(b, "compile-run", func() {
for i := 0; i < b.N; i++ {
if err := state.CompileAndRun(code, "simple"); err != nil {
b.Fatalf("CompileAndRun failed: %v", err)
}
}
})
}
// BenchmarkSimpleCompileLoadRun benchmarks compile, load, and run of a simple expression
func BenchmarkSimpleCompileLoadRun(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := "local x = 1 + 1"
b.ResetTimer()
TrackMemoryUsage(b, "compile-load-run", func() {
for i := 0; i < b.N; i++ {
bytecode, err := state.CompileBytecode(code, "simple")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
if err := state.LoadAndRunBytecode(bytecode, "simple"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkSimplePrecompiledBytecode benchmarks running precompiled bytecode
func BenchmarkSimplePrecompiledBytecode(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := "local x = 1 + 1"
bytecode, err := state.CompileBytecode(code, "simple")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "precompiled", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadAndRunBytecode(bytecode, "simple"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkFunctionCallDoString benchmarks direct execution of a function call
func BenchmarkFunctionCallDoString(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
// Setup function
setupCode := `
function add(a, b)
return a + b
end
`
if err := state.DoString(setupCode); err != nil {
b.Fatalf("Failed to set up function: %v", err)
}
code := "local result = add(10, 20)"
b.ResetTimer()
TrackMemoryUsage(b, "func-dostring", func() {
for i := 0; i < b.N; i++ {
if err := state.DoString(code); err != nil {
b.Fatalf("DoString failed: %v", err)
}
}
})
}
// BenchmarkFunctionCallPrecompiled benchmarks precompiled function call
func BenchmarkFunctionCallPrecompiled(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
// Setup function
setupCode := `
function add(a, b)
return a + b
end
`
if err := state.DoString(setupCode); err != nil {
b.Fatalf("Failed to set up function: %v", err)
}
code := "local result = add(10, 20)"
bytecode, err := state.CompileBytecode(code, "call")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "func-precompiled", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadAndRunBytecode(bytecode, "call"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkLoopDoString benchmarks direct execution of a loop
func BenchmarkLoopDoString(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
`
b.ResetTimer()
TrackMemoryUsage(b, "loop-dostring", func() {
for i := 0; i < b.N; i++ {
if err := state.DoString(code); err != nil {
b.Fatalf("DoString failed: %v", err)
}
}
})
}
// BenchmarkLoopPrecompiled benchmarks precompiled loop execution
func BenchmarkLoopPrecompiled(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
`
bytecode, err := state.CompileBytecode(code, "loop")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "loop-precompiled", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadAndRunBytecode(bytecode, "loop"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkTableOperationsDoString benchmarks direct execution of table operations
func BenchmarkTableOperationsDoString(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
local t = {}
for i = 1, 100 do
t[i] = i * 2
end
local sum = 0
for i, v in ipairs(t) do
sum = sum + v
end
`
b.ResetTimer()
TrackMemoryUsage(b, "table-dostring", func() {
for i := 0; i < b.N; i++ {
if err := state.DoString(code); err != nil {
b.Fatalf("DoString failed: %v", err)
}
}
})
}
// BenchmarkTableOperationsPrecompiled benchmarks precompiled table operations
func BenchmarkTableOperationsPrecompiled(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
local t = {}
for i = 1, 100 do
t[i] = i * 2
end
local sum = 0
for i, v in ipairs(t) do
sum = sum + v
end
`
bytecode, err := state.CompileBytecode(code, "table")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "table-precompiled", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadAndRunBytecode(bytecode, "table"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkGoFunctionCall benchmarks calling a Go function from Lua
func BenchmarkGoFunctionCall(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
// Register a simple Go function
add := func(s *luajit.State) int {
a := s.ToNumber(1)
b := s.ToNumber(2)
s.PushNumber(a + b)
return 1
}
if err := state.RegisterGoFunction("add", add); err != nil {
b.Fatalf("RegisterGoFunction failed: %v", err)
}
code := "local result = add(10, 20)"
bytecode, err := state.CompileBytecode(code, "gofunc")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "go-func-call", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadAndRunBytecode(bytecode, "gofunc"); err != nil {
b.Fatalf("LoadAndRunBytecode failed: %v", err)
}
}
})
}
// BenchmarkComplexScript benchmarks a more complex script
func BenchmarkComplexScript(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
-- Define a simple class
local Class = {}
Class.__index = Class
function Class.new(x, y)
local self = setmetatable({}, Class)
self.x = x or 0
self.y = y or 0
return self
end
function Class:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
return self
end
function Class:getPosition()
return self.x, self.y
end
-- Create instances and operate on them
local instances = {}
for i = 1, 50 do
instances[i] = Class.new(i, i*2)
end
local result = 0
for i, obj in ipairs(instances) do
obj:move(i, -i)
local x, y = obj:getPosition()
result = result + x + y
end
return result
`
b.ResetTimer()
TrackMemoryUsage(b, "complex-script", func() {
for i := 0; i < b.N; i++ {
if _, err := state.ExecuteWithResult(code); err != nil {
b.Fatalf("ExecuteWithResult failed: %v", err)
}
}
})
}
// BenchmarkComplexScriptPrecompiled benchmarks a precompiled complex script
func BenchmarkComplexScriptPrecompiled(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
code := `
-- Define a simple class
local Class = {}
Class.__index = Class
function Class.new(x, y)
local self = setmetatable({}, Class)
self.x = x or 0
self.y = y or 0
return self
end
function Class:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
return self
end
function Class:getPosition()
return self.x, self.y
end
-- Create instances and operate on them
local instances = {}
for i = 1, 50 do
instances[i] = Class.new(i, i*2)
end
local result = 0
for i, obj in ipairs(instances) do
obj:move(i, -i)
local x, y = obj:getPosition()
result = result + x + y
end
return result
`
bytecode, err := state.CompileBytecode(code, "complex")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "complex-precompiled", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadBytecode(bytecode, "complex"); err != nil {
b.Fatalf("LoadBytecode failed: %v", err)
}
if err := state.RunBytecodeWithResults(1); err != nil {
b.Fatalf("RunBytecodeWithResults failed: %v", err)
}
state.Pop(1) // Pop the result
}
})
}
// BenchmarkMultipleExecutions benchmarks executing the same bytecode multiple times
func BenchmarkMultipleExecutions(b *testing.B) {
state := luajit.New()
if state == nil {
b.Fatal("Failed to create Lua state")
}
defer state.Close()
// Setup a stateful environment
setupCode := `
counter = 0
function increment(amount)
counter = counter + (amount or 1)
return counter
end
`
if err := state.DoString(setupCode); err != nil {
b.Fatalf("Failed to set up environment: %v", err)
}
// Compile the function call
code := "return increment(5)"
bytecode, err := state.CompileBytecode(code, "increment")
if err != nil {
b.Fatalf("CompileBytecode failed: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "multiple-executions", func() {
for i := 0; i < b.N; i++ {
if err := state.LoadBytecode(bytecode, "increment"); err != nil {
b.Fatalf("LoadBytecode failed: %v", err)
}
if err := state.RunBytecodeWithResults(1); err != nil {
b.Fatalf("RunBytecodeWithResults failed: %v", err)
}
state.Pop(1) // Pop the result
}
})
}

138
bench/ezbench_test.go Normal file
View File

@ -0,0 +1,138 @@
package luajit_bench
import (
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
var benchCases = []struct {
name string
code string
}{
{
name: "SimpleAddition",
code: `return 1 + 1`,
},
{
name: "LoopSum",
code: `
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
return sum
`,
},
{
name: "FunctionCall",
code: `
local result = 0
for i = 1, 100 do
result = result + i
end
return result
`,
},
{
name: "TableCreation",
code: `
local t = {}
for i = 1, 100 do
t[i] = i * 2
end
return t[50]
`,
},
{
name: "StringOperations",
code: `
local s = "hello"
for i = 1, 10 do
s = s .. " world"
end
return #s
`,
},
}
func BenchmarkLuaDirectExecution(b *testing.B) {
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
L := luajit.New()
if L == nil {
b.Fatal("Failed to create Lua state")
}
defer L.Close()
defer L.Cleanup()
// First verify we can execute the code
if err := L.DoString(bc.code); err != nil {
b.Fatalf("Failed to execute test code: %v", err)
}
b.ResetTimer()
TrackMemoryUsage(b, "direct-"+bc.name, func() {
for i := 0; i < b.N; i++ {
// Execute string and get results
nresults, err := L.Execute(bc.code)
if err != nil {
b.Fatalf("Failed to execute code: %v", err)
}
L.Pop(nresults) // Clean up any results
}
})
})
}
}
func BenchmarkLuaBytecodeExecution(b *testing.B) {
// First compile all bytecode
bytecodes := make(map[string][]byte)
for _, bc := range benchCases {
L := luajit.New()
if L == nil {
b.Fatal("Failed to create Lua state")
}
defer L.Cleanup()
bytecode, err := L.CompileBytecode(bc.code, bc.name)
if err != nil {
L.Close()
b.Fatalf("Error compiling bytecode for %s: %v", bc.name, err)
}
bytecodes[bc.name] = bytecode
L.Close()
}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
L := luajit.New()
if L == nil {
b.Fatal("Failed to create Lua state")
}
defer L.Close()
defer L.Cleanup()
bytecode := bytecodes[bc.name]
// First verify we can execute the bytecode
if err := L.LoadAndRunBytecodeWithResults(bytecode, bc.name, 1); err != nil {
b.Fatalf("Failed to execute test bytecode: %v", err)
}
L.Pop(1) // Clean up the result
b.ResetTimer()
b.SetBytes(int64(len(bytecode))) // Track bytecode size in benchmarks
TrackMemoryUsage(b, "bytecode-"+bc.name, func() {
for i := 0; i < b.N; i++ {
if err := L.LoadAndRunBytecode(bytecode, bc.name); err != nil {
b.Fatalf("Error executing bytecode: %v", err)
}
}
})
})
}
}

78
bench/profile.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# Easy script to run benchmarks with profiling enabled
# Usage: ./profile_benchmarks.sh [benchmark_pattern]
set -e
# Default values
BENCHMARK=${1:-"."}
OUTPUT_DIR="./profile_results"
CPU_PROFILE="$OUTPUT_DIR/cpu.prof"
MEM_PROFILE="$OUTPUT_DIR/mem.prof"
BLOCK_PROFILE="$OUTPUT_DIR/block.prof"
MUTEX_PROFILE="$OUTPUT_DIR/mutex.prof"
TRACE_FILE="$OUTPUT_DIR/trace.out"
HTML_OUTPUT="$OUTPUT_DIR/profile_report.html"
# Create output directory
mkdir -p "$OUTPUT_DIR"
echo "Running benchmarks with profiling enabled..."
# Run benchmarks with profiling flags
go test -bench="$BENCHMARK" -benchmem -cpuprofile="$CPU_PROFILE" -memprofile="$MEM_PROFILE" -blockprofile="$BLOCK_PROFILE" -mutexprofile="$MUTEX_PROFILE" -count=5 -timeout=30m
echo "Generating CPU profile analysis..."
go tool pprof -http=":1880" -output="$OUTPUT_DIR/cpu_graph.svg" "$CPU_PROFILE"
echo "Generating memory profile analysis..."
go tool pprof -http=":1880" -output="$OUTPUT_DIR/mem_graph.svg" "$MEM_PROFILE"
# Generate a simple HTML report
cat > "$HTML_OUTPUT" << EOF
<!DOCTYPE html>
<html>
<head>
<title>LuaJIT Benchmark Profiling Results</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
.profile { margin-bottom: 30px; }
img { max-width: 100%; border: 1px solid #ddd; }
</style>
</head>
<body>
<h1>LuaJIT Benchmark Profiling Results</h1>
<p>Generated on: $(date)</p>
<div class="profile">
<h2>CPU Profile</h2>
<img src="cpu_graph.svg" alt="CPU Profile Graph">
<p>Command to explore: <code>go tool pprof $CPU_PROFILE</code></p>
</div>
<div class="profile">
<h2>Memory Profile</h2>
<img src="mem_graph.svg" alt="Memory Profile Graph">
<p>Command to explore: <code>go tool pprof $MEM_PROFILE</code></p>
</div>
<div class="profile">
<h2>Tips for Profile Analysis</h2>
<ul>
<li>Use <code>go tool pprof -http=:8080 $CPU_PROFILE</code> for interactive web UI</li>
<li>Use <code>top10</code> in pprof to see the top 10 functions by CPU/memory usage</li>
<li>Use <code>list FunctionName</code> to see line-by-line stats for a specific function</li>
</ul>
</div>
</body>
</html>
EOF
echo "Profiling complete! Results available in $OUTPUT_DIR"
echo "View the HTML report at $HTML_OUTPUT"
echo ""
echo "For detailed interactive analysis, run:"
echo " go tool pprof -http=:1880 $CPU_PROFILE # For CPU profile"
echo " go tool pprof -http=:1880 $MEM_PROFILE # For memory profile"

View File

@ -1,148 +0,0 @@
package main
import (
"fmt"
"time"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
type benchCase struct {
name string
code string
}
var cases = []benchCase{
{
name: "Simple Addition",
code: `return 1 + 1`,
},
{
name: "Loop Sum",
code: `
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
return sum
`,
},
{
name: "Function Call",
code: `
local result = 0
for i = 1, 100 do
result = result + i
end
return result
`,
},
{
name: "Table Creation",
code: `
local t = {}
for i = 1, 100 do
t[i] = i * 2
end
return t[50]
`,
},
{
name: "String Operations",
code: `
local s = "hello"
for i = 1, 10 do
s = s .. " world"
end
return #s
`,
},
}
func runBenchmark(L *luajit.State, code string, duration time.Duration) (time.Duration, int64) {
start := time.Now()
deadline := start.Add(duration)
var ops int64
for time.Now().Before(deadline) {
if err := L.DoString(code); err != nil {
fmt.Printf("Error executing code: %v\n", err)
return 0, 0
}
L.Pop(1)
ops++
}
return time.Since(start), ops
}
func runBytecodeTest(L *luajit.State, code string, duration time.Duration) (time.Duration, int64) {
// First compile the bytecode
bytecode, err := L.CompileBytecode(code, "bench")
if err != nil {
fmt.Printf("Error compiling bytecode: %v\n", err)
return 0, 0
}
start := time.Now()
deadline := start.Add(duration)
var ops int64
for time.Now().Before(deadline) {
if err := L.LoadBytecode(bytecode, "bench"); err != nil {
fmt.Printf("Error executing bytecode: %v\n", err)
return 0, 0
}
ops++
}
return time.Since(start), ops
}
func benchmarkCase(newState func() *luajit.State, bc benchCase) {
fmt.Printf("\n%s:\n", bc.name)
// Direct execution benchmark
L := newState()
if L == nil {
fmt.Printf(" Failed to create Lua state\n")
return
}
execTime, ops := runBenchmark(L, bc.code, 2*time.Second)
L.Close()
if ops > 0 {
opsPerSec := float64(ops) / execTime.Seconds()
fmt.Printf(" Direct: %.0f ops/sec\n", opsPerSec)
}
// Bytecode execution benchmark
L = newState()
if L == nil {
fmt.Printf(" Failed to create Lua state\n")
return
}
execTime, ops = runBytecodeTest(L, bc.code, 2*time.Second)
L.Close()
if ops > 0 {
opsPerSec := float64(ops) / execTime.Seconds()
fmt.Printf(" Bytecode: %.0f ops/sec\n", opsPerSec)
}
}
func main() {
modes := []struct {
name string
newState func() *luajit.State
}{
{"Safe", luajit.NewSafe},
{"Unsafe", luajit.New},
}
for _, mode := range modes {
fmt.Printf("\n=== %s Mode ===\n", mode.name)
for _, c := range cases {
benchmarkCase(mode.newState, c)
}
}
}

72
builder.go Normal file
View File

@ -0,0 +1,72 @@
package luajit
// TableBuilder provides a fluent interface for building Lua tables
type TableBuilder struct {
state *State
index int
}
// NewTableBuilder creates a new table and returns a builder
func (s *State) NewTableBuilder() *TableBuilder {
s.NewTable()
return &TableBuilder{
state: s,
index: s.GetTop(),
}
}
// SetString sets a string field
func (tb *TableBuilder) SetString(key, value string) *TableBuilder {
tb.state.PushString(value)
tb.state.SetField(tb.index, key)
return tb
}
// SetNumber sets a number field
func (tb *TableBuilder) SetNumber(key string, value float64) *TableBuilder {
tb.state.PushNumber(value)
tb.state.SetField(tb.index, key)
return tb
}
// SetBool sets a boolean field
func (tb *TableBuilder) SetBool(key string, value bool) *TableBuilder {
tb.state.PushBoolean(value)
tb.state.SetField(tb.index, key)
return tb
}
// SetNil sets a nil field
func (tb *TableBuilder) SetNil(key string) *TableBuilder {
tb.state.PushNil()
tb.state.SetField(tb.index, key)
return tb
}
// SetTable sets a table field
func (tb *TableBuilder) SetTable(key string, value any) *TableBuilder {
if err := tb.state.PushValue(value); err == nil {
tb.state.SetField(tb.index, key)
}
return tb
}
// SetArray sets an array field
func (tb *TableBuilder) SetArray(key string, values []any) *TableBuilder {
tb.state.CreateTable(len(values), 0)
for i, v := range values {
tb.state.PushNumber(float64(i + 1))
if err := tb.state.PushValue(v); err == nil {
tb.state.SetTable(-3)
} else {
tb.state.Pop(1)
}
}
tb.state.SetField(tb.index, key)
return tb
}
// Build finalizes the table (no-op, table is already on stack)
func (tb *TableBuilder) Build() {
// Table is already on the stack at tb.index
}

View File

@ -7,99 +7,128 @@ package luajit
#include <string.h>
typedef struct {
const unsigned char *buf;
size_t size;
const char *name;
const unsigned char *buf;
size_t size;
const char *name;
} BytecodeReader;
static const char *bytecode_reader(lua_State *L, void *ud, size_t *size) {
BytecodeReader *r = (BytecodeReader *)ud;
(void)L; // unused
if (r->size == 0) return NULL;
*size = r->size;
r->size = 0; // Only read once
return (const char *)r->buf;
}
static int load_bytecode_chunk(lua_State *L, const unsigned char *buf, size_t len, const char *name) {
BytecodeReader reader = {buf, len, name};
return lua_load(L, bytecode_reader, &reader, name);
}
typedef struct {
unsigned char *buf;
size_t len;
} BytecodeWriter;
unsigned char *buf;
size_t size;
size_t capacity;
} BytecodeBuffer;
int bytecode_writer(lua_State *L, const void *p, size_t sz, void *ud) {
BytecodeWriter *w = (BytecodeWriter *)ud;
unsigned char *newbuf;
(void)L; // unused
const char *bytecode_reader(lua_State *L, void *ud, size_t *size) {
BytecodeReader *r = (BytecodeReader *)ud;
(void)L; // unused
if (r->size == 0) return NULL;
*size = r->size;
r->size = 0; // Only read once
return (const char *)r->buf;
}
newbuf = (unsigned char *)realloc(w->buf, w->len + sz);
if (newbuf == NULL) return 1;
int load_bytecode(lua_State *L, const unsigned char *buf, size_t len, const char *name) {
BytecodeReader reader = {buf, len, name};
return lua_load(L, bytecode_reader, &reader, name);
}
memcpy(newbuf + w->len, p, sz);
w->buf = newbuf;
w->len += sz;
return 0;
// Optimized bytecode writer with pre-allocated buffer
int buffered_bytecode_writer(lua_State *L, const void *p, size_t sz, void *ud) {
BytecodeBuffer *buf = (BytecodeBuffer *)ud;
// Grow buffer if needed (double size to avoid frequent reallocs)
if (buf->size + sz > buf->capacity) {
size_t new_capacity = buf->capacity;
while (new_capacity < buf->size + sz) {
new_capacity *= 2;
}
unsigned char *newbuf = realloc(buf->buf, new_capacity);
if (newbuf == NULL) return 1;
buf->buf = newbuf;
buf->capacity = new_capacity;
}
memcpy(buf->buf + buf->size, p, sz);
buf->size += sz;
return 0;
}
// Combined load and run bytecode in a single call
int load_and_run_bytecode(lua_State *L, const unsigned char *buf, size_t len,
const char *name, int nresults) {
BytecodeReader reader = {buf, len, name};
int status = lua_load(L, bytecode_reader, &reader, name);
if (status != 0) return status;
return lua_pcall(L, 0, nresults, 0);
}
*/
import "C"
import (
"fmt"
"sync"
"unsafe"
)
func (s *State) compileBytecodeUnsafe(code string, name string) ([]byte, error) {
// First load the string but don't execute it
ccode := C.CString(code)
defer C.free(unsafe.Pointer(ccode))
// bytecodeBuffer wraps []byte to avoid boxing allocations in sync.Pool
type bytecodeBuffer struct {
data []byte
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
// Buffer pool for bytecode generation
var bytecodeBufferPool = sync.Pool{
New: func() any {
return &bytecodeBuffer{data: make([]byte, 0, 1024)}
},
}
if C.luaL_loadstring(s.L, ccode) != 0 {
err := &LuaError{
Code: int(C.lua_status(s.L)),
Message: s.ToString(-1),
}
s.Pop(1)
// CompileBytecode compiles a Lua chunk to bytecode without executing it
func (s *State) CompileBytecode(code string, name string) ([]byte, error) {
if err := s.LoadString(code); err != nil {
return nil, fmt.Errorf("failed to load string: %w", err)
}
// Set up writer
var writer C.BytecodeWriter
writer.buf = nil
writer.len = 0
// Always use C memory for dump operation to avoid cgo pointer issues
cbuf := C.BytecodeBuffer{
buf: (*C.uchar)(C.malloc(1024)),
size: 0,
capacity: 1024,
}
if cbuf.buf == nil {
return nil, fmt.Errorf("failed to allocate initial buffer")
}
// Dump the function to bytecode
if C.lua_dump(s.L, (*[0]byte)(C.bytecode_writer), unsafe.Pointer(&writer)) != 0 {
if writer.buf != nil {
C.free(unsafe.Pointer(writer.buf))
}
s.Pop(1)
return nil, fmt.Errorf("failed to dump bytecode")
status := C.lua_dump(s.L, (*[0]byte)(unsafe.Pointer(C.buffered_bytecode_writer)), unsafe.Pointer(&cbuf))
s.Pop(1) // Remove the function from stack
if status != 0 {
C.free(unsafe.Pointer(cbuf.buf))
return nil, fmt.Errorf("failed to dump bytecode: status %d", status)
}
// Copy to Go slice
bytecode := C.GoBytes(unsafe.Pointer(writer.buf), C.int(writer.len))
// Clean up
if writer.buf != nil {
C.free(unsafe.Pointer(writer.buf))
// Copy to Go memory and free C buffer
var result []byte
if cbuf.size > 0 {
result = C.GoBytes(unsafe.Pointer(cbuf.buf), C.int(cbuf.size))
}
s.Pop(1) // Remove the function
C.free(unsafe.Pointer(cbuf.buf))
return bytecode, nil
return result, nil
}
func (s *State) loadBytecodeUnsafe(bytecode []byte, name string) error {
// LoadBytecode loads precompiled bytecode without executing it
func (s *State) LoadBytecode(bytecode []byte, name string) error {
if len(bytecode) == 0 {
return fmt.Errorf("empty bytecode")
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
// Load the bytecode
status := C.load_bytecode_chunk(
status := C.load_bytecode(
s.L,
(*C.uchar)(unsafe.Pointer(&bytecode[0])),
C.size_t(len(bytecode)),
@ -107,53 +136,96 @@ func (s *State) loadBytecodeUnsafe(bytecode []byte, name string) error {
)
if status != 0 {
err := &LuaError{
Code: int(status),
Message: s.ToString(-1),
}
s.Pop(1)
return fmt.Errorf("failed to load bytecode: %w", err)
}
// Execute the loaded chunk
if err := s.safeCall(func() C.int {
return C.lua_pcall(s.L, 0, 0, 0)
}); err != nil {
return fmt.Errorf("failed to execute bytecode: %w", err)
err := s.CreateLuaError(int(status), fmt.Sprintf("LoadBytecode(%s)", name))
s.Pop(1) // Remove error message
return err
}
return nil
}
// CompileBytecode compiles a Lua chunk to bytecode without executing it
func (s *State) CompileBytecode(code string, name string) ([]byte, error) {
if s.safeStack {
return stackGuardValue[[]byte](s, func() ([]byte, error) {
return s.compileBytecodeUnsafe(code, name)
})
}
return s.compileBytecodeUnsafe(code, name)
// RunBytecode executes previously loaded bytecode with 0 results
func (s *State) RunBytecode() error {
return s.RunBytecodeWithResults(0)
}
// LoadBytecode loads precompiled bytecode and executes it
func (s *State) LoadBytecode(bytecode []byte, name string) error {
if s.safeStack {
return stackGuardErr(s, func() error {
return s.loadBytecodeUnsafe(bytecode, name)
})
// RunBytecodeWithResults executes bytecode and keeps nresults on the stack
func (s *State) RunBytecodeWithResults(nresults int) error {
status := C.lua_pcall(s.L, 0, C.int(nresults), 0)
if status != 0 {
err := s.CreateLuaError(int(status), fmt.Sprintf("RunBytecode(nresults=%d)", nresults))
s.Pop(1) // Remove error message
return err
}
return s.loadBytecodeUnsafe(bytecode, name)
return nil
}
// Helper function to compile and immediately load/execute bytecode
func (s *State) CompileAndLoad(code string, name string) error {
// LoadAndRunBytecode loads and executes bytecode in a single CGO transition
func (s *State) LoadAndRunBytecode(bytecode []byte, name string) error {
if len(bytecode) == 0 {
return fmt.Errorf("empty bytecode")
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
status := C.load_and_run_bytecode(
s.L,
(*C.uchar)(unsafe.Pointer(&bytecode[0])),
C.size_t(len(bytecode)),
cname,
0,
)
if status != 0 {
err := s.CreateLuaError(int(status), fmt.Sprintf("LoadAndRunBytecode(%s)", name))
s.Pop(1) // Remove error message
return err
}
return nil
}
// LoadAndRunBytecodeWithResults loads and executes bytecode, preserving results
func (s *State) LoadAndRunBytecodeWithResults(bytecode []byte, name string, nresults int) error {
if len(bytecode) == 0 {
return fmt.Errorf("empty bytecode")
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
status := C.load_and_run_bytecode(
s.L,
(*C.uchar)(unsafe.Pointer(&bytecode[0])),
C.size_t(len(bytecode)),
cname,
C.int(nresults),
)
if status != 0 {
err := s.CreateLuaError(int(status), fmt.Sprintf("LoadAndRunBytecodeWithResults(%s, nresults=%d)", name, nresults))
s.Pop(1) // Remove error message
return err
}
return nil
}
// CompileAndRun compiles and immediately executes Lua code
func (s *State) CompileAndRun(code string, name string) error {
// Skip bytecode step for small scripts - direct execution is faster
if len(code) < 1024 {
return s.DoString(code)
}
bytecode, err := s.CompileBytecode(code, name)
if err != nil {
return fmt.Errorf("compile error: %w", err)
}
if err := s.LoadBytecode(bytecode, name); err != nil {
return fmt.Errorf("load error: %w", err)
if err := s.LoadAndRunBytecode(bytecode, name); err != nil {
return fmt.Errorf("execution error: %w", err)
}
return nil

View File

@ -1,178 +0,0 @@
package luajit
import (
"fmt"
"testing"
)
func TestBytecodeCompilation(t *testing.T) {
tests := []struct {
name string
code string
wantErr bool
}{
{
name: "simple assignment",
code: "x = 42",
wantErr: false,
},
{
name: "function definition",
code: "function add(a,b) return a+b end",
wantErr: false,
},
{
name: "syntax error",
code: "function bad syntax",
wantErr: true,
},
}
for _, f := range factories {
for _, tt := range tests {
t.Run(f.name+"/"+tt.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
bytecode, err := L.CompileBytecode(tt.code, "test")
if (err != nil) != tt.wantErr {
t.Errorf("CompileBytecode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if len(bytecode) == 0 {
t.Error("CompileBytecode() returned empty bytecode")
}
}
})
}
}
}
func TestBytecodeExecution(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
// Compile some test code
code := `
function add(a, b)
return a + b
end
result = add(40, 2)
`
bytecode, err := L.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode() error = %v", err)
}
// Load and execute the bytecode
if err := L.LoadBytecode(bytecode, "test"); err != nil {
t.Fatalf("LoadBytecode() error = %v", err)
}
// Verify the result
L.GetGlobal("result")
if result := L.ToNumber(-1); result != 42 {
t.Errorf("got result = %v, want 42", result)
}
})
}
}
func TestInvalidBytecode(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
// Test with invalid bytecode
invalidBytecode := []byte("this is not valid bytecode")
if err := L.LoadBytecode(invalidBytecode, "test"); err == nil {
t.Error("LoadBytecode() expected error with invalid bytecode")
}
})
}
}
func TestBytecodeRoundTrip(t *testing.T) {
tests := []struct {
name string
code string
check func(*State) error
}{
{
name: "global variable",
code: "x = 42",
check: func(L *State) error {
L.GetGlobal("x")
if x := L.ToNumber(-1); x != 42 {
return fmt.Errorf("got x = %v, want 42", x)
}
return nil
},
},
{
name: "function definition",
code: "function test() return 'hello' end",
check: func(L *State) error {
if err := L.DoString("result = test()"); err != nil {
return err
}
L.GetGlobal("result")
if s := L.ToString(-1); s != "hello" {
return fmt.Errorf("got result = %q, want 'hello'", s)
}
return nil
},
},
}
for _, f := range factories {
for _, tt := range tests {
t.Run(f.name+"/"+tt.name, func(t *testing.T) {
// First state for compilation
L1 := f.new()
if L1 == nil {
t.Fatal("Failed to create first Lua state")
}
defer L1.Close()
// Compile the code
bytecode, err := L1.CompileBytecode(tt.code, "test")
if err != nil {
t.Fatalf("CompileBytecode() error = %v", err)
}
// Second state for execution
L2 := f.new()
if L2 == nil {
t.Fatal("Failed to create second Lua state")
}
defer L2.Close()
// Load and execute the bytecode
if err := L2.LoadBytecode(bytecode, "test"); err != nil {
t.Fatalf("LoadBytecode() error = %v", err)
}
// Run the check function
if err := tt.check(L2); err != nil {
t.Errorf("check failed: %v", err)
}
})
}
}
}

70
example/main.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go script.lua")
os.Exit(1)
}
scriptPath := os.Args[1]
// Create a new Lua state
L := luajit.New()
if L == nil {
log.Fatal("Failed to create Lua state")
}
defer L.Close()
// Register a Go function to be called from Lua
L.RegisterGoFunction("printFromGo", func(s *luajit.State) int {
msg := s.ToString(1) // Get first argument
fmt.Printf("Go received from Lua: %s\n", msg)
// Return a value to Lua
s.PushString("Hello from Go!")
return 1 // Number of return values
})
// Add some values to the Lua environment
L.PushValue(map[string]any{
"appName": "LuaJIT Example",
"version": 1.0,
"features": []float64{1, 2, 3},
})
L.SetGlobal("config")
// Get the directory of the script to properly handle requires
dir := filepath.Dir(scriptPath)
L.AddPackagePath(filepath.Join(dir, "?.lua"))
// Execute the script
fmt.Printf("Running Lua script: %s\n", scriptPath)
if err := L.DoFile(scriptPath); err != nil {
log.Fatalf("Error executing script: %v", err)
}
// Call a Lua function and get its result
L.GetGlobal("getResult")
if L.IsFunction(-1) {
if err := L.Call(0, 1); err != nil {
log.Fatalf("Error calling Lua function: %v", err)
}
result, err := L.ToValue(-1)
if err != nil {
log.Fatalf("Error converting Lua result: %v", err)
}
fmt.Printf("Result from Lua: %v\n", result)
L.Pop(1) // Clean up the result
}
}

35
example/script.lua Normal file
View File

@ -0,0 +1,35 @@
-- Example Lua script to demonstrate Go-Lua integration
-- Access the config table passed from Go
print("Script started")
print("App name:", config.appName)
print("Version:", config.version)
print("Features:", table.concat(config.features, ", "))
-- Call the Go function
local response = printFromGo("Hello from Lua!")
print("Response from Go:", response)
-- Function that will be called from Go
function getResult()
local result = {
status = "success",
calculations = {
sum = 10 + 20,
product = 5 * 7
},
message = "Calculation completed"
}
return result
end
-- Load external module (if available)
local success, utils = pcall(require, "utils")
if success then
print("Utils module loaded")
utils.doSomething()
else
print("Utils module not available:", utils)
end
print("Script completed")

19
example/utils.lua Normal file
View File

@ -0,0 +1,19 @@
-- Optional utility module
local utils = {}
function utils.doSomething()
print("Utils module function called")
return true
end
function utils.calculate(a, b)
return {
sum = a + b,
difference = a - b,
product = a * b,
quotient = a / b
}
end
return utils

View File

@ -7,8 +7,9 @@ package luajit
extern int goFunctionWrapper(lua_State* L);
// Helper function to access upvalues
static int get_upvalue_index(int i) {
return -10002 - i; // LUA_GLOBALSINDEX - i
return lua_upvalueindex(i);
}
*/
import "C"
@ -18,25 +19,39 @@ import (
"unsafe"
)
// GoFunction defines the signature for Go functions callable from Lua
type GoFunction func(*State) int
// Static registry size reduces resizing operations
const initialRegistrySize = 64
var (
// functionRegistry stores all registered Go functions
functionRegistry = struct {
sync.RWMutex
funcs map[unsafe.Pointer]GoFunction
funcs map[unsafe.Pointer]GoFunction
initOnce sync.Once
}{
funcs: make(map[unsafe.Pointer]GoFunction),
funcs: make(map[unsafe.Pointer]GoFunction, initialRegistrySize),
}
// statePool reuses State structs to avoid allocations
statePool = sync.Pool{
New: func() any {
return &State{}
},
}
)
//export goFunctionWrapper
func goFunctionWrapper(L *C.lua_State) C.int {
state := &State{L: L, safeStack: true}
state := statePool.Get().(*State)
state.L = L
defer statePool.Put(state)
// Get upvalue using standard Lua 5.1 macro
ptr := C.lua_touserdata(L, C.get_upvalue_index(1))
if ptr == nil {
state.PushString("error: function not found")
state.PushString("error: function pointer not found")
return -1
}
@ -49,50 +64,53 @@ func goFunctionWrapper(L *C.lua_State) C.int {
return -1
}
result := fn(state)
return C.int(result)
return C.int(fn(state))
}
// PushGoFunction wraps a Go function and pushes it onto the Lua stack
func (s *State) PushGoFunction(fn GoFunction) error {
// Push lightuserdata as upvalue and create closure
ptr := C.malloc(1)
// Allocate unique memory for each function
ptr := C.malloc(C.size_t(unsafe.Sizeof(uintptr(0))))
if ptr == nil {
return fmt.Errorf("failed to allocate memory for function pointer")
}
// Register the function
functionRegistry.Lock()
functionRegistry.funcs[ptr] = fn
functionRegistry.Unlock()
// Push the pointer as lightuserdata
C.lua_pushlightuserdata(s.L, ptr)
C.lua_pushcclosure(s.L, (*[0]byte)(C.goFunctionWrapper), 1)
return nil
}
// RegisterGoFunction registers a Go function as a global Lua function
func (s *State) RegisterGoFunction(name string, fn GoFunction) error {
if err := s.PushGoFunction(fn); err != nil {
return err
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
C.lua_setfield(s.L, C.LUA_GLOBALSINDEX, cname)
s.SetGlobal(name)
return nil
}
// UnregisterGoFunction removes a global function
func (s *State) UnregisterGoFunction(name string) {
s.PushNil()
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
C.lua_setfield(s.L, C.LUA_GLOBALSINDEX, cname)
s.SetGlobal(name)
}
// Cleanup frees all function pointers and clears the registry
func (s *State) Cleanup() {
functionRegistry.Lock()
defer functionRegistry.Unlock()
// Free all allocated pointers
for ptr := range functionRegistry.funcs {
C.free(ptr)
delete(functionRegistry.funcs, ptr)
}
functionRegistry.funcs = make(map[unsafe.Pointer]GoFunction)
}

View File

@ -1,109 +0,0 @@
package luajit
import "testing"
func TestGoFunctions(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
defer L.Cleanup()
addFunc := func(s *State) int {
s.PushNumber(s.ToNumber(1) + s.ToNumber(2))
return 1
}
if err := L.RegisterGoFunction("add", addFunc); err != nil {
t.Fatalf("Failed to register function: %v", err)
}
// Test basic function call
if err := L.DoString("result = add(40, 2)"); err != nil {
t.Fatalf("Failed to call function: %v", err)
}
L.GetGlobal("result")
if result := L.ToNumber(-1); result != 42 {
t.Errorf("got %v, want 42", result)
}
L.Pop(1)
// Test multiple return values
multiFunc := func(s *State) int {
s.PushString("hello")
s.PushNumber(42)
s.PushBoolean(true)
return 3
}
if err := L.RegisterGoFunction("multi", multiFunc); err != nil {
t.Fatalf("Failed to register multi function: %v", err)
}
code := `
a, b, c = multi()
result = (a == "hello" and b == 42 and c == true)
`
if err := L.DoString(code); err != nil {
t.Fatalf("Failed to call multi function: %v", err)
}
L.GetGlobal("result")
if !L.ToBoolean(-1) {
t.Error("Multiple return values test failed")
}
L.Pop(1)
// Test error handling
errFunc := func(s *State) int {
s.PushString("test error")
return -1
}
if err := L.RegisterGoFunction("err", errFunc); err != nil {
t.Fatalf("Failed to register error function: %v", err)
}
if err := L.DoString("err()"); err == nil {
t.Error("Expected error from error function")
}
// Test unregistering
L.UnregisterGoFunction("add")
if err := L.DoString("add(1, 2)"); err == nil {
t.Error("Expected error calling unregistered function")
}
})
}
}
func TestStackSafety(t *testing.T) {
L := NewSafe()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
defer L.Cleanup()
// Test stack overflow protection
overflowFunc := func(s *State) int {
for i := 0; i < 100; i++ {
s.PushNumber(float64(i))
}
s.PushString("done")
return 101
}
if err := L.RegisterGoFunction("overflow", overflowFunc); err != nil {
t.Fatal(err)
}
if err := L.DoString("overflow()"); err != nil {
t.Logf("Got expected error: %v", err)
}
}

175
stack.go
View File

@ -5,104 +5,115 @@ package luajit
#include <lauxlib.h>
*/
import "C"
import "fmt"
import (
"fmt"
"strings"
)
// LuaError represents an error from the Lua state
// LuaError represents an enhanced error from the Lua state
type LuaError struct {
Code int
Message string
Code int
Message string
File string
Line int
StackTrace string
Context string
}
func (e *LuaError) Error() string {
return fmt.Sprintf("lua error (code=%d): %s", e.Code, e.Message)
}
var parts []string
// Stack management constants from lua.h
const (
LUA_MINSTACK = 20 // Minimum Lua stack size
LUA_MAXSTACK = 1000000 // Maximum Lua stack size
LUA_REGISTRYINDEX = -10000 // Pseudo-index for the Lua registry
LUA_GLOBALSINDEX = -10002 // Pseudo-index for globals table
)
// checkStack ensures there is enough space on the Lua stack
func (s *State) checkStack(n int) error {
if C.lua_checkstack(s.L, C.int(n)) == 0 {
return fmt.Errorf("stack overflow (cannot allocate %d slots)", n)
}
return nil
}
// safeCall wraps a potentially dangerous C call with stack checking
func (s *State) safeCall(f func() C.int) error {
// Save current stack size
top := s.GetTop()
// Ensure we have enough stack space (minimum 20 slots as per Lua standard)
if err := s.checkStack(LUA_MINSTACK); err != nil {
return err
if e.File != "" && e.Line > 0 {
parts = append(parts, fmt.Sprintf("%s:%d", e.File, e.Line))
}
// Make the call
status := f()
if e.Context != "" {
parts = append(parts, fmt.Sprintf("[%s]", e.Context))
}
// Check for errors
if status != 0 {
err := &LuaError{
Code: int(status),
Message: s.ToString(-1),
parts = append(parts, e.Message)
if e.Code != 0 {
parts = append(parts, fmt.Sprintf("(code=%d)", e.Code))
}
result := strings.Join(parts, " ")
if e.StackTrace != "" {
result += "\n" + e.StackTrace
}
return result
}
// GetStackTrace returns the current Lua stack trace
func (s *State) GetStackTrace() string {
s.GetGlobal("debug")
if !s.IsTable(-1) {
s.Pop(1)
return "debug table not available"
}
s.GetField(-1, "traceback")
if !s.IsFunction(-1) {
s.Pop(2)
return "debug.traceback not available"
}
s.Call(0, 1)
trace := s.ToString(-1)
s.Pop(1)
return trace
}
// GetErrorInfo extracts detailed error information from the Lua stack
func (s *State) GetErrorInfo(context string) *LuaError {
if s.GetTop() == 0 {
return &LuaError{
Message: "unknown error (empty stack)",
Context: context,
}
s.Pop(1) // Remove error message
return err
}
// For lua_pcall, the function and arguments are popped before results are pushed
// So we don't consider it an underflow if the new top is less than the original
if status == 0 && s.GetType(-1) == TypeFunction {
// If we still have a function on the stack, restore original size
s.SetTop(top)
message := s.ToString(-1)
// Parse file:line from common Lua error format
var file string
var line int
if colonPos := strings.Index(message, ":"); colonPos > 0 {
beforeColon := message[:colonPos]
afterColon := message[colonPos+1:]
if secondColonPos := strings.Index(afterColon, ":"); secondColonPos > 0 {
file = beforeColon
if n, err := fmt.Sscanf(afterColon[:secondColonPos], "%d", &line); n == 1 && err == nil {
message = strings.TrimSpace(afterColon[secondColonPos+1:])
}
}
}
return nil
stackTrace := s.GetStackTrace()
return &LuaError{
Message: message,
File: file,
Line: line,
StackTrace: stackTrace,
Context: context,
}
}
// stackGuard wraps a function with stack checking
func stackGuard[T any](s *State, f func() (T, error)) (T, error) {
// Save current stack size
top := s.GetTop()
defer func() {
// Only restore if stack is larger than original
if s.GetTop() > top {
s.SetTop(top)
}
}()
// Run the protected function
return f()
// CreateLuaError creates a LuaError with full context information
func (s *State) CreateLuaError(code int, context string) *LuaError {
err := s.GetErrorInfo(context)
err.Code = code
return err
}
// stackGuardValue executes a function with stack protection
func stackGuardValue[T any](s *State, f func() (T, error)) (T, error) {
return stackGuard(s, f)
}
// stackGuardErr executes a function that only returns an error with stack protection
func stackGuardErr(s *State, f func() error) error {
// Save current stack size
top := s.GetTop()
defer func() {
// Only restore if stack is larger than original
if s.GetTop() > top {
s.SetTop(top)
}
}()
// Run the protected function
return f()
}
// getStackTrace returns the current Lua stack trace
func (s *State) getStackTrace() string {
// Same implementation...
return ""
// PushError pushes an error string and returns -1
func (s *State) PushError(format string, args ...any) int {
s.PushString(fmt.Sprintf(format, args...))
return -1
}

177
table.go
View File

@ -1,177 +0,0 @@
package luajit
/*
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdlib.h>
static int get_table_length(lua_State *L, int index) {
return lua_objlen(L, index);
}
*/
import "C"
import (
"fmt"
)
// TableValue represents any value that can be stored in a Lua table
type TableValue interface {
~string | ~float64 | ~bool | ~int | ~map[string]interface{} | ~[]float64 | ~[]interface{}
}
func (s *State) GetTableLength(index int) int { return int(C.get_table_length(s.L, C.int(index))) }
// ToTable converts a Lua table to a Go map
func (s *State) ToTable(index int) (map[string]interface{}, error) {
if s.safeStack {
return stackGuardValue[map[string]interface{}](s, func() (map[string]interface{}, error) {
if !s.IsTable(index) {
return nil, fmt.Errorf("not a table at index %d", index)
}
return s.toTableUnsafe(index)
})
}
if !s.IsTable(index) {
return nil, fmt.Errorf("not a table at index %d", index)
}
return s.toTableUnsafe(index)
}
func (s *State) pushTableSafe(table map[string]interface{}) error {
size := 2
if err := s.checkStack(size); err != nil {
return fmt.Errorf("insufficient stack space: %w", err)
}
s.NewTable()
for k, v := range table {
if err := s.pushValueSafe(v); err != nil {
return err
}
s.SetField(-2, k)
}
return nil
}
func (s *State) pushTableUnsafe(table map[string]interface{}) error {
s.NewTable()
for k, v := range table {
if err := s.pushValueUnsafe(v); err != nil {
return err
}
s.SetField(-2, k)
}
return nil
}
func (s *State) toTableSafe(index int) (map[string]interface{}, error) {
if err := s.checkStack(2); err != nil {
return nil, err
}
return s.toTableUnsafe(index)
}
func (s *State) toTableUnsafe(index int) (map[string]interface{}, error) {
absIdx := s.absIndex(index)
table := make(map[string]interface{})
// Check if it's an array-like table
length := s.GetTableLength(absIdx)
if length > 0 {
array := make([]float64, length)
isArray := true
// Try to convert to array
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(absIdx)
if s.GetType(-1) != TypeNumber {
isArray = false
s.Pop(1)
break
}
array[i-1] = s.ToNumber(-1)
s.Pop(1)
}
if isArray {
return map[string]interface{}{"": array}, nil
}
}
// Handle regular table
s.PushNil()
for C.lua_next(s.L, C.int(absIdx)) != 0 {
key := ""
valueType := C.lua_type(s.L, -2)
if valueType == C.LUA_TSTRING {
key = s.ToString(-2)
} else if valueType == C.LUA_TNUMBER {
key = fmt.Sprintf("%g", s.ToNumber(-2))
}
value, err := s.toValueUnsafe(-1)
if err != nil {
s.Pop(1)
return nil, err
}
// Handle nested array case
if m, ok := value.(map[string]interface{}); ok {
if arr, ok := m[""]; ok {
value = arr
}
}
table[key] = value
s.Pop(1)
}
return table, nil
}
// NewTable creates a new table and pushes it onto the stack
func (s *State) NewTable() {
if s.safeStack {
if err := s.checkStack(1); err != nil {
// Since we can't return an error, we'll push nil instead
s.PushNil()
return
}
}
C.lua_createtable(s.L, 0, 0)
}
// SetTable sets a table field with cached absolute index
func (s *State) SetTable(index int) {
absIdx := index
if s.safeStack && (index < 0 && index > LUA_REGISTRYINDEX) {
absIdx = s.GetTop() + index + 1
}
C.lua_settable(s.L, C.int(absIdx))
}
// GetTable gets a table field with cached absolute index
func (s *State) GetTable(index int) {
absIdx := index
if s.safeStack && (index < 0 && index > LUA_REGISTRYINDEX) {
absIdx = s.GetTop() + index + 1
}
if s.safeStack {
if err := s.checkStack(1); err != nil {
s.PushNil()
return
}
}
C.lua_gettable(s.L, C.int(absIdx))
}
// PushTable pushes a Go map onto the Lua stack as a table with stack checking
func (s *State) PushTable(table map[string]interface{}) error {
if s.safeStack {
return s.pushTableSafe(table)
}
return s.pushTableUnsafe(table)
}

View File

@ -1,97 +0,0 @@
package luajit
import (
"math"
"testing"
)
func TestTableOperations(t *testing.T) {
tests := []struct {
name string
data map[string]interface{}
}{
{
name: "empty",
data: map[string]interface{}{},
},
{
name: "primitives",
data: map[string]interface{}{
"str": "hello",
"num": 42.0,
"bool": true,
"array": []float64{1.1, 2.2, 3.3},
},
},
{
name: "nested",
data: map[string]interface{}{
"nested": map[string]interface{}{
"value": 123.0,
"array": []float64{4.4, 5.5},
},
},
},
}
for _, f := range factories {
for _, tt := range tests {
t.Run(f.name+"/"+tt.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
if err := L.PushTable(tt.data); err != nil {
t.Fatalf("PushTable() error = %v", err)
}
got, err := L.ToTable(-1)
if err != nil {
t.Fatalf("ToTable() error = %v", err)
}
if !tablesEqual(got, tt.data) {
t.Errorf("table mismatch\ngot = %v\nwant = %v", got, tt.data)
}
})
}
}
}
func tablesEqual(a, b map[string]interface{}) bool {
if len(a) != len(b) {
return false
}
for k, v1 := range a {
v2, ok := b[k]
if !ok {
return false
}
switch v1 := v1.(type) {
case map[string]interface{}:
v2, ok := v2.(map[string]interface{})
if !ok || !tablesEqual(v1, v2) {
return false
}
case []float64:
v2, ok := v2.([]float64)
if !ok || len(v1) != len(v2) {
return false
}
for i := range v1 {
if math.Abs(v1[i]-v2[i]) > 1e-10 {
return false
}
}
default:
if v1 != v2 {
return false
}
}
}
return true
}

443
tests/bytecode_test.go Normal file
View File

@ -0,0 +1,443 @@
package luajit_test
import (
"bytes"
"errors"
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
// TestCompileBytecode tests basic bytecode compilation
func TestCompileBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
code := "return 42"
bytecode, err := state.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
if len(bytecode) == 0 {
t.Fatal("Expected non-empty bytecode")
}
}
// TestLoadBytecode tests loading precompiled bytecode
func TestLoadBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// First compile some bytecode
code := "answer = 42"
bytecode, err := state.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Then load it
err = state.LoadBytecode(bytecode, "test")
if err != nil {
t.Fatalf("LoadBytecode failed: %v", err)
}
// Verify a function is on the stack
if !state.IsFunction(-1) {
t.Fatal("Expected function at top of stack after LoadBytecode")
}
// Pop the function
state.Pop(1)
}
// TestRunBytecode tests running previously loaded bytecode
func TestRunBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// First compile and load bytecode
code := "answer = 42"
bytecode, err := state.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
err = state.LoadBytecode(bytecode, "test")
if err != nil {
t.Fatalf("LoadBytecode failed: %v", err)
}
// Run the bytecode
err = state.RunBytecode()
if err != nil {
t.Fatalf("RunBytecode failed: %v", err)
}
// Verify the code has executed correctly
state.GetGlobal("answer")
if !state.IsNumber(-1) || state.ToNumber(-1) != 42 {
t.Fatalf("Expected answer to be 42, got %v", state.ToNumber(-1))
}
state.Pop(1)
}
// TestLoadAndRunBytecode tests the combined load and run functionality
func TestLoadAndRunBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Compile bytecode
code := "answer = 42"
bytecode, err := state.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Load and run in one step
err = state.LoadAndRunBytecode(bytecode, "test")
if err != nil {
t.Fatalf("LoadAndRunBytecode failed: %v", err)
}
// Verify execution
state.GetGlobal("answer")
if !state.IsNumber(-1) || state.ToNumber(-1) != 42 {
t.Fatalf("Expected answer to be 42, got %v", state.ToNumber(-1))
}
state.Pop(1)
}
// TestCompileAndRun tests compile and run functionality
func TestCompileAndRun(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Compile and run in one step
code := "answer = 42"
err := state.CompileAndRun(code, "test")
if err != nil {
t.Fatalf("CompileAndRun failed: %v", err)
}
// Verify execution
state.GetGlobal("answer")
if !state.IsNumber(-1) || state.ToNumber(-1) != 42 {
t.Fatalf("Expected answer to be 42, got %v", state.ToNumber(-1))
}
state.Pop(1)
}
// TestEmptyBytecode tests error handling for empty bytecode
func TestEmptyBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Try to load empty bytecode
err := state.LoadBytecode([]byte{}, "empty")
if err == nil {
t.Fatal("Expected error for empty bytecode, got nil")
}
}
// TestInvalidBytecode tests error handling for invalid bytecode
func TestInvalidBytecode(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create some invalid bytecode
invalidBytecode := []byte("not valid bytecode")
// Try to load invalid bytecode
err := state.LoadBytecode(invalidBytecode, "invalid")
if err == nil {
t.Fatal("Expected error for invalid bytecode, got nil")
}
}
// TestBytecodeSerialization tests serializing and deserializing bytecode
func TestBytecodeSerialization(t *testing.T) {
// First state to compile
state1 := luajit.New()
if state1 == nil {
t.Fatal("Failed to create first Lua state")
}
defer state1.Close()
// Compile bytecode
code := `
function add(a, b)
return a + b
end
result = add(10, 20)
`
bytecode, err := state1.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Second state to execute
state2 := luajit.New()
if state2 == nil {
t.Fatal("Failed to create second Lua state")
}
defer state2.Close()
// Load and run the bytecode in the second state
err = state2.LoadAndRunBytecode(bytecode, "test")
if err != nil {
t.Fatalf("LoadAndRunBytecode failed: %v", err)
}
// Verify execution
state2.GetGlobal("result")
if !state2.IsNumber(-1) || state2.ToNumber(-1) != 30 {
t.Fatalf("Expected result to be 30, got %v", state2.ToNumber(-1))
}
state2.Pop(1)
// Call the function to verify it was properly transferred
state2.GetGlobal("add")
if !state2.IsFunction(-1) {
t.Fatal("Expected add to be a function")
}
state2.PushNumber(5)
state2.PushNumber(7)
if err := state2.Call(2, 1); err != nil {
t.Fatalf("Failed to call function: %v", err)
}
if state2.ToNumber(-1) != 12 {
t.Fatalf("Expected add(5, 7) to return 12, got %v", state2.ToNumber(-1))
}
state2.Pop(1)
}
// TestCompilationError tests error handling for compilation errors
func TestCompilationError(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Invalid Lua code that should fail to compile
code := "function without end"
// Try to compile
_, err := state.CompileBytecode(code, "invalid")
if err == nil {
t.Fatal("Expected compilation error, got nil")
}
// Check error type
var luaErr *luajit.LuaError
if !errors.As(err, &luaErr) {
t.Fatalf("Expected error to wrap *luajit.LuaError, got %T", err)
}
}
// TestExecutionError tests error handling for runtime errors
func TestExecutionError(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Code that compiles but fails at runtime
code := "error('deliberate error')"
// Compile bytecode
bytecode, err := state.CompileBytecode(code, "error")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Try to execute
err = state.LoadAndRunBytecode(bytecode, "error")
if err == nil {
t.Fatal("Expected execution error, got nil")
}
// Check error type
if _, ok := err.(*luajit.LuaError); !ok {
t.Fatalf("Expected *luajit.LuaError, got %T", err)
}
}
// TestBytecodeEquivalence tests that bytecode execution produces the same results as direct execution
func TestBytecodeEquivalence(t *testing.T) {
code := `
local result = 0
for i = 1, 10 do
result = result + i
end
return result
`
// First, execute directly
state1 := luajit.New()
if state1 == nil {
t.Fatal("Failed to create first Lua state")
}
defer state1.Close()
directResult, err := state1.ExecuteWithResult(code)
if err != nil {
t.Fatalf("ExecuteWithResult failed: %v", err)
}
// Then, compile and execute bytecode
state2 := luajit.New()
if state2 == nil {
t.Fatal("Failed to create second Lua state")
}
defer state2.Close()
bytecode, err := state2.CompileBytecode(code, "test")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
err = state2.LoadBytecode(bytecode, "test")
if err != nil {
t.Fatalf("LoadBytecode failed: %v", err)
}
err = state2.Call(0, 1)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
bytecodeResult, err := state2.ToValue(-1)
if err != nil {
t.Fatalf("ToValue failed: %v", err)
}
state2.Pop(1)
// Compare results
if directResult != bytecodeResult {
t.Fatalf("Results differ: direct=%v, bytecode=%v", directResult, bytecodeResult)
}
}
// TestBytecodeReuse tests reusing the same bytecode multiple times
func TestBytecodeReuse(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create a function in bytecode
code := `
return function(x)
return x * 2
end
`
bytecode, err := state.CompileBytecode(code, "func")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Execute it several times
for i := 1; i <= 3; i++ {
// Load and run to get the function
err = state.LoadAndRunBytecodeWithResults(bytes.Clone(bytecode), "func", 1)
if err != nil {
t.Fatalf("LoadAndRunBytecodeWithResults failed: %v", err)
}
// Stack now has the function at the top
if !state.IsFunction(-1) {
t.Fatal("Expected function at top of stack")
}
// Call with parameter i
state.PushNumber(float64(i))
if err := state.Call(1, 1); err != nil {
t.Fatalf("Call failed: %v", err)
}
// Check result
expected := float64(i * 2)
if state.ToNumber(-1) != expected {
t.Fatalf("Expected %v, got %v", expected, state.ToNumber(-1))
}
// Pop the result
state.Pop(1)
}
}
// TestBytecodeClosure tests that bytecode properly handles closures and upvalues
func TestBytecodeClosure(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create a closure
code := `
local counter = 0
return function()
counter = counter + 1
return counter
end
`
// Compile to bytecode
bytecode, err := state.CompileBytecode(code, "closure")
if err != nil {
t.Fatalf("CompileBytecode failed: %v", err)
}
// Load and run to get the counter function
err = state.LoadAndRunBytecodeWithResults(bytecode, "closure", 1)
if err != nil {
t.Fatalf("LoadAndRunBytecode failed: %v", err)
}
// Stack now has the function at the top
if !state.IsFunction(-1) {
t.Fatal("Expected function at top of stack")
}
// Store in a global
state.SetGlobal("counter_func")
// Call it multiple times and check the results
for i := 1; i <= 3; i++ {
state.GetGlobal("counter_func")
if err := state.Call(0, 1); err != nil {
t.Fatalf("Call failed: %v", err)
}
if state.ToNumber(-1) != float64(i) {
t.Fatalf("Expected counter to be %d, got %v", i, state.ToNumber(-1))
}
state.Pop(1)
}
}

178
tests/functions_test.go Normal file
View File

@ -0,0 +1,178 @@
package luajit_test
import (
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
func TestPushGoFunction(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Define a simple function that adds two numbers
add := func(s *luajit.State) int {
a := s.ToNumber(1)
b := s.ToNumber(2)
s.PushNumber(a + b)
return 1 // Return one result
}
// Push the function onto the stack
if err := state.PushGoFunction(add); err != nil {
t.Fatalf("PushGoFunction failed: %v", err)
}
// Verify that a function is on the stack
if !state.IsFunction(-1) {
t.Fatalf("Expected function at top of stack")
}
// Push arguments
state.PushNumber(3)
state.PushNumber(4)
// Call the function
if err := state.Call(2, 1); err != nil {
t.Fatalf("Failed to call function: %v", err)
}
// Check the result
if state.ToNumber(-1) != 7 {
t.Fatalf("Function returned %f, expected 7", state.ToNumber(-1))
}
state.Pop(1)
}
func TestRegisterGoFunction(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Define a function that squares a number
square := func(s *luajit.State) int {
x := s.ToNumber(1)
s.PushNumber(x * x)
return 1
}
// Register the function
if err := state.RegisterGoFunction("square", square); err != nil {
t.Fatalf("RegisterGoFunction failed: %v", err)
}
// Call the function from Lua
if err := state.DoString("result = square(5)"); err != nil {
t.Fatalf("Failed to call registered function: %v", err)
}
// Check the result
state.GetGlobal("result")
if state.ToNumber(-1) != 25 {
t.Fatalf("Function returned %f, expected 25", state.ToNumber(-1))
}
state.Pop(1)
// Test UnregisterGoFunction
state.UnregisterGoFunction("square")
// Function should no longer exist
err := state.DoString("result = square(5)")
if err == nil {
t.Fatalf("Expected error after unregistering function, got nil")
}
}
func TestGoFunctionWithErrorHandling(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Function that returns an error in Lua
errFunc := func(s *luajit.State) int {
s.PushString("error from Go function")
return -1 // Signal error
}
// Register the function
if err := state.RegisterGoFunction("errorFunc", errFunc); err != nil {
t.Fatalf("RegisterGoFunction failed: %v", err)
}
// Call the function expecting an error
err := state.DoString("result = errorFunc()")
if err == nil {
t.Fatalf("Expected error from function, got nil")
}
// Error message should contain our message
luaErr, ok := err.(*luajit.LuaError)
if !ok {
t.Fatalf("Expected LuaError, got %T: %v", err, err)
}
if luaErr.Message == "" {
t.Fatalf("Expected non-empty error message from Go function")
}
}
func TestCleanup(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
// Register several functions
for i := 0; i < 5; i++ {
dummy := func(s *luajit.State) int { return 0 }
if err := state.RegisterGoFunction("dummy", dummy); err != nil {
t.Fatalf("RegisterGoFunction failed: %v", err)
}
}
// Call Cleanup explicitly
state.Cleanup()
// Make sure we can still close the state
state.Close()
// Also test that Close can be called after Cleanup
state = luajit.New()
if state == nil {
t.Fatal("Failed to create second Lua state")
}
state.Close() // Should call Cleanup internally
}
func TestGoFunctionErrorPointer(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create a Lua function that calls a non-existent Go function pointer
// This isn't a direct test of internal implementation, but tries to cover
// error cases in the goFunctionWrapper
code := `
function test()
-- This is a stub that doesn't actually call the wrapper,
-- but we're testing error handling in our State.DoString
return "test"
end
`
if err := state.DoString(code); err != nil {
t.Fatalf("Failed to define test function: %v", err)
}
// The real test is that Cleanup doesn't crash
state.Cleanup()
}

53
tests/stack_test.go Normal file
View File

@ -0,0 +1,53 @@
package luajit_test
import (
"strings"
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
func TestLuaError(t *testing.T) {
err := &luajit.LuaError{
Code: 123,
Message: "test error",
}
expected := "test error (code=123)"
if err.Error() != expected {
t.Errorf("Expected error message %q, got %q", expected, err.Error())
}
}
func TestGetStackTrace(t *testing.T) {
s := luajit.New()
defer s.Close()
// Test with debug library available
trace := s.GetStackTrace()
if !strings.Contains(trace, "stack traceback:") {
t.Errorf("Expected trace to contain 'stack traceback:', got %q", trace)
}
// Test when debug table is not available
err := s.DoString("debug = nil")
if err != nil {
t.Fatalf("Failed to set debug to nil: %v", err)
}
trace = s.GetStackTrace()
if trace != "debug table not available" {
t.Errorf("Expected 'debug table not available', got %q", trace)
}
// Test when debug.traceback is not available
err = s.DoString("debug = {}")
if err != nil {
t.Fatalf("Failed to set debug to empty table: %v", err)
}
trace = s.GetStackTrace()
if trace != "debug.traceback not available" {
t.Errorf("Expected 'debug.traceback not available', got %q", trace)
}
}

272
tests/table_test.go Normal file
View File

@ -0,0 +1,272 @@
package luajit_test
import (
"reflect"
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
func TestGetTableLength(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create a table with numeric indices
if err := state.DoString("t = {10, 20, 30, 40, 50}"); err != nil {
t.Fatalf("Failed to create test table: %v", err)
}
state.GetGlobal("t")
length := state.GetTableLength(-1)
if length != 5 {
t.Fatalf("Expected length 5, got %d", length)
}
state.Pop(1)
// Create a table with string keys
if err := state.DoString("t2 = {a=1, b=2, c=3}"); err != nil {
t.Fatalf("Failed to create test table: %v", err)
}
state.GetGlobal("t2")
length = state.GetTableLength(-1)
if length != 0 {
t.Fatalf("Expected length 0 for string-keyed table, got %d", length)
}
state.Pop(1)
}
func TestPushTypedArrays(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test []int
intArr := []int{1, 2, 3, 4, 5}
if err := state.PushValue(intArr); err != nil {
t.Fatalf("Failed to push int array: %v", err)
}
state.SetGlobal("int_arr")
// Test []string
stringArr := []string{"hello", "world", "test"}
if err := state.PushValue(stringArr); err != nil {
t.Fatalf("Failed to push string array: %v", err)
}
state.SetGlobal("string_arr")
// Test []bool
boolArr := []bool{true, false, true}
if err := state.PushValue(boolArr); err != nil {
t.Fatalf("Failed to push bool array: %v", err)
}
state.SetGlobal("bool_arr")
// Test []float64
floatArr := []float64{1.1, 2.2, 3.3}
if err := state.PushValue(floatArr); err != nil {
t.Fatalf("Failed to push float array: %v", err)
}
state.SetGlobal("float_arr")
// Verify arrays in Lua
if err := state.DoString(`
assert(int_arr[1] == 1 and int_arr[5] == 5)
assert(string_arr[1] == "hello" and string_arr[3] == "test")
assert(bool_arr[1] == true and bool_arr[2] == false)
assert(math.abs(float_arr[1] - 1.1) < 0.0001)
`); err != nil {
t.Fatalf("Array verification failed: %v", err)
}
}
func TestPushTypedMaps(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test map[string]string
stringMap := map[string]string{"name": "John", "city": "NYC"}
if err := state.PushValue(stringMap); err != nil {
t.Fatalf("Failed to push string map: %v", err)
}
state.SetGlobal("string_map")
// Test map[string]int
intMap := map[string]int{"age": 25, "score": 100}
if err := state.PushValue(intMap); err != nil {
t.Fatalf("Failed to push int map: %v", err)
}
state.SetGlobal("int_map")
// Test map[int]any
intKeyMap := map[int]any{1: "first", 2: 42, 3: true}
if err := state.PushValue(intKeyMap); err != nil {
t.Fatalf("Failed to push int key map: %v", err)
}
state.SetGlobal("int_key_map")
// Verify maps in Lua
if err := state.DoString(`
assert(string_map.name == "John" and string_map.city == "NYC")
assert(int_map.age == 25 and int_map.score == 100)
assert(int_key_map[1] == "first" and int_key_map[2] == 42 and int_key_map[3] == true)
`); err != nil {
t.Fatalf("Map verification failed: %v", err)
}
}
func TestToTableTypedArrays(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test integer array detection
if err := state.DoString("int_arr = {10, 20, 30}"); err != nil {
t.Fatalf("Failed to create int array: %v", err)
}
state.GetGlobal("int_arr")
result, err := state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert int array: %v", err)
}
intArr, ok := result.([]int)
if !ok {
t.Fatalf("Expected []int, got %T", result)
}
expected := []int{10, 20, 30}
if !reflect.DeepEqual(intArr, expected) {
t.Fatalf("Expected %v, got %v", expected, intArr)
}
state.Pop(1)
// Test float array detection
if err := state.DoString("float_arr = {1.5, 2.7, 3.9}"); err != nil {
t.Fatalf("Failed to create float array: %v", err)
}
state.GetGlobal("float_arr")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert float array: %v", err)
}
floatArr, ok := result.([]float64)
if !ok {
t.Fatalf("Expected []float64, got %T", result)
}
expectedFloat := []float64{1.5, 2.7, 3.9}
if !reflect.DeepEqual(floatArr, expectedFloat) {
t.Fatalf("Expected %v, got %v", expectedFloat, floatArr)
}
state.Pop(1)
// Test string array detection
if err := state.DoString(`string_arr = {"hello", "world"}`); err != nil {
t.Fatalf("Failed to create string array: %v", err)
}
state.GetGlobal("string_arr")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert string array: %v", err)
}
stringArr, ok := result.([]string)
if !ok {
t.Fatalf("Expected []string, got %T", result)
}
expectedString := []string{"hello", "world"}
if !reflect.DeepEqual(stringArr, expectedString) {
t.Fatalf("Expected %v, got %v", expectedString, stringArr)
}
state.Pop(1)
// Test bool array detection
if err := state.DoString("bool_arr = {true, false, true}"); err != nil {
t.Fatalf("Failed to create bool array: %v", err)
}
state.GetGlobal("bool_arr")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert bool array: %v", err)
}
boolArr, ok := result.([]bool)
if !ok {
t.Fatalf("Expected []bool, got %T", result)
}
expectedBool := []bool{true, false, true}
if !reflect.DeepEqual(boolArr, expectedBool) {
t.Fatalf("Expected %v, got %v", expectedBool, boolArr)
}
state.Pop(1)
}
func TestToTableTypedMaps(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test string map detection
if err := state.DoString(`string_map = {name="John", city="NYC"}`); err != nil {
t.Fatalf("Failed to create string map: %v", err)
}
state.GetGlobal("string_map")
result, err := state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert string map: %v", err)
}
stringMap, ok := result.(map[string]string)
if !ok {
t.Fatalf("Expected map[string]string, got %T", result)
}
expectedStringMap := map[string]string{"name": "John", "city": "NYC"}
if !reflect.DeepEqual(stringMap, expectedStringMap) {
t.Fatalf("Expected %v, got %v", expectedStringMap, stringMap)
}
state.Pop(1)
// Test int map detection
if err := state.DoString("int_map = {age=25, score=100}"); err != nil {
t.Fatalf("Failed to create int map: %v", err)
}
state.GetGlobal("int_map")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert int map: %v", err)
}
intMap, ok := result.(map[string]int)
if !ok {
t.Fatalf("Expected map[string]int, got %T", result)
}
expectedIntMap := map[string]int{"age": 25, "score": 100}
if !reflect.DeepEqual(intMap, expectedIntMap) {
t.Fatalf("Expected %v, got %v", expectedIntMap, intMap)
}
state.Pop(1)
// Test mixed map (should fallback to map[string]any)
if err := state.DoString(`mixed_map = {name="John", age=25, active=true}`); err != nil {
t.Fatalf("Failed to create mixed map: %v", err)
}
state.GetGlobal("mixed_map")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Failed to convert mixed map: %v", err)
}
mixedMap, ok := result.(map[string]any)
if !ok {
t.Fatalf("Expected map[string]any, got %T", result)
}
if mixedMap["name"] != "John" || mixedMap["age"] != 25 || mixedMap["active"] != true {
t.Fatalf("Mixed map conversion failed: %v", mixedMap)
}
state.Pop(1)
}

425
tests/wrapper_test.go Normal file
View File

@ -0,0 +1,425 @@
package luajit_test
import (
"os"
"reflect"
"testing"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
func TestStateLifecycle(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
state.Close()
state.Close() // Test idempotent close
}
func TestStackOperations(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test stack manipulation
if state.GetTop() != 0 {
t.Fatalf("Expected empty stack, got %d", state.GetTop())
}
state.PushNil()
state.PushBoolean(true)
state.PushNumber(42)
state.PushString("hello")
if state.GetTop() != 4 {
t.Fatalf("Expected 4 elements, got %d", state.GetTop())
}
state.SetTop(2)
if state.GetTop() != 2 {
t.Fatalf("Expected 2 elements after SetTop, got %d", state.GetTop())
}
state.PushCopy(2)
if !state.IsBoolean(-1) {
t.Fatal("Expected boolean at top")
}
state.Pop(1)
state.PushNumber(99)
state.Remove(1)
if !state.IsBoolean(1) {
t.Fatal("Expected boolean at index 1 after Remove")
}
}
func TestTypeChecking(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
values := []struct {
push func()
luaType luajit.LuaType
checkFn func(int) bool
}{
{state.PushNil, luajit.TypeNil, state.IsNil},
{func() { state.PushBoolean(true) }, luajit.TypeBoolean, state.IsBoolean},
{func() { state.PushNumber(42) }, luajit.TypeNumber, state.IsNumber},
{func() { state.PushString("test") }, luajit.TypeString, state.IsString},
{state.NewTable, luajit.TypeTable, state.IsTable},
}
for i, v := range values {
v.push()
idx := i + 1
if state.GetType(idx) != v.luaType {
t.Fatalf("Type mismatch at %d: expected %s, got %s", idx, v.luaType, state.GetType(idx))
}
if !v.checkFn(idx) {
t.Fatalf("Type check failed at %d", idx)
}
}
state.DoString("function test() return true end")
state.GetGlobal("test")
if !state.IsFunction(-1) {
t.Fatal("IsFunction failed")
}
}
func TestValueConversion(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
state.PushBoolean(true)
state.PushNumber(42.5)
state.PushString("hello")
if !state.ToBoolean(1) {
t.Fatal("ToBoolean failed")
}
if state.ToNumber(2) != 42.5 {
t.Fatalf("ToNumber failed: expected 42.5, got %f", state.ToNumber(2))
}
if state.ToString(3) != "hello" {
t.Fatalf("ToString failed: expected 'hello', got '%s'", state.ToString(3))
}
}
func TestTableOperations(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
state.CreateTable(0, 3)
// Set fields
state.PushNumber(42)
state.SetField(-2, "answer")
state.PushString("hello")
state.SetField(-2, "greeting")
state.PushBoolean(true)
state.SetField(-2, "flag")
// Get fields
state.GetField(-1, "answer")
if state.ToNumber(-1) != 42 {
t.Fatal("GetField failed for 'answer'")
}
state.Pop(1)
// Test iteration
state.PushNil()
count := 0
for state.Next(-2) {
count++
state.Pop(1)
}
if count != 3 {
t.Fatalf("Expected 3 entries, found %d", count)
}
state.Pop(1)
}
func TestGlobalOperations(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
state.PushNumber(42)
state.SetGlobal("answer")
state.GetGlobal("answer")
if state.ToNumber(-1) != 42 {
t.Fatalf("GetGlobal failed: expected 42, got %f", state.ToNumber(-1))
}
state.Pop(1)
state.GetGlobal("nonexistent")
if !state.IsNil(-1) {
t.Fatal("Expected nil for non-existent global")
}
state.Pop(1)
}
func TestCodeExecution(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test LoadString and Call
if err := state.LoadString("return 42"); err != nil {
t.Fatalf("LoadString failed: %v", err)
}
if err := state.Call(0, 1); err != nil {
t.Fatalf("Call failed: %v", err)
}
if state.ToNumber(-1) != 42 {
t.Fatalf("Call result incorrect: expected 42, got %f", state.ToNumber(-1))
}
state.Pop(1)
// Test DoString
if err := state.DoString("answer = 42 + 1"); err != nil {
t.Fatalf("DoString failed: %v", err)
}
state.GetGlobal("answer")
if state.ToNumber(-1) != 43 {
t.Fatalf("DoString result incorrect: expected 43, got %f", state.ToNumber(-1))
}
state.Pop(1)
// Test Execute
nresults, err := state.Execute("return 5, 10, 15")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if nresults != 3 {
t.Fatalf("Execute returned %d results, expected 3", nresults)
}
if state.ToNumber(-3) != 5 || state.ToNumber(-2) != 10 || state.ToNumber(-1) != 15 {
t.Fatal("Execute results incorrect")
}
state.Pop(3)
// Test ExecuteWithResult
result, err := state.ExecuteWithResult("return 'hello'")
if err != nil {
t.Fatalf("ExecuteWithResult failed: %v", err)
}
if result != "hello" {
t.Fatalf("ExecuteWithResult returned %v, expected 'hello'", result)
}
// Test error handling
if err := state.DoString("invalid lua code"); err == nil {
t.Fatal("Expected error for invalid code")
}
}
func TestFileOperations(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Create temp file
content := []byte("answer = 42")
tmpfile, err := os.CreateTemp("", "test-*.lua")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(content); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
tmpfile.Close()
// Test DoFile
if err := state.DoFile(tmpfile.Name()); err != nil {
t.Fatalf("DoFile failed: %v", err)
}
state.GetGlobal("answer")
if state.ToNumber(-1) != 42 {
t.Fatalf("DoFile result incorrect: expected 42, got %f", state.ToNumber(-1))
}
state.Pop(1)
}
func TestPackagePath(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
testPath := "/test/path/?.lua"
if err := state.SetPackagePath(testPath); err != nil {
t.Fatalf("SetPackagePath failed: %v", err)
}
result, err := state.ExecuteWithResult("return package.path")
if err != nil {
t.Fatalf("Failed to get package.path: %v", err)
}
if result != testPath {
t.Fatalf("SetPackagePath failed: expected '%s', got '%s'", testPath, result)
}
addPath := "/another/path/?.lua"
if err := state.AddPackagePath(addPath); err != nil {
t.Fatalf("AddPackagePath failed: %v", err)
}
result, err = state.ExecuteWithResult("return package.path")
if err != nil {
t.Fatalf("Failed to get package.path: %v", err)
}
expected := testPath + ";" + addPath
if result != expected {
t.Fatalf("AddPackagePath failed: expected '%s', got '%s'", expected, result)
}
}
func TestEnhancedTypes(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test typed arrays
testCases := []struct {
input any
expected any
}{
// Primitive types
{nil, nil},
{true, true},
{42, 42}, // Should preserve as int
{42.5, 42.5}, // Should be float64
{"hello", "hello"},
// Typed arrays
{[]int{1, 2, 3}, []int{1, 2, 3}},
{[]string{"a", "b"}, []string{"a", "b"}},
{[]bool{true, false}, []bool{true, false}},
{[]float64{1.1, 2.2}, []float64{1.1, 2.2}},
// Typed maps
{map[string]string{"name": "John"}, map[string]string{"name": "John"}},
{map[string]int{"age": 25}, map[string]int{"age": 25}},
{map[int]any{10: "first", 20: 42}, map[string]any{"10": "first", "20": 42}},
}
for i, tc := range testCases {
// Push and retrieve value
if err := state.PushValue(tc.input); err != nil {
t.Fatalf("Case %d: PushValue failed: %v", i, err)
}
result, err := state.ToValue(-1)
if err != nil {
t.Fatalf("Case %d: ToValue failed: %v", i, err)
}
if !reflect.DeepEqual(result, tc.expected) {
t.Fatalf("Case %d: expected %v (%T), got %v (%T)",
i, tc.expected, tc.expected, result, result)
}
state.Pop(1)
}
// Test mixed array (should become []any)
state.DoString("mixed = {1, 'hello', true}")
state.GetGlobal("mixed")
result, err := state.ToValue(-1)
if err != nil {
t.Fatalf("Mixed array conversion failed: %v", err)
}
if _, ok := result.([]any); !ok {
t.Fatalf("Expected []any for mixed array, got %T", result)
}
state.Pop(1)
// Test mixed map (should become map[string]any)
state.DoString("mixedMap = {name='John', age=25, active=true}")
state.GetGlobal("mixedMap")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Mixed map conversion failed: %v", err)
}
if _, ok := result.(map[string]any); !ok {
t.Fatalf("Expected map[string]any for mixed map, got %T", result)
}
state.Pop(1)
}
func TestIntegerPreservation(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test that integers are preserved
state.DoString("num = 42")
state.GetGlobal("num")
result, err := state.ToValue(-1)
if err != nil {
t.Fatalf("Integer conversion failed: %v", err)
}
if val, ok := result.(int); !ok || val != 42 {
t.Fatalf("Expected int 42, got %T %v", result, result)
}
state.Pop(1)
// Test that floats remain floats
state.DoString("fnum = 42.5")
state.GetGlobal("fnum")
result, err = state.ToValue(-1)
if err != nil {
t.Fatalf("Float conversion failed: %v", err)
}
if val, ok := result.(float64); !ok || val != 42.5 {
t.Fatalf("Expected float64 42.5, got %T %v", result, result)
}
state.Pop(1)
}
func TestErrorHandling(t *testing.T) {
state := luajit.New()
if state == nil {
t.Fatal("Failed to create Lua state")
}
defer state.Close()
// Test unsupported type
type customStruct struct{ Field int }
if err := state.PushValue(customStruct{Field: 42}); err == nil {
t.Fatal("Expected error for unsupported type")
}
// Test invalid stack index
_, err := state.ToValue(100)
if err == nil {
t.Fatal("Expected error for invalid index")
}
}

319
types.go
View File

@ -4,12 +4,15 @@ package luajit
#include <lua.h>
*/
import "C"
import (
"fmt"
"strconv"
)
// LuaType represents Lua value types
type LuaType int
const (
// These constants must match lua.h's LUA_T* values
TypeNone LuaType = -1
TypeNil LuaType = 0
TypeBoolean LuaType = 1
@ -22,7 +25,6 @@ const (
TypeThread LuaType = 8
)
// String returns the string representation of the Lua type
func (t LuaType) String() string {
switch t {
case TypeNone:
@ -49,3 +51,316 @@ func (t LuaType) String() string {
return "unknown"
}
}
// ConvertValue converts a value to the requested type with comprehensive type conversion
func ConvertValue[T any](value any) (T, bool) {
var zero T
if value == nil {
return zero, false
}
if result, ok := value.(T); ok {
return result, true
}
switch any(zero).(type) {
case string:
return convertToString[T](value)
case int:
return convertToInt[T](value)
case float64:
return convertToFloat[T](value)
case bool:
return convertToBool[T](value)
case []int:
return convertToIntSlice[T](value)
case []string:
return convertToStringSlice[T](value)
case []bool:
return convertToBoolSlice[T](value)
case []float64:
return convertToFloatSlice[T](value)
case []any:
return convertToAnySlice[T](value)
case map[string]string:
return convertToStringMap[T](value)
case map[string]int:
return convertToIntMap[T](value)
case map[int]any:
return convertToIntKeyMap[T](value)
case map[string]any:
return convertToAnyMap[T](value)
}
return zero, false
}
func convertToString[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case float64:
if v == float64(int(v)) {
return any(strconv.Itoa(int(v))).(T), true
}
return any(fmt.Sprintf("%g", v)).(T), true
case int:
return any(strconv.Itoa(v)).(T), true
case bool:
return any(strconv.FormatBool(v)).(T), true
}
return zero, false
}
func convertToInt[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case float64:
return any(int(v)).(T), true
case string:
if i, err := strconv.Atoi(v); err == nil {
return any(i).(T), true
}
case bool:
if v {
return any(1).(T), true
}
return any(0).(T), true
}
return zero, false
}
func convertToFloat[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case int:
return any(float64(v)).(T), true
case string:
if f, err := strconv.ParseFloat(v, 64); err == nil {
return any(f).(T), true
}
case bool:
if v {
return any(1.0).(T), true
}
return any(0.0).(T), true
}
return zero, false
}
func convertToBool[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case string:
switch v {
case "true", "yes", "1":
return any(true).(T), true
case "false", "no", "0":
return any(false).(T), true
}
case int:
return any(v != 0).(T), true
case float64:
return any(v != 0).(T), true
}
return zero, false
}
func convertToIntSlice[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case []float64:
result := make([]int, len(v))
for i, f := range v {
result[i] = int(f)
}
return any(result).(T), true
case []any:
result := make([]int, 0, len(v))
for _, item := range v {
if i, ok := ConvertValue[int](item); ok {
result = append(result, i)
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToStringSlice[T any](value any) (T, bool) {
var zero T
if v, ok := value.([]any); ok {
result := make([]string, 0, len(v))
for _, item := range v {
if s, ok := ConvertValue[string](item); ok {
result = append(result, s)
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToBoolSlice[T any](value any) (T, bool) {
var zero T
if v, ok := value.([]any); ok {
result := make([]bool, 0, len(v))
for _, item := range v {
if b, ok := ConvertValue[bool](item); ok {
result = append(result, b)
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToFloatSlice[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case []int:
result := make([]float64, len(v))
for i, n := range v {
result[i] = float64(n)
}
return any(result).(T), true
case []any:
result := make([]float64, 0, len(v))
for _, item := range v {
if f, ok := ConvertValue[float64](item); ok {
result = append(result, f)
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToAnySlice[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case []int:
result := make([]any, len(v))
for i, n := range v {
result[i] = n
}
return any(result).(T), true
case []string:
result := make([]any, len(v))
for i, s := range v {
result[i] = s
}
return any(result).(T), true
case []bool:
result := make([]any, len(v))
for i, b := range v {
result[i] = b
}
return any(result).(T), true
case []float64:
result := make([]any, len(v))
for i, f := range v {
result[i] = f
}
return any(result).(T), true
}
return zero, false
}
func convertToStringMap[T any](value any) (T, bool) {
var zero T
if v, ok := value.(map[string]any); ok {
result := make(map[string]string, len(v))
for k, val := range v {
if s, ok := ConvertValue[string](val); ok {
result[k] = s
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToIntMap[T any](value any) (T, bool) {
var zero T
if v, ok := value.(map[string]any); ok {
result := make(map[string]int, len(v))
for k, val := range v {
if i, ok := ConvertValue[int](val); ok {
result[k] = i
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToIntKeyMap[T any](value any) (T, bool) {
var zero T
if v, ok := value.(map[string]any); ok {
result := make(map[int]any, len(v))
for k, val := range v {
if i, err := strconv.Atoi(k); err == nil {
result[i] = val
} else {
return zero, false
}
}
return any(result).(T), true
}
return zero, false
}
func convertToAnyMap[T any](value any) (T, bool) {
var zero T
switch v := value.(type) {
case map[string]string:
result := make(map[string]any, len(v))
for k, s := range v {
result[k] = s
}
return any(result).(T), true
case map[string]int:
result := make(map[string]any, len(v))
for k, i := range v {
result[k] = i
}
return any(result).(T), true
case map[int]any:
result := make(map[string]any, len(v))
for k, val := range v {
result[strconv.Itoa(k)] = val
}
return any(result).(T), true
}
return zero, false
}
// GetTypedValue gets a value from the state with type conversion
func GetTypedValue[T any](s *State, index int) (T, bool) {
value, err := s.ToValue(index)
if err != nil {
var zero T
return zero, false
}
return ConvertValue[T](value)
}
// GetGlobalTyped gets a global variable with type conversion
func GetGlobalTyped[T any](s *State, name string) (T, bool) {
s.GetGlobal(name)
defer s.Pop(1)
return GetTypedValue[T](s, -1)
}

59
validation.go Normal file
View File

@ -0,0 +1,59 @@
package luajit
import "fmt"
// ArgSpec defines an argument specification for validation
type ArgSpec struct {
Name string
Type string
Required bool
Check func(*State, int) bool
}
// Common argument checkers
var (
CheckString = func(s *State, i int) bool { return s.IsString(i) }
CheckNumber = func(s *State, i int) bool { return s.IsNumber(i) }
CheckBool = func(s *State, i int) bool { return s.IsBoolean(i) }
CheckTable = func(s *State, i int) bool { return s.IsTable(i) }
CheckFunc = func(s *State, i int) bool { return s.IsFunction(i) }
CheckAny = func(s *State, i int) bool { return true }
)
// CheckArgs validates function arguments against specifications
func (s *State) CheckArgs(specs ...ArgSpec) error {
for i, spec := range specs {
argIdx := i + 1
if argIdx > s.GetTop() {
if spec.Required {
return fmt.Errorf("missing argument %d: %s", argIdx, spec.Name)
}
break
}
if s.IsNil(argIdx) && !spec.Required {
continue
}
if !spec.Check(s, argIdx) {
return fmt.Errorf("argument %d (%s) must be %s", argIdx, spec.Name, spec.Type)
}
}
return nil
}
// CheckMinArgs checks for minimum number of arguments
func (s *State) CheckMinArgs(min int) error {
if s.GetTop() < min {
return fmt.Errorf("expected at least %d arguments, got %d", min, s.GetTop())
}
return nil
}
// CheckExactArgs checks for exact number of arguments
func (s *State) CheckExactArgs(count int) error {
if s.GetTop() != count {
return fmt.Errorf("expected exactly %d arguments, got %d", count, s.GetTop())
}
return nil
}

View File

@ -1,161 +0,0 @@
/*
** $Id: lauxlib.h,v 1.88.1.1 2007/12/27 13:02:25 roberto Exp $
** Auxiliary functions for building Lua libraries
** See Copyright Notice in lua.h
*/
#ifndef lauxlib_h
#define lauxlib_h
#include <stddef.h>
#include <stdio.h>
#include "lua.h"
/* extra error code for `luaL_load' */
#define LUA_ERRFILE (LUA_ERRERR+1)
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg;
LUALIB_API void (luaL_openlib) (lua_State *L, const char *libname,
const luaL_Reg *l, int nup);
LUALIB_API void (luaL_register) (lua_State *L, const char *libname,
const luaL_Reg *l);
LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e);
LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e);
LUALIB_API int (luaL_typerror) (lua_State *L, int narg, const char *tname);
LUALIB_API int (luaL_argerror) (lua_State *L, int numarg, const char *extramsg);
LUALIB_API const char *(luaL_checklstring) (lua_State *L, int numArg,
size_t *l);
LUALIB_API const char *(luaL_optlstring) (lua_State *L, int numArg,
const char *def, size_t *l);
LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int numArg);
LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int nArg, lua_Number def);
LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int numArg);
LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int nArg,
lua_Integer def);
LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg);
LUALIB_API void (luaL_checktype) (lua_State *L, int narg, int t);
LUALIB_API void (luaL_checkany) (lua_State *L, int narg);
LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname);
LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname);
LUALIB_API void (luaL_where) (lua_State *L, int lvl);
LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...);
LUALIB_API int (luaL_checkoption) (lua_State *L, int narg, const char *def,
const char *const lst[]);
/* pre-defined references */
#define LUA_NOREF (-2)
#define LUA_REFNIL (-1)
LUALIB_API int (luaL_ref) (lua_State *L, int t);
LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref);
LUALIB_API int (luaL_loadfile) (lua_State *L, const char *filename);
LUALIB_API int (luaL_loadbuffer) (lua_State *L, const char *buff, size_t sz,
const char *name);
LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s);
LUALIB_API lua_State *(luaL_newstate) (void);
LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s, const char *p,
const char *r);
LUALIB_API const char *(luaL_findtable) (lua_State *L, int idx,
const char *fname, int szhint);
/* From Lua 5.2. */
LUALIB_API int luaL_fileresult(lua_State *L, int stat, const char *fname);
LUALIB_API int luaL_execresult(lua_State *L, int stat);
LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename,
const char *mode);
LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz,
const char *name, const char *mode);
LUALIB_API void luaL_traceback (lua_State *L, lua_State *L1, const char *msg,
int level);
LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);
LUALIB_API void (luaL_pushmodule) (lua_State *L, const char *modname,
int sizehint);
LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname);
LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname);
/*
** ===============================================================
** some useful macros
** ===============================================================
*/
#define luaL_argcheck(L, cond,numarg,extramsg) \
((void)((cond) || luaL_argerror(L, (numarg), (extramsg))))
#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL))
#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL))
#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n)))
#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d)))
#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n)))
#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d)))
#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i)))
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
#define luaL_dostring(L, s) \
(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))
#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))
#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n)))
/* From Lua 5.2. */
#define luaL_newlibtable(L, l) \
lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)
#define luaL_newlib(L, l) (luaL_newlibtable(L, l), luaL_setfuncs(L, l, 0))
/*
** {======================================================
** Generic Buffer manipulation
** =======================================================
*/
typedef struct luaL_Buffer {
char *p; /* current position in buffer */
int lvl; /* number of strings in the stack (level) */
lua_State *L;
char buffer[LUAL_BUFFERSIZE];
} luaL_Buffer;
#define luaL_addchar(B,c) \
((void)((B)->p < ((B)->buffer+LUAL_BUFFERSIZE) || luaL_prepbuffer(B)), \
(*(B)->p++ = (char)(c)))
/* compatibility only */
#define luaL_putchar(B,c) luaL_addchar(B,c)
#define luaL_addsize(B,n) ((B)->p += (n))
LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B);
LUALIB_API char *(luaL_prepbuffer) (luaL_Buffer *B);
LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);
LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s);
LUALIB_API void (luaL_addvalue) (luaL_Buffer *B);
LUALIB_API void (luaL_pushresult) (luaL_Buffer *B);
/* }====================================================== */
#endif

View File

@ -1,402 +0,0 @@
/*
** $Id: lua.h,v 1.218.1.5 2008/08/06 13:30:12 roberto Exp $
** Lua - An Extensible Extension Language
** Lua.org, PUC-Rio, Brazil (https://www.lua.org)
** See Copyright Notice at the end of this file
*/
#ifndef lua_h
#define lua_h
#include <stdarg.h>
#include <stddef.h>
#include "luaconf.h"
#define LUA_VERSION "Lua 5.1"
#define LUA_RELEASE "Lua 5.1.4"
#define LUA_VERSION_NUM 501
#define LUA_COPYRIGHT "Copyright (C) 1994-2008 Lua.org, PUC-Rio"
#define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo & W. Celes"
/* mark for precompiled code (`<esc>Lua') */
#define LUA_SIGNATURE "\033Lua"
/* option for multiple returns in `lua_pcall' and `lua_call' */
#define LUA_MULTRET (-1)
/*
** pseudo-indices
*/
#define LUA_REGISTRYINDEX (-10000)
#define LUA_ENVIRONINDEX (-10001)
#define LUA_GLOBALSINDEX (-10002)
#define lua_upvalueindex(i) (LUA_GLOBALSINDEX-(i))
/* thread status */
#define LUA_OK 0
#define LUA_YIELD 1
#define LUA_ERRRUN 2
#define LUA_ERRSYNTAX 3
#define LUA_ERRMEM 4
#define LUA_ERRERR 5
typedef struct lua_State lua_State;
typedef int (*lua_CFunction) (lua_State *L);
/*
** functions that read/write blocks when loading/dumping Lua chunks
*/
typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz);
typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud);
/*
** prototype for memory-allocation functions
*/
typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);
/*
** basic types
*/
#define LUA_TNONE (-1)
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8
/* minimum Lua stack available to a C function */
#define LUA_MINSTACK 20
/*
** generic extra include file
*/
#if defined(LUA_USER_H)
#include LUA_USER_H
#endif
/* type of numbers in Lua */
typedef LUA_NUMBER lua_Number;
/* type for integer functions */
typedef LUA_INTEGER lua_Integer;
/*
** state manipulation
*/
LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud);
LUA_API void (lua_close) (lua_State *L);
LUA_API lua_State *(lua_newthread) (lua_State *L);
LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf);
/*
** basic stack manipulation
*/
LUA_API int (lua_gettop) (lua_State *L);
LUA_API void (lua_settop) (lua_State *L, int idx);
LUA_API void (lua_pushvalue) (lua_State *L, int idx);
LUA_API void (lua_remove) (lua_State *L, int idx);
LUA_API void (lua_insert) (lua_State *L, int idx);
LUA_API void (lua_replace) (lua_State *L, int idx);
LUA_API int (lua_checkstack) (lua_State *L, int sz);
LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n);
/*
** access functions (stack -> C)
*/
LUA_API int (lua_isnumber) (lua_State *L, int idx);
LUA_API int (lua_isstring) (lua_State *L, int idx);
LUA_API int (lua_iscfunction) (lua_State *L, int idx);
LUA_API int (lua_isuserdata) (lua_State *L, int idx);
LUA_API int (lua_type) (lua_State *L, int idx);
LUA_API const char *(lua_typename) (lua_State *L, int tp);
LUA_API int (lua_equal) (lua_State *L, int idx1, int idx2);
LUA_API int (lua_rawequal) (lua_State *L, int idx1, int idx2);
LUA_API int (lua_lessthan) (lua_State *L, int idx1, int idx2);
LUA_API lua_Number (lua_tonumber) (lua_State *L, int idx);
LUA_API lua_Integer (lua_tointeger) (lua_State *L, int idx);
LUA_API int (lua_toboolean) (lua_State *L, int idx);
LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len);
LUA_API size_t (lua_objlen) (lua_State *L, int idx);
LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx);
LUA_API void *(lua_touserdata) (lua_State *L, int idx);
LUA_API lua_State *(lua_tothread) (lua_State *L, int idx);
LUA_API const void *(lua_topointer) (lua_State *L, int idx);
/*
** push functions (C -> stack)
*/
LUA_API void (lua_pushnil) (lua_State *L);
LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n);
LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n);
LUA_API void (lua_pushlstring) (lua_State *L, const char *s, size_t l);
LUA_API void (lua_pushstring) (lua_State *L, const char *s);
LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt,
va_list argp);
LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...);
LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
LUA_API void (lua_pushboolean) (lua_State *L, int b);
LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p);
LUA_API int (lua_pushthread) (lua_State *L);
/*
** get functions (Lua -> stack)
*/
LUA_API void (lua_gettable) (lua_State *L, int idx);
LUA_API void (lua_getfield) (lua_State *L, int idx, const char *k);
LUA_API void (lua_rawget) (lua_State *L, int idx);
LUA_API void (lua_rawgeti) (lua_State *L, int idx, int n);
LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec);
LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz);
LUA_API int (lua_getmetatable) (lua_State *L, int objindex);
LUA_API void (lua_getfenv) (lua_State *L, int idx);
/*
** set functions (stack -> Lua)
*/
LUA_API void (lua_settable) (lua_State *L, int idx);
LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k);
LUA_API void (lua_rawset) (lua_State *L, int idx);
LUA_API void (lua_rawseti) (lua_State *L, int idx, int n);
LUA_API int (lua_setmetatable) (lua_State *L, int objindex);
LUA_API int (lua_setfenv) (lua_State *L, int idx);
/*
** `load' and `call' functions (load and run Lua code)
*/
LUA_API void (lua_call) (lua_State *L, int nargs, int nresults);
LUA_API int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc);
LUA_API int (lua_cpcall) (lua_State *L, lua_CFunction func, void *ud);
LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt,
const char *chunkname);
LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data);
/*
** coroutine functions
*/
LUA_API int (lua_yield) (lua_State *L, int nresults);
LUA_API int (lua_resume) (lua_State *L, int narg);
LUA_API int (lua_status) (lua_State *L);
/*
** garbage-collection function and options
*/
#define LUA_GCSTOP 0
#define LUA_GCRESTART 1
#define LUA_GCCOLLECT 2
#define LUA_GCCOUNT 3
#define LUA_GCCOUNTB 4
#define LUA_GCSTEP 5
#define LUA_GCSETPAUSE 6
#define LUA_GCSETSTEPMUL 7
#define LUA_GCISRUNNING 9
LUA_API int (lua_gc) (lua_State *L, int what, int data);
/*
** miscellaneous functions
*/
LUA_API int (lua_error) (lua_State *L);
LUA_API int (lua_next) (lua_State *L, int idx);
LUA_API void (lua_concat) (lua_State *L, int n);
LUA_API lua_Alloc (lua_getallocf) (lua_State *L, void **ud);
LUA_API void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);
/*
** ===============================================================
** some useful macros
** ===============================================================
*/
#define lua_pop(L,n) lua_settop(L, -(n)-1)
#define lua_newtable(L) lua_createtable(L, 0, 0)
#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0)
#define lua_strlen(L,i) lua_objlen(L, (i))
#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION)
#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE)
#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA)
#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL)
#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN)
#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD)
#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE)
#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0)
#define lua_pushliteral(L, s) \
lua_pushlstring(L, "" s, (sizeof(s)/sizeof(char))-1)
#define lua_setglobal(L,s) lua_setfield(L, LUA_GLOBALSINDEX, (s))
#define lua_getglobal(L,s) lua_getfield(L, LUA_GLOBALSINDEX, (s))
#define lua_tostring(L,i) lua_tolstring(L, (i), NULL)
/*
** compatibility macros and functions
*/
#define lua_open() luaL_newstate()
#define lua_getregistry(L) lua_pushvalue(L, LUA_REGISTRYINDEX)
#define lua_getgccount(L) lua_gc(L, LUA_GCCOUNT, 0)
#define lua_Chunkreader lua_Reader
#define lua_Chunkwriter lua_Writer
/* hack */
LUA_API void lua_setlevel (lua_State *from, lua_State *to);
/*
** {======================================================================
** Debug API
** =======================================================================
*/
/*
** Event codes
*/
#define LUA_HOOKCALL 0
#define LUA_HOOKRET 1
#define LUA_HOOKLINE 2
#define LUA_HOOKCOUNT 3
#define LUA_HOOKTAILRET 4
/*
** Event masks
*/
#define LUA_MASKCALL (1 << LUA_HOOKCALL)
#define LUA_MASKRET (1 << LUA_HOOKRET)
#define LUA_MASKLINE (1 << LUA_HOOKLINE)
#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT)
typedef struct lua_Debug lua_Debug; /* activation record */
/* Functions to be called by the debuger in specific events */
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar);
LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar);
LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n);
LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n);
LUA_API const char *lua_getupvalue (lua_State *L, int funcindex, int n);
LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n);
LUA_API int lua_sethook (lua_State *L, lua_Hook func, int mask, int count);
LUA_API lua_Hook lua_gethook (lua_State *L);
LUA_API int lua_gethookmask (lua_State *L);
LUA_API int lua_gethookcount (lua_State *L);
/* From Lua 5.2. */
LUA_API void *lua_upvalueid (lua_State *L, int idx, int n);
LUA_API void lua_upvaluejoin (lua_State *L, int idx1, int n1, int idx2, int n2);
LUA_API int lua_loadx (lua_State *L, lua_Reader reader, void *dt,
const char *chunkname, const char *mode);
LUA_API const lua_Number *lua_version (lua_State *L);
LUA_API void lua_copy (lua_State *L, int fromidx, int toidx);
LUA_API lua_Number lua_tonumberx (lua_State *L, int idx, int *isnum);
LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *isnum);
/* From Lua 5.3. */
LUA_API int lua_isyieldable (lua_State *L);
struct lua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) `global', `local', `field', `method' */
const char *what; /* (S) `Lua', `C', `main', `tail' */
const char *source; /* (S) */
int currentline; /* (l) */
int nups; /* (u) number of upvalues */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
char short_src[LUA_IDSIZE]; /* (S) */
/* private part */
int i_ci; /* active function */
};
/* }====================================================================== */
/******************************************************************************
* Copyright (C) 1994-2008 Lua.org, PUC-Rio. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/
#endif

View File

@ -1,154 +0,0 @@
/*
** Configuration header.
** Copyright (C) 2005-2025 Mike Pall. See Copyright Notice in luajit.h
*/
#ifndef luaconf_h
#define luaconf_h
#ifndef WINVER
#define WINVER 0x0501
#endif
#include <stddef.h>
/* Default path for loading Lua and C modules with require(). */
#if defined(_WIN32)
/*
** In Windows, any exclamation mark ('!') in the path is replaced by the
** path of the directory of the executable file of the current process.
*/
#define LUA_LDIR "!\\lua\\"
#define LUA_CDIR "!\\"
#define LUA_PATH_DEFAULT \
".\\?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;"
#define LUA_CPATH_DEFAULT \
".\\?.dll;" LUA_CDIR"?.dll;" LUA_CDIR"loadall.dll"
#else
/*
** Note to distribution maintainers: do NOT patch the following lines!
** Please read ../doc/install.html#distro and pass PREFIX=/usr instead.
*/
#ifndef LUA_MULTILIB
#define LUA_MULTILIB "lib"
#endif
#ifndef LUA_LMULTILIB
#define LUA_LMULTILIB "lib"
#endif
#define LUA_LROOT "/usr/local"
#define LUA_LUADIR "/lua/5.1/"
#ifdef LUA_ROOT
#define LUA_JROOT LUA_ROOT
#define LUA_RLDIR LUA_ROOT "/share" LUA_LUADIR
#define LUA_RCDIR LUA_ROOT "/" LUA_MULTILIB LUA_LUADIR
#define LUA_RLPATH ";" LUA_RLDIR "?.lua;" LUA_RLDIR "?/init.lua"
#define LUA_RCPATH ";" LUA_RCDIR "?.so"
#else
#define LUA_JROOT LUA_LROOT
#define LUA_RLPATH
#define LUA_RCPATH
#endif
#ifndef LUA_LJDIR
#define LUA_LJDIR LUA_JROOT "/share/luajit-2.1"
#endif
#define LUA_JPATH ";" LUA_LJDIR "/?.lua"
#define LUA_LLDIR LUA_LROOT "/share" LUA_LUADIR
#define LUA_LCDIR LUA_LROOT "/" LUA_LMULTILIB LUA_LUADIR
#define LUA_LLPATH ";" LUA_LLDIR "?.lua;" LUA_LLDIR "?/init.lua"
#define LUA_LCPATH1 ";" LUA_LCDIR "?.so"
#define LUA_LCPATH2 ";" LUA_LCDIR "loadall.so"
#define LUA_PATH_DEFAULT "./?.lua" LUA_JPATH LUA_LLPATH LUA_RLPATH
#define LUA_CPATH_DEFAULT "./?.so" LUA_LCPATH1 LUA_RCPATH LUA_LCPATH2
#endif
/* Environment variable names for path overrides and initialization code. */
#define LUA_PATH "LUA_PATH"
#define LUA_CPATH "LUA_CPATH"
#define LUA_INIT "LUA_INIT"
/* Special file system characters. */
#if defined(_WIN32)
#define LUA_DIRSEP "\\"
#else
#define LUA_DIRSEP "/"
#endif
#define LUA_PATHSEP ";"
#define LUA_PATH_MARK "?"
#define LUA_EXECDIR "!"
#define LUA_IGMARK "-"
#define LUA_PATH_CONFIG \
LUA_DIRSEP "\n" LUA_PATHSEP "\n" LUA_PATH_MARK "\n" \
LUA_EXECDIR "\n" LUA_IGMARK "\n"
/* Quoting in error messages. */
#define LUA_QL(x) "'" x "'"
#define LUA_QS LUA_QL("%s")
/* Various tunables. */
#define LUAI_MAXSTACK 65500 /* Max. # of stack slots for a thread (<64K). */
#define LUAI_MAXCSTACK 8000 /* Max. # of stack slots for a C func (<10K). */
#define LUAI_GCPAUSE 200 /* Pause GC until memory is at 200%. */
#define LUAI_GCMUL 200 /* Run GC at 200% of allocation speed. */
#define LUA_MAXCAPTURES 32 /* Max. pattern captures. */
/* Configuration for the frontend (the luajit executable). */
#if defined(luajit_c)
#define LUA_PROGNAME "luajit" /* Fallback frontend name. */
#define LUA_PROMPT "> " /* Interactive prompt. */
#define LUA_PROMPT2 ">> " /* Continuation prompt. */
#define LUA_MAXINPUT 512 /* Max. input line length. */
#endif
/* Note: changing the following defines breaks the Lua 5.1 ABI. */
#define LUA_INTEGER ptrdiff_t
#define LUA_IDSIZE 60 /* Size of lua_Debug.short_src. */
/*
** Size of lauxlib and io.* on-stack buffers. Weird workaround to avoid using
** unreasonable amounts of stack space, but still retain ABI compatibility.
** Blame Lua for depending on BUFSIZ in the ABI, blame **** for wrecking it.
*/
#define LUAL_BUFFERSIZE (BUFSIZ > 16384 ? 8192 : BUFSIZ)
/* The following defines are here only for compatibility with luaconf.h
** from the standard Lua distribution. They must not be changed for LuaJIT.
*/
#define LUA_NUMBER_DOUBLE
#define LUA_NUMBER double
#define LUAI_UACNUMBER double
#define LUA_NUMBER_SCAN "%lf"
#define LUA_NUMBER_FMT "%.14g"
#define lua_number2str(s, n) sprintf((s), LUA_NUMBER_FMT, (n))
#define LUAI_MAXNUMBER2STR 32
#define LUA_INTFRMLEN "l"
#define LUA_INTFRM_T long
/* Linkage of public API functions. */
#if defined(LUA_BUILD_AS_DLL)
#if defined(LUA_CORE) || defined(LUA_LIB)
#define LUA_API __declspec(dllexport)
#else
#define LUA_API __declspec(dllimport)
#endif
#else
#define LUA_API extern
#endif
#define LUALIB_API LUA_API
/* Compatibility support for assertions. */
#if defined(LUA_USE_ASSERT) || defined(LUA_USE_APICHECK)
#include <assert.h>
#endif
#ifdef LUA_USE_ASSERT
#define lua_assert(x) assert(x)
#endif
#ifdef LUA_USE_APICHECK
#define luai_apicheck(L, o) { (void)L; assert(o); }
#else
#define luai_apicheck(L, o) { (void)L; }
#endif
#endif

View File

@ -1,79 +0,0 @@
/*
** LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
**
** Copyright (C) 2005-2025 Mike Pall. All rights reserved.
**
** Permission is hereby granted, free of charge, to any person obtaining
** a copy of this software and associated documentation files (the
** "Software"), to deal in the Software without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Software, and to
** permit persons to whom the Software is furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be
** included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**
** [ MIT license: https://www.opensource.org/licenses/mit-license.php ]
*/
#ifndef _LUAJIT_H
#define _LUAJIT_H
#include "lua.h"
#define LUAJIT_VERSION "LuaJIT 2.1.1736781742"
#define LUAJIT_VERSION_NUM 20199 /* Deprecated. */
#define LUAJIT_VERSION_SYM luaJIT_version_2_1_1736781742
#define LUAJIT_COPYRIGHT "Copyright (C) 2005-2025 Mike Pall"
#define LUAJIT_URL "https://luajit.org/"
/* Modes for luaJIT_setmode. */
#define LUAJIT_MODE_MASK 0x00ff
enum {
LUAJIT_MODE_ENGINE, /* Set mode for whole JIT engine. */
LUAJIT_MODE_DEBUG, /* Set debug mode (idx = level). */
LUAJIT_MODE_FUNC, /* Change mode for a function. */
LUAJIT_MODE_ALLFUNC, /* Recurse into subroutine protos. */
LUAJIT_MODE_ALLSUBFUNC, /* Change only the subroutines. */
LUAJIT_MODE_TRACE, /* Flush a compiled trace. */
LUAJIT_MODE_WRAPCFUNC = 0x10, /* Set wrapper mode for C function calls. */
LUAJIT_MODE_MAX
};
/* Flags or'ed in to the mode. */
#define LUAJIT_MODE_OFF 0x0000 /* Turn feature off. */
#define LUAJIT_MODE_ON 0x0100 /* Turn feature on. */
#define LUAJIT_MODE_FLUSH 0x0200 /* Flush JIT-compiled code. */
/* LuaJIT public C API. */
/* Control the JIT engine. */
LUA_API int luaJIT_setmode(lua_State *L, int idx, int mode);
/* Low-overhead profiling API. */
typedef void (*luaJIT_profile_callback)(void *data, lua_State *L,
int samples, int vmstate);
LUA_API void luaJIT_profile_start(lua_State *L, const char *mode,
luaJIT_profile_callback cb, void *data);
LUA_API void luaJIT_profile_stop(lua_State *L);
LUA_API const char *luaJIT_profile_dumpstack(lua_State *L, const char *fmt,
int depth, size_t *len);
/* Enforce (dynamic) linker error for version mismatches. Call from main. */
LUA_API void LUAJIT_VERSION_SYM(void);
#endif

View File

@ -1,44 +0,0 @@
/*
** Standard library header.
** Copyright (C) 2005-2025 Mike Pall. See Copyright Notice in luajit.h
*/
#ifndef _LUALIB_H
#define _LUALIB_H
#include "lua.h"
#define LUA_FILEHANDLE "FILE*"
#define LUA_COLIBNAME "coroutine"
#define LUA_MATHLIBNAME "math"
#define LUA_STRLIBNAME "string"
#define LUA_TABLIBNAME "table"
#define LUA_IOLIBNAME "io"
#define LUA_OSLIBNAME "os"
#define LUA_LOADLIBNAME "package"
#define LUA_DBLIBNAME "debug"
#define LUA_BITLIBNAME "bit"
#define LUA_JITLIBNAME "jit"
#define LUA_FFILIBNAME "ffi"
LUALIB_API int luaopen_base(lua_State *L);
LUALIB_API int luaopen_math(lua_State *L);
LUALIB_API int luaopen_string(lua_State *L);
LUALIB_API int luaopen_table(lua_State *L);
LUALIB_API int luaopen_io(lua_State *L);
LUALIB_API int luaopen_os(lua_State *L);
LUALIB_API int luaopen_package(lua_State *L);
LUALIB_API int luaopen_debug(lua_State *L);
LUALIB_API int luaopen_bit(lua_State *L);
LUALIB_API int luaopen_jit(lua_State *L);
LUALIB_API int luaopen_ffi(lua_State *L);
LUALIB_API int luaopen_string_buffer(lua_State *L);
LUALIB_API void luaL_openlibs(lua_State *L);
#ifndef lua_assert
#define lua_assert(x) ((void)0)
#endif
#endif

Binary file not shown.

Binary file not shown.

View File

@ -1,61 +1,110 @@
package luajit
/*
#cgo CFLAGS: -I${SRCDIR}/vendor/luajit/include
#cgo windows LDFLAGS: -L${SRCDIR}/vendor/luajit/windows -lluajit -static
#cgo !windows LDFLAGS: -L${SRCDIR}/vendor/luajit/linux -lluajit -static
#cgo !windows pkg-config: --static luajit
#cgo windows CFLAGS: -I/usr/local/include/luajit-2.1
#cgo windows LDFLAGS: -lluajit-5.1 -static
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <string.h>
static int do_string(lua_State *L, const char *s) {
int status = luaL_loadstring(L, s);
if (status) return status;
return lua_pcall(L, 0, LUA_MULTRET, 0);
int status = luaL_loadstring(L, s);
if (status == 0) {
status = lua_pcall(L, 0, 0, 0);
}
return status;
}
static int do_file(lua_State *L, const char *filename) {
int status = luaL_loadfile(L, filename);
if (status) return status;
return lua_pcall(L, 0, LUA_MULTRET, 0);
int status = luaL_loadfile(L, filename);
if (status == 0) {
status = lua_pcall(L, 0, 0, 0);
}
return status;
}
static int execute_with_results(lua_State *L, const char *code, int store_results) {
int status = luaL_loadstring(L, code);
if (status != 0) return status;
return lua_pcall(L, 0, store_results ? LUA_MULTRET : 0, 0);
}
static size_t get_table_length(lua_State *L, int index) {
return lua_objlen(L, index);
}
static int is_integer(lua_State *L, int index) {
if (!lua_isnumber(L, index)) return 0;
lua_Number n = lua_tonumber(L, index);
return n == (lua_Number)(lua_Integer)n;
}
static int sample_array_type(lua_State *L, int index, int count) {
int all_numbers = 1;
int all_integers = 1;
int all_strings = 1;
int all_bools = 1;
for (int i = 1; i <= count && i <= 5; i++) {
lua_pushnumber(L, i);
lua_gettable(L, index);
int type = lua_type(L, -1);
if (type != LUA_TNUMBER) all_numbers = all_integers = 0;
if (type != LUA_TSTRING) all_strings = 0;
if (type != LUA_TBOOLEAN) all_bools = 0;
if (all_numbers && !is_integer(L, -1)) all_integers = 0;
lua_pop(L, 1);
if (!all_numbers && !all_strings && !all_bools) break;
}
if (all_integers) return 1;
if (all_numbers) return 2;
if (all_strings) return 3;
if (all_bools) return 4;
return 0;
}
*/
import "C"
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"unsafe"
)
// State represents a Lua state with configurable stack safety
// Stack management constants
const (
LUA_MINSTACK = 20
LUA_MAXSTACK = 1000000
LUA_REGISTRYINDEX = -10000
LUA_GLOBALSINDEX = -10002
)
type State struct {
L *C.lua_State
safeStack bool
L *C.lua_State
}
// NewSafe creates a new Lua state with full stack safety guarantees
func NewSafe() *State {
func New(openLibs ...bool) *State {
L := C.luaL_newstate()
if L == nil {
return nil
}
C.luaL_openlibs(L)
return &State{L: L, safeStack: true}
}
// New creates a new Lua state with minimal stack checking
func New() *State {
L := C.luaL_newstate()
if L == nil {
return nil
if len(openLibs) == 0 || openLibs[0] {
C.luaL_openlibs(L)
}
C.luaL_openlibs(L)
return &State{L: L, safeStack: false}
return &State{L: L}
}
// Close closes the Lua state
func (s *State) Close() {
if s.L != nil {
C.lua_close(s.L)
@ -63,157 +112,12 @@ func (s *State) Close() {
}
}
// DoString executes a Lua string with appropriate stack management
func (s *State) DoString(str string) error {
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))
if s.safeStack {
return stackGuardErr(s, func() error {
return s.safeCall(func() C.int {
return C.do_string(s.L, cstr)
})
})
}
status := C.do_string(s.L, cstr)
if status != 0 {
return &LuaError{
Code: int(status),
Message: s.ToString(-1),
}
}
return nil
}
// PushValue pushes a Go value onto the stack
func (s *State) PushValue(v interface{}) error {
if s.safeStack {
return stackGuardErr(s, func() error {
if err := s.checkStack(1); err != nil {
return fmt.Errorf("pushing value: %w", err)
}
return s.pushValueUnsafe(v)
})
}
return s.pushValueUnsafe(v)
}
func (s *State) pushValueSafe(v interface{}) error {
if err := s.checkStack(1); err != nil {
return fmt.Errorf("pushing value: %w", err)
}
return s.pushValueUnsafe(v)
}
func (s *State) pushValueUnsafe(v interface{}) error {
switch v := v.(type) {
case nil:
s.PushNil()
case bool:
s.PushBoolean(v)
case float64:
s.PushNumber(v)
case int:
s.PushNumber(float64(v))
case string:
s.PushString(v)
case map[string]interface{}:
// Special case: handle array stored in map
if arr, ok := v[""].([]float64); ok {
s.NewTable()
for i, elem := range arr {
s.PushNumber(float64(i + 1))
s.PushNumber(elem)
s.SetTable(-3)
}
return nil
}
return s.pushTableUnsafe(v)
case []float64:
s.NewTable()
for i, elem := range v {
s.PushNumber(float64(i + 1))
s.PushNumber(elem)
s.SetTable(-3)
}
case []interface{}:
s.NewTable()
for i, elem := range v {
s.PushNumber(float64(i + 1))
if err := s.pushValueUnsafe(elem); err != nil {
return err
}
s.SetTable(-3)
}
default:
return fmt.Errorf("unsupported type: %T", v)
}
return nil
}
// ToValue converts a Lua value to a Go value
func (s *State) ToValue(index int) (interface{}, error) {
if s.safeStack {
return stackGuardValue[interface{}](s, func() (interface{}, error) {
return s.toValueUnsafe(index)
})
}
return s.toValueUnsafe(index)
}
func (s *State) toValueUnsafe(index int) (interface{}, error) {
switch s.GetType(index) {
case TypeNil:
return nil, nil
case TypeBoolean:
return s.ToBoolean(index), nil
case TypeNumber:
return s.ToNumber(index), nil
case TypeString:
return s.ToString(index), nil
case TypeTable:
if !s.IsTable(index) {
return nil, fmt.Errorf("not a table at index %d", index)
}
return s.toTableUnsafe(index)
default:
return nil, fmt.Errorf("unsupported type: %s", s.GetType(index))
}
}
// Simple operations remain unchanged as they don't need stack protection
func (s *State) GetType(index int) LuaType { return LuaType(C.lua_type(s.L, C.int(index))) }
func (s *State) IsFunction(index int) bool { return s.GetType(index) == TypeFunction }
func (s *State) IsTable(index int) bool { return s.GetType(index) == TypeTable }
func (s *State) ToBoolean(index int) bool { return C.lua_toboolean(s.L, C.int(index)) != 0 }
func (s *State) ToNumber(index int) float64 { return float64(C.lua_tonumber(s.L, C.int(index))) }
func (s *State) ToString(index int) string {
return C.GoString(C.lua_tolstring(s.L, C.int(index), nil))
}
func (s *State) GetTop() int { return int(C.lua_gettop(s.L)) }
func (s *State) Pop(n int) { C.lua_settop(s.L, C.int(-n-1)) }
func (s *State) SetTop(index int) { C.lua_settop(s.L, C.int(index)) }
// Push operations
func (s *State) PushNil() { C.lua_pushnil(s.L) }
func (s *State) PushBoolean(b bool) { C.lua_pushboolean(s.L, C.int(bool2int(b))) }
func (s *State) PushNumber(n float64) { C.lua_pushnumber(s.L, C.double(n)) }
func (s *State) PushString(str string) {
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))
C.lua_pushstring(s.L, cstr)
}
// Helper functions
func bool2int(b bool) int {
if b {
return 1
}
return 0
}
// Stack operations
func (s *State) GetTop() int { return int(C.lua_gettop(s.L)) }
func (s *State) SetTop(index int) { C.lua_settop(s.L, C.int(index)) }
func (s *State) PushCopy(index int) { C.lua_pushvalue(s.L, C.int(index)) }
func (s *State) Pop(n int) { C.lua_settop(s.L, C.int(-n-1)) }
func (s *State) Remove(index int) { C.lua_remove(s.L, C.int(index)) }
func (s *State) absIndex(index int) int {
if index > 0 || index <= LUA_REGISTRYINDEX {
@ -222,102 +126,602 @@ func (s *State) absIndex(index int) int {
return s.GetTop() + index + 1
}
// SetField sets a field in a table at the given index with cached absolute index
func (s *State) SetField(index int, key string) {
absIdx := index
if s.safeStack && (index < 0 && index > LUA_REGISTRYINDEX) {
absIdx = s.GetTop() + index + 1
}
// Type checking
func (s *State) GetType(index int) LuaType { return LuaType(C.lua_type(s.L, C.int(index))) }
func (s *State) IsNil(index int) bool { return s.GetType(index) == TypeNil }
func (s *State) IsBoolean(index int) bool { return s.GetType(index) == TypeBoolean }
func (s *State) IsNumber(index int) bool { return C.lua_isnumber(s.L, C.int(index)) != 0 }
func (s *State) IsString(index int) bool { return C.lua_isstring(s.L, C.int(index)) != 0 }
func (s *State) IsTable(index int) bool { return s.GetType(index) == TypeTable }
func (s *State) IsFunction(index int) bool { return s.GetType(index) == TypeFunction }
cstr := C.CString(key)
defer C.free(unsafe.Pointer(cstr))
C.lua_setfield(s.L, C.int(absIdx), cstr)
// Value conversion
func (s *State) ToBoolean(index int) bool { return C.lua_toboolean(s.L, C.int(index)) != 0 }
func (s *State) ToNumber(index int) float64 { return float64(C.lua_tonumber(s.L, C.int(index))) }
func (s *State) ToString(index int) string {
var length C.size_t
cstr := C.lua_tolstring(s.L, C.int(index), &length)
if cstr == nil {
return ""
}
return C.GoStringN(cstr, C.int(length))
}
// GetField gets a field from a table with cached absolute index
// Push methods
func (s *State) PushNil() { C.lua_pushnil(s.L) }
func (s *State) PushBoolean(b bool) { C.lua_pushboolean(s.L, boolToInt(b)) }
func (s *State) PushNumber(n float64) { C.lua_pushnumber(s.L, C.lua_Number(n)) }
func (s *State) PushString(str string) {
if len(str) < 128 {
cstr := C.CString(str)
defer C.free(unsafe.Pointer(cstr))
C.lua_pushlstring(s.L, cstr, C.size_t(len(str)))
} else {
header := (*struct {
p unsafe.Pointer
len int
cap int
})(unsafe.Pointer(&str))
C.lua_pushlstring(s.L, (*C.char)(header.p), C.size_t(len(str)))
}
}
// Table operations
func (s *State) CreateTable(narr, nrec int) { C.lua_createtable(s.L, C.int(narr), C.int(nrec)) }
func (s *State) NewTable() { C.lua_createtable(s.L, 0, 0) }
func (s *State) GetTable(index int) { C.lua_gettable(s.L, C.int(index)) }
func (s *State) SetTable(index int) { C.lua_settable(s.L, C.int(index)) }
func (s *State) Next(index int) bool { return C.lua_next(s.L, C.int(index)) != 0 }
func (s *State) GetField(index int, key string) {
absIdx := index
if s.safeStack && (index < 0 && index > LUA_REGISTRYINDEX) {
absIdx = s.GetTop() + index + 1
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))
C.lua_getfield(s.L, C.int(index), ckey)
}
func (s *State) SetField(index int, key string) {
ckey := C.CString(key)
defer C.free(unsafe.Pointer(ckey))
C.lua_setfield(s.L, C.int(index), ckey)
}
func (s *State) GetTableLength(index int) int {
return int(C.get_table_length(s.L, C.int(index)))
}
// Enhanced PushValue with comprehensive type support
func (s *State) PushValue(v any) error {
switch val := v.(type) {
case nil:
s.PushNil()
case bool:
s.PushBoolean(val)
case int:
s.PushNumber(float64(val))
case int64:
s.PushNumber(float64(val))
case float64:
s.PushNumber(val)
case string:
s.PushString(val)
case []int:
return s.pushIntSlice(val)
case []string:
return s.pushStringSlice(val)
case []bool:
return s.pushBoolSlice(val)
case []float64:
return s.pushFloatSlice(val)
case []any:
return s.pushAnySlice(val)
case []map[string]any:
return s.pushMapSlice(val)
case map[string]string:
return s.pushStringMap(val)
case map[string]int:
return s.pushIntMap(val)
case map[int]any:
return s.pushIntKeyMap(val)
case map[string]any:
return s.pushAnyMap(val)
default:
return fmt.Errorf("unsupported type: %T", v)
}
return nil
}
func (s *State) pushIntSlice(arr []int) error {
s.CreateTable(len(arr), 0)
for i, v := range arr {
s.PushNumber(float64(i + 1))
s.PushNumber(float64(v))
s.SetTable(-3)
}
return nil
}
func (s *State) pushStringSlice(arr []string) error {
s.CreateTable(len(arr), 0)
for i, v := range arr {
s.PushNumber(float64(i + 1))
s.PushString(v)
s.SetTable(-3)
}
return nil
}
func (s *State) pushBoolSlice(arr []bool) error {
s.CreateTable(len(arr), 0)
for i, v := range arr {
s.PushNumber(float64(i + 1))
s.PushBoolean(v)
s.SetTable(-3)
}
return nil
}
func (s *State) pushFloatSlice(arr []float64) error {
s.CreateTable(len(arr), 0)
for i, v := range arr {
s.PushNumber(float64(i + 1))
s.PushNumber(v)
s.SetTable(-3)
}
return nil
}
func (s *State) pushAnySlice(arr []any) error {
s.CreateTable(len(arr), 0)
for i, v := range arr {
s.PushNumber(float64(i + 1))
if err := s.PushValue(v); err != nil {
return err
}
s.SetTable(-3)
}
return nil
}
func (s *State) pushStringMap(m map[string]string) error {
s.CreateTable(0, len(m))
for k, v := range m {
s.PushString(k)
s.PushString(v)
s.SetTable(-3)
}
return nil
}
func (s *State) pushIntMap(m map[string]int) error {
s.CreateTable(0, len(m))
for k, v := range m {
s.PushString(k)
s.PushNumber(float64(v))
s.SetTable(-3)
}
return nil
}
func (s *State) pushIntKeyMap(m map[int]any) error {
s.CreateTable(0, len(m))
for k, v := range m {
s.PushNumber(float64(k))
if err := s.PushValue(v); err != nil {
return err
}
s.SetTable(-3)
}
return nil
}
func (s *State) pushAnyMap(m map[string]any) error {
s.CreateTable(0, len(m))
for k, v := range m {
s.PushString(k)
if err := s.PushValue(v); err != nil {
return err
}
s.SetTable(-3)
}
return nil
}
// Enhanced ToValue with automatic type detection
func (s *State) ToValue(index int) (any, error) {
switch s.GetType(index) {
case TypeNil:
return nil, nil
case TypeBoolean:
return s.ToBoolean(index), nil
case TypeNumber:
num := s.ToNumber(index)
if num == float64(int(num)) && num >= -2147483648 && num <= 2147483647 {
return int(num), nil
}
return num, nil
case TypeString:
return s.ToString(index), nil
case TypeTable:
return s.ToTable(index)
default:
return nil, fmt.Errorf("unsupported type: %s", s.GetType(index))
}
}
// ToTable converts a Lua table to optimal Go type
func (s *State) ToTable(index int) (any, error) {
absIdx := s.absIndex(index)
if !s.IsTable(absIdx) {
return nil, fmt.Errorf("value at index %d is not a table", index)
}
if s.safeStack {
if err := s.checkStack(1); err != nil {
s.PushNil()
return
length := s.GetTableLength(absIdx)
if length > 0 {
arrayType := int(C.sample_array_type(s.L, C.int(absIdx), C.int(length)))
switch arrayType {
case 1: // int array
return s.extractIntArray(absIdx, length), nil
case 2: // float array
return s.extractFloatArray(absIdx, length), nil
case 3: // string array
return s.extractStringArray(absIdx, length), nil
case 4: // bool array
return s.extractBoolArray(absIdx, length), nil
default: // mixed array
return s.extractAnyArray(absIdx, length), nil
}
}
cstr := C.CString(key)
defer C.free(unsafe.Pointer(cstr))
C.lua_getfield(s.L, C.int(absIdx), cstr)
return s.extractAnyMap(absIdx)
}
// GetGlobal gets a global variable and pushes it onto the stack
func (s *State) GetGlobal(name string) {
if s.safeStack {
if err := s.checkStack(1); err != nil {
s.PushNil()
return
func (s *State) extractIntArray(index, length int) []int {
result := make([]int, length)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(index)
result[i-1] = int(s.ToNumber(-1))
s.Pop(1)
}
return result
}
func (s *State) extractFloatArray(index, length int) []float64 {
result := make([]float64, length)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(index)
result[i-1] = s.ToNumber(-1)
s.Pop(1)
}
return result
}
func (s *State) extractStringArray(index, length int) []string {
result := make([]string, length)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(index)
result[i-1] = s.ToString(-1)
s.Pop(1)
}
return result
}
func (s *State) extractBoolArray(index, length int) []bool {
result := make([]bool, length)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(index)
result[i-1] = s.ToBoolean(-1)
s.Pop(1)
}
return result
}
func (s *State) extractAnyArray(index, length int) []any {
result := make([]any, length)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(index)
if val, err := s.ToValue(-1); err == nil {
result[i-1] = val
}
s.Pop(1)
}
cstr := C.CString(name)
defer C.free(unsafe.Pointer(cstr))
C.lua_getfield(s.L, C.LUA_GLOBALSINDEX, cstr)
return result
}
// SetGlobal sets a global variable from the value at the top of the stack
func (s *State) SetGlobal(name string) {
// SetGlobal doesn't need stack space checking as it pops the value
cstr := C.CString(name)
defer C.free(unsafe.Pointer(cstr))
C.lua_setfield(s.L, C.LUA_GLOBALSINDEX, cstr)
}
func (s *State) extractAnyMap(index int) (map[string]any, error) {
result := make(map[string]any)
s.PushNil()
for s.Next(index) {
var key string
switch s.GetType(-2) {
case TypeString:
key = s.ToString(-2)
case TypeNumber:
key = strconv.FormatFloat(s.ToNumber(-2), 'g', -1, 64)
default:
s.Pop(1)
continue
}
// Remove removes element with cached absolute index
func (s *State) Remove(index int) {
absIdx := index
if s.safeStack && (index < 0 && index > LUA_REGISTRYINDEX) {
absIdx = s.GetTop() + index + 1
if value, err := s.ToValue(-1); err == nil {
result[key] = value
}
s.Pop(1)
}
C.lua_remove(s.L, C.int(absIdx))
return result, nil
}
// Global operations
func (s *State) GetGlobal(name string) { s.GetField(LUA_GLOBALSINDEX, name) }
func (s *State) SetGlobal(name string) { s.SetField(LUA_GLOBALSINDEX, name) }
// Code execution
func (s *State) LoadString(code string) error {
ccode := C.CString(code)
defer C.free(unsafe.Pointer(ccode))
status := C.luaL_loadstring(s.L, ccode)
if status != 0 {
err := s.CreateLuaError(int(status), "LoadString")
s.Pop(1)
return err
}
return nil
}
func (s *State) LoadFile(filename string) error {
cfilename := C.CString(filename)
defer C.free(unsafe.Pointer(cfilename))
status := C.luaL_loadfile(s.L, cfilename)
if status != 0 {
err := s.CreateLuaError(int(status), fmt.Sprintf("LoadFile(%s)", filename))
s.Pop(1)
return err
}
return nil
}
func (s *State) Call(nargs, nresults int) error {
status := C.lua_pcall(s.L, C.int(nargs), C.int(nresults), 0)
if status != 0 {
err := s.CreateLuaError(int(status), fmt.Sprintf("Call(%d,%d)", nargs, nresults))
s.Pop(1)
return err
}
return nil
}
func (s *State) DoString(code string) error {
ccode := C.CString(code)
defer C.free(unsafe.Pointer(ccode))
status := C.do_string(s.L, ccode)
if status != 0 {
err := s.CreateLuaError(int(status), "DoString")
s.Pop(1)
return err
}
return nil
}
// DoFile executes a Lua file with appropriate stack management
func (s *State) DoFile(filename string) error {
cfilename := C.CString(filename)
defer C.free(unsafe.Pointer(cfilename))
if s.safeStack {
return stackGuardErr(s, func() error {
return s.safeCall(func() C.int {
return C.do_file(s.L, cfilename)
})
})
}
status := C.do_file(s.L, cfilename)
if status != 0 {
return &LuaError{
Code: int(status),
Message: s.ToString(-1),
}
err := s.CreateLuaError(int(status), fmt.Sprintf("DoFile(%s)", filename))
s.Pop(1)
return err
}
return nil
}
func (s *State) SetPackagePath(path string) error {
path = filepath.ToSlash(path)
if err := s.DoString(fmt.Sprintf(`package.path = %q`, path)); err != nil {
return fmt.Errorf("setting package.path: %w", err)
func (s *State) Execute(code string) (int, error) {
baseTop := s.GetTop()
ccode := C.CString(code)
defer C.free(unsafe.Pointer(ccode))
status := C.execute_with_results(s.L, ccode, 1)
if status != 0 {
err := s.CreateLuaError(int(status), "Execute")
s.Pop(1)
return 0, err
}
return nil
return s.GetTop() - baseTop, nil
}
func (s *State) ExecuteWithResult(code string) (any, error) {
top := s.GetTop()
defer s.SetTop(top)
nresults, err := s.Execute(code)
if err != nil {
return nil, err
}
if nresults == 0 {
return nil, nil
}
return s.ToValue(-nresults)
}
func (s *State) BatchExecute(statements []string) error {
return s.DoString(strings.Join(statements, "; "))
}
// Package path operations
func (s *State) SetPackagePath(path string) error {
path = strings.ReplaceAll(path, "\\", "/")
return s.DoString(fmt.Sprintf(`package.path = %q`, path))
}
func (s *State) AddPackagePath(path string) error {
path = filepath.ToSlash(path)
if err := s.DoString(fmt.Sprintf(`package.path = package.path .. ";%s"`, path)); err != nil {
return fmt.Errorf("adding to package.path: %w", err)
path = strings.ReplaceAll(path, "\\", "/")
return s.DoString(fmt.Sprintf(`package.path = package.path .. ";%s"`, path))
}
// Metatable operations
func (s *State) SetMetatable(index int) { C.lua_setmetatable(s.L, C.int(index)) }
func (s *State) GetMetatable(index int) bool { return C.lua_getmetatable(s.L, C.int(index)) != 0 }
// Helper functions
func boolToInt(b bool) C.int {
if b {
return 1
}
return 0
}
// GetFieldString gets a string field from a table with default
func (s *State) GetFieldString(index int, key string, defaultVal string) string {
s.GetField(index, key)
defer s.Pop(1)
if s.IsString(-1) {
return s.ToString(-1)
}
return defaultVal
}
// GetFieldNumber gets a number field from a table with default
func (s *State) GetFieldNumber(index int, key string, defaultVal float64) float64 {
s.GetField(index, key)
defer s.Pop(1)
if s.IsNumber(-1) {
return s.ToNumber(-1)
}
return defaultVal
}
// GetFieldBool gets a boolean field from a table with default
func (s *State) GetFieldBool(index int, key string, defaultVal bool) bool {
s.GetField(index, key)
defer s.Pop(1)
if s.IsBoolean(-1) {
return s.ToBoolean(-1)
}
return defaultVal
}
// GetFieldTable gets a table field from a table
func (s *State) GetFieldTable(index int, key string) (any, bool) {
s.GetField(index, key)
defer s.Pop(1)
if s.IsTable(-1) {
val, err := s.ToTable(-1)
return val, err == nil
}
return nil, false
}
// ForEachTableKV iterates over string key-value pairs in a table
func (s *State) ForEachTableKV(index int, fn func(key, value string) bool) {
absIdx := s.absIndex(index)
s.PushNil()
for s.Next(absIdx) {
if s.IsString(-2) && s.IsString(-1) {
if !fn(s.ToString(-2), s.ToString(-1)) {
s.Pop(2)
return
}
}
s.Pop(1)
}
}
// ForEachArray iterates over array elements
func (s *State) ForEachArray(index int, fn func(i int, state *State) bool) {
absIdx := s.absIndex(index)
length := s.GetTableLength(absIdx)
for i := 1; i <= length; i++ {
s.PushNumber(float64(i))
s.GetTable(absIdx)
if !fn(i, s) {
s.Pop(1)
return
}
s.Pop(1)
}
}
// SafeToString safely converts value to string with error
func (s *State) SafeToString(index int) (string, error) {
if !s.IsString(index) && !s.IsNumber(index) {
return "", fmt.Errorf("value at index %d is not a string", index)
}
return s.ToString(index), nil
}
// SafeToNumber safely converts value to number with error
func (s *State) SafeToNumber(index int) (float64, error) {
if !s.IsNumber(index) {
return 0, fmt.Errorf("value at index %d is not a number", index)
}
return s.ToNumber(index), nil
}
// SafeToTable safely converts value to table with error
func (s *State) SafeToTable(index int) (any, error) {
if !s.IsTable(index) {
return nil, fmt.Errorf("value at index %d is not a table", index)
}
return s.ToTable(index)
}
// CallGlobal calls a global function with arguments
func (s *State) CallGlobal(name string, args ...any) ([]any, error) {
s.GetGlobal(name)
if !s.IsFunction(-1) {
s.Pop(1)
return nil, fmt.Errorf("global '%s' is not a function", name)
}
for i, arg := range args {
if err := s.PushValue(arg); err != nil {
s.Pop(i + 1)
return nil, fmt.Errorf("failed to push argument %d: %w", i+1, err)
}
}
baseTop := s.GetTop() - len(args) - 1
if err := s.Call(len(args), C.LUA_MULTRET); err != nil {
return nil, err
}
newTop := s.GetTop()
nresults := newTop - baseTop
results := make([]any, nresults)
for i := 0; i < nresults; i++ {
val, err := s.ToValue(baseTop + i + 1)
if err != nil {
results[i] = nil
} else {
results[i] = val
}
}
s.SetTop(baseTop)
return results, nil
}
func (s *State) pushMapSlice(arr []map[string]any) error {
s.CreateTable(len(arr), 0)
for i, m := range arr {
s.PushNumber(float64(i + 1))
if err := s.PushValue(m); err != nil {
return err
}
s.SetTable(-3)
}
return nil
}

View File

@ -1,276 +0,0 @@
package luajit
import (
"fmt"
"os"
"path/filepath"
"testing"
)
type stateFactory struct {
name string
new func() *State
}
var factories = []stateFactory{
{"unsafe", New},
{"safe", NewSafe},
}
func TestNew(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
})
}
}
func TestDoString(t *testing.T) {
tests := []struct {
name string
code string
wantErr bool
}{
{"simple addition", "return 1 + 1", false},
{"set global", "test = 42", false},
{"syntax error", "this is not valid lua", true},
{"runtime error", "error('test error')", true},
}
for _, f := range factories {
for _, tt := range tests {
t.Run(f.name+"/"+tt.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
err := L.DoString(tt.code)
if (err != nil) != tt.wantErr {
t.Errorf("DoString() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
}
func TestPushAndGetValues(t *testing.T) {
values := []struct {
name string
push func(*State)
check func(*State) error
}{
{
name: "string",
push: func(L *State) { L.PushString("hello") },
check: func(L *State) error {
if got := L.ToString(-1); got != "hello" {
return fmt.Errorf("got %q, want %q", got, "hello")
}
return nil
},
},
{
name: "number",
push: func(L *State) { L.PushNumber(42.5) },
check: func(L *State) error {
if got := L.ToNumber(-1); got != 42.5 {
return fmt.Errorf("got %f, want %f", got, 42.5)
}
return nil
},
},
{
name: "boolean",
push: func(L *State) { L.PushBoolean(true) },
check: func(L *State) error {
if got := L.ToBoolean(-1); !got {
return fmt.Errorf("got %v, want true", got)
}
return nil
},
},
{
name: "nil",
push: func(L *State) { L.PushNil() },
check: func(L *State) error {
if typ := L.GetType(-1); typ != TypeNil {
return fmt.Errorf("got type %v, want TypeNil", typ)
}
return nil
},
},
}
for _, f := range factories {
for _, v := range values {
t.Run(f.name+"/"+v.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
v.push(L)
if err := v.check(L); err != nil {
t.Error(err)
}
})
}
}
}
func TestStackManipulation(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
// Push values
values := []string{"first", "second", "third"}
for _, v := range values {
L.PushString(v)
}
// Check size
if top := L.GetTop(); top != len(values) {
t.Errorf("stack size = %d, want %d", top, len(values))
}
// Pop one value
L.Pop(1)
// Check new top
if str := L.ToString(-1); str != "second" {
t.Errorf("top value = %q, want 'second'", str)
}
// Check new size
if top := L.GetTop(); top != len(values)-1 {
t.Errorf("stack size after pop = %d, want %d", top, len(values)-1)
}
})
}
}
func TestGlobals(t *testing.T) {
for _, f := range factories {
t.Run(f.name, func(t *testing.T) {
L := f.new()
if L == nil {
t.Fatal("Failed to create Lua state")
}
defer L.Close()
// Test via Lua
if err := L.DoString(`globalVar = "test"`); err != nil {
t.Fatalf("DoString error: %v", err)
}
// Get the global
L.GetGlobal("globalVar")
if str := L.ToString(-1); str != "test" {
t.Errorf("global value = %q, want 'test'", str)
}
L.Pop(1)
// Set and get via API
L.PushNumber(42)
L.SetGlobal("testNum")
L.GetGlobal("testNum")
if num := L.ToNumber(-1); num != 42 {
t.Errorf("global number = %f, want 42", num)
}
})
}
}
func TestDoFile(t *testing.T) {
L := NewSafe()
defer L.Close()
// Create test file
content := []byte(`
function add(a, b)
return a + b
end
result = add(40, 2)
`)
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.lua")
if err := os.WriteFile(filename, content, 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
if err := L.DoFile(filename); err != nil {
t.Fatalf("DoFile failed: %v", err)
}
L.GetGlobal("result")
if result := L.ToNumber(-1); result != 42 {
t.Errorf("Expected result=42, got %v", result)
}
}
func TestRequireAndPackagePath(t *testing.T) {
L := NewSafe()
defer L.Close()
tmpDir := t.TempDir()
// Create module file
moduleContent := []byte(`
local M = {}
function M.multiply(a, b)
return a * b
end
return M
`)
if err := os.WriteFile(filepath.Join(tmpDir, "mathmod.lua"), moduleContent, 0644); err != nil {
t.Fatalf("Failed to create module file: %v", err)
}
// Add module path and test require
if err := L.AddPackagePath(filepath.Join(tmpDir, "?.lua")); err != nil {
t.Fatalf("AddPackagePath failed: %v", err)
}
if err := L.DoString(`
local math = require("mathmod")
result = math.multiply(6, 7)
`); err != nil {
t.Fatalf("Failed to require module: %v", err)
}
L.GetGlobal("result")
if result := L.ToNumber(-1); result != 42 {
t.Errorf("Expected result=42, got %v", result)
}
}
func TestSetPackagePath(t *testing.T) {
L := NewSafe()
defer L.Close()
customPath := "./custom/?.lua"
if err := L.SetPackagePath(customPath); err != nil {
t.Fatalf("SetPackagePath failed: %v", err)
}
L.GetGlobal("package")
L.GetField(-1, "path")
if path := L.ToString(-1); path != customPath {
t.Errorf("Expected package.path=%q, got %q", customPath, path)
}
}