glua

package module
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: BSD-3-Clause Imports: 27 Imported by: 0

README

glua

Go Reference

A pure-Go Lua 5.4 for embedding small scripts in Go programs: rules, policies, handlers and configuration. Scripts are loaded once, frozen, and then called from any goroutine as typed Go functions, under limits on steps, memory and nesting.

Goals

  • Easy to use. A small, modern Go API: generics for typed calls, context.Context for cancellation, plain Go values in and out. Pure Go with no cgo, so it cross-compiles and builds anywhere Go does.
  • Performant. A register VM tuned for the hot paths of embedded scripts, measured against C Lua and other Go implementations (see Performance).

Examples

import lua "github.com/folbricht/glua"
Run Lua from a string

Eval runs a chunk once and converts its result:

n, err := lua.Eval[int64](ctx, `return 6 * 7`) // 42

To call into a script more than once, load it into a Runtime and bind its functions to Go functions:

rt := lua.MustNew()
script, err := rt.Load(ctx, "greet", `
	function greet(name)
		return "hello, " .. name
	end`)
greet, err := script.Func1[string, string]("greet")

s, err := greet(ctx, "world") // "hello, world"
Load a script from a file

Load takes the script's source and a name that error messages use, here the file's:

src, err := os.ReadFile("policy.lua")
rt := lua.MustNew(lua.Limits{Steps: 100_000, AllocBytes: 1 << 20})
script, err := rt.Load(ctx, "policy.lua", string(src))
allow, err := script.Func2[*Request, int64, bool]("allow")

ok, err := allow(ctx, req, 10) // a plain Go function, safe from any goroutine
-- policy.lua
function allow(req, limit)
  return req.method == "GET" and req.cost <= limit
end

Load runs the script's top level once and freezes what it built, so one Script serves every goroutine. The fields of *Request are visible to Lua by their snake_case names.

Call Go from Lua

Go functions are registered as globals when the Runtime is made. Typed adapters such as lua.Fn1 wrap plain Go functions; a lua.GoFunc reads its arguments and sets its results itself:

clamp := lua.GoFunc(func(c *lua.Call) error {
	x, lo, hi := c.Arg[int64](0), c.Arg[int64](1), c.Arg[int64](2)
	return c.Return(min(max(x, lo), hi))
})

rt := lua.MustNew(
	lua.Output(os.Stdout), // where print writes; the default discards
	lua.Global("upper", lua.Fn1(strings.ToUpper)),
	lua.Global("clamp", clamp),
)
script, err := rt.Load(ctx, "main.lua", `
	print(upper("hello"), clamp(15, 0, 10)) -- HELLO   10
	function level(n) return upper("level ") .. clamp(n, 1, 5) end`)

level, err := script.Func1[int64, string]("level") // takes an int64, returns a string
out, err := level(ctx, 7) // "LEVEL 5"

A Go error returned by a host function becomes a Lua error, which the script can catch with pcall, unless the host made it with lua.Terminate to end the call.

Rules over a Go struct

Expressions over a Go struct compile into rules, with misspelt names caught when the rule is made:

rule, err := rt.Expr[Order, bool]("gold", `total > 100 and customer.tier == "gold"`)
ok, err := rule.Eval(ctx, &order)

Requirements: Go 1.27 or later, a 64-bit platform. No cgo.

Status: feature-complete and tested against the Lua 5.4.9 test suite and against C Lua by differential fuzzing. Runtimes run the fast interpreter loop, which trusts bytecode the verifier has checked; Hardened runs the bounds-checked loop instead.

Performance

Time to run a suite of 21 small scripts, relative to C Lua 5.4.9 (geometric mean; lower is faster). Details, per-script numbers and method are in the benchmarks.

Engine Relative to C Lua
C Lua 5.4.9 1.00x
glua 1.23x
wangshu 4.28x
GopherLua 5.21x
golua 5.96x

Documentation

  • Package documentation: the API, with runnable examples.
  • luajson: converting between Lua values and JSON, and a json module for scripts.
  • The language: how glua differs from Lua 5.4, and which libraries it has.
  • Sandbox and limits: what a script can and cannot do, the limits and the fixed caps.
  • GopherLua and glua side by side: how common tasks look in each.
  • Benchmarks: small scripts in C Lua, glua and other Lua implementations for Go, glua's two interpreter loops, and the cost of calls between Go and Lua.

License

BSD 3-Clause, see LICENSE.

Documentation

Overview

Package glua is a pure-Go Lua 5.4 for small embedded scripts: rules, policies, handlers and configuration. It needs no cgo and no assembly, and supports 64-bit platforms only.

Clients that prefer the shorter name can alias the import:

import lua "github.com/folbricht/glua"

The model

Three types carry it. A Runtime is the host's configuration: libraries, host globals, Go modules, bound Go types and limits. A Script is code loaded into a Runtime: its top level runs once, in Runtime.Load, and everything it built is then frozen. A VM is a private, mutable instance of a Script, for the rare script that must keep state between calls. Runtimes and Scripts never change after they are made, so any goroutine may use them at the same time.

rt := lua.MustNew(lua.Limits{Steps: 100_000})
script, err := rt.Load(ctx, "policy.lua", src)
allow, err := script.Func2[*Request, int64, bool]("allow")
ok, err := allow(ctx, req, 10) // safe from any goroutine

The functions Func0 to Func3 return plain Go functions backed by Lua functions. Their argument and result types are checked when they are bound, not when they are called, and a call allocates nothing in the library. Script.Get reads a frozen global without running anything, and Script.Call and Script.CallN call any function with Values.

Runtime.Expr compiles an expression over the fields of a Go struct into a Rule:

rule, err := rt.Expr[Order, bool]("gold", `total > 100 and customer.tier == "gold"`)
ok, err := rule.Eval(ctx, &order)

Free names are the struct's fields, then the Runtime's globals; a misspelt name is an error from Expr, not from Eval. In a chunk, fields tagged rw can be assigned and come back to Go. Eval runs a chunk on a default Runtime, for one-off use.

Freezing

After the top level runs, Load freezes every table reachable from the globals and from what the chunk returned, their metatables included, and every captured variable that closures can write. Writing one later raises an ordinary Lua error, "attempt to modify a frozen table". A call can still build and change tables of its own, which die with the call. Load fails if the state reaches something that cannot be shared: a coroutine, a library function with hidden state such as a gmatch iterator, a file, or a Go value a script can change, itself or through its fields: one of a type with writable fields or a __newindex metamethod, or that the host marked Mutable. State that must outlive a call belongs in a host function or in a VM.

Names that the host or the libraries define are reserved: a script cannot assign them, although it can shadow them with a local.

Host functions

GoFunc is the one native signature for Go functions called from Lua. Call gives a GoFunc its arguments, with Call.Arg and Call.Opt converting them, and takes its results with Call.Return. The typed adapters Fn1, Fn2E, Do1 and their relatives wrap plain Go functions without reflection; Reflect wraps any function by reflection, and is never applied implicitly. A first parameter of type context.Context receives the call's context and takes no Lua argument. Host functions see their arguments the Lua way: a numeric string is a number. Results going back to Go are strict.

Go values in Lua

Scalars and strings convert directly; time.Duration is a number of seconds. A pointer to a struct is a userdata whose fields Lua reads by their snake_case names, or the name a lua struct tag gives; fields are read-only unless tagged rw. Nested structs, slices and maps are live views of the Go value. Type adds methods and metamethods and renames, hides or opens fields; Convert gives a type a scalar or string form instead. Values, tables and GoFuncs pass through. ValueOf and Value.As convert by the same rules.

Limits

Every entry from Go runs under Limits: a step budget, an allocation budget, a call depth and a depth of nesting between Go and Lua. A zero field takes its default, so a Runtime made with no options is bounded. Running out of steps or bytes, nesting too deep and a cancelled context are terminal errors: pcall cannot catch them, and the entry returns them as an *Error that wraps ErrSteps, ErrAlloc, ErrHostDepth or the context's cause. A host function ends the entry the same way by returning an error made with Terminate. A set of fixed caps holds even with NoLimit. Hardened is the profile for untrusted scripts. An entry that runs long and waits on its host can have its budgets renewed after each wait, with Call.Renew.

The documents under docs/ in the repository describe the sandbox and limits in full, the ways glua differs from Lua 5.4, and how common tasks look in GopherLua and in glua.

Errors

A runtime error is an *Error: Error returns one line such as "policy.lua:4: boom", Error.Traceback the frames it passed through, and Unwrap the Go error behind it. A Go error returned by a host function is a string inside Lua; if that same string leaves Lua, errors.Is and errors.As still find the original, as they find the error of a module that require could not load. Code that does not compile gives a *SyntaxError, and a binding that cannot work a *BindError, before anything runs.

Example (Config)
package main

import (
	"context"
	"fmt"
	"slices"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

type Config struct {
	Port  int
	Hosts []string
}

func main() {
	rt := lua.MustNew()
	script, err := rt.Load(ctx, "config.lua", `config = { port = 8080, hosts = {"a", "b"} }
		count = 0
		function bump() count = count + 1 return count end`)
	if err != nil {
		panic(err)
	}
	cfg, err := script.Get[Config]("config") // decoded from the frozen globals; runs nothing
	fmt.Printf("%+v %v\n", cfg, err)

	t, _ := script.Get[*lua.Table]("config") // or walk it, from any goroutine
	port, _ := t.Field[int]("port")
	fmt.Println(port)
	var keys []string
	for k := range t.All() {
		keys = append(keys, k.String())
	}
	slices.Sort(keys)
	fmt.Println(keys)

	vm, err := script.NewVM(ctx) // runs the top level again in a private, unfrozen environment
	if err != nil {
		panic(err)
	}
	err = vm.Set("verbose", true) // state persists between calls; one goroutine at a time
	bump, _ := vm.Func0[int]("bump")
	bump(ctx)
	n, _ := bump(ctx)
	fmt.Println(err, n)

	shared, _ := script.Func0[int]("bump")
	_, err = shared(ctx)
	fmt.Println(err)
}
Output:
{Port:8080 Hosts:[a b]} <nil>
8080
[hosts port]
<nil> 2
config.lua:3: attempt to modify a frozen table
Example (Errors)

6. Errors.

package main

import (
	"context"
	"errors"
	"fmt"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

func main() {
	_, err := lua.Eval[int64](ctx, `local function check(n) if n > 1 then error("boom") end end
		check(2)`)
	if le, ok := errors.AsType[*lua.Error](err); ok {
		fmt.Println(le) // one line
		for _, f := range le.Traceback() {
			fmt.Printf("%s:%d: in %s\n", f.Source, f.Line, f.Function)
		}
	}

	rt := lua.MustNew(lua.Limits{Steps: 1000})
	_, err = rt.Load(ctx, "spin.lua", `while true do end`)
	if errors.Is(err, lua.ErrSteps) || errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("ran too long:", err)
	}

	_, err = rt.Load(ctx, "bad.lua", "x = = 1")
	if se, ok := errors.AsType[*lua.SyntaxError](err); ok {
		fmt.Println(se.Line, se.Msg)
	}
}
Output:
eval:1: boom
eval:1: in function 'check'
eval:2: in main chunk
ran too long: spin.lua:1: glua: step limit exceeded
1 unexpected symbol near '='
Example (Handlers)
package main

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

type Request struct {
	Method string
	Path   string
	hdr    map[string]string
}

func (r *Request) Header(name string) string { return r.hdr[name] }

const policySrc = `
local prefix = "/api/"
function allow(req, limit)
  if req.method ~= "GET" then return false end
  local cost = tonumber(req:header("x-cost")) or 0
  return req.path:sub(1, #prefix) == prefix and cost <= limit
end
`

func main() {
	rt, err := lua.New(
		lua.Limits{Steps: 100_000, AllocBytes: 1 << 20},
		lua.Global("env", "prod"),
		lua.Global("upper", lua.Fn1(strings.ToUpper)),
		lua.Type[Request](lua.Method("header", lua.Fn2((*Request).Header))),
	)
	if err != nil {
		panic(err)
	}
	script, err := rt.Load(ctx, "policy.lua", policySrc) // compiles, runs the top level once, freezes the result
	if err != nil {
		panic(err)
	}
	allow, err := script.Func2[*Request, int64, bool]("allow")
	if err != nil {
		panic(err)
	}

	reqs := []*Request{
		{Method: "GET", Path: "/api/users", hdr: map[string]string{"x-cost": "3"}},
		{Method: "GET", Path: "/api/report", hdr: map[string]string{"x-cost": "50"}},
		{Method: "POST", Path: "/api/users"},
		{Method: "GET", Path: "/static/app.js"},
	}
	results := make([]bool, len(reqs))
	var wg sync.WaitGroup
	for i, req := range reqs {
		wg.Go(func() {
			results[i], _ = allow(ctx, req, 10) // a plain func(context.Context, *Request, int64) (bool, error)
		})
	}
	wg.Wait()
	fmt.Println(results)

	// optional hooks can be probed
	if _, err := script.Func1[*Request, struct{}]("on_deny"); errors.Is(err, lua.ErrUndefined) {
		fmt.Println("no on_deny hook")
	}
}
Output:
[true false false false]
no on_deny hook
Example (HostFunctions)
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

type Request struct {
	Method string
	Path   string
	hdr    map[string]string
}

func (r *Request) Header(name string) string { return r.hdr[name] }

var store = map[string]string{"user:42": "ann"}

func lookup(ctx context.Context, key string) (string, error) {
	if v, ok := store[key]; ok {
		return v, nil
	}
	return "", errors.New("not found")
}

func main() {
	clamp := lua.GoFunc(func(c *lua.Call) error {
		x, lo := c.Arg[float64](0), c.Arg[float64](1)
		hi := c.Opt[float64](2, 1)
		return c.Return(min(max(x, lo), hi))
	})
	split := lua.GoFunc(func(c *lua.Call) error {
		s, sep := c.Arg[string](0), c.Arg[string](1)
		head, tail, _ := strings.Cut(s, sep)
		return c.Return2(head, tail)
	})
	each := lua.GoFunc(func(c *lua.Call) error {
		t := c.Arg[*lua.Table](0)
		for _, v := range t.Items() {
			if _, err := c.Invoke(c.Value(1), v); err != nil {
				return err
			}
		}
		return nil
	})
	rt := lua.MustNew(
		lua.Output(os.Stdout), // where print goes; the default discards it
		lua.Global("upper", lua.Fn1(strings.ToUpper)),
		lua.Global("clamp", clamp),
		lua.Global("log_warn", lua.Do1(func(msg string) { fmt.Println("warning:", msg) })),
		lua.Module("kv", lua.Members{
			"get":     lua.Fn2E(lookup), // func(context.Context, string) (string, error)
			"split":   split,
			"each":    each,
			"version": 3,
		}),
		lua.Type[Request](lua.Method("header", lua.Fn2((*Request).Header))),
	)
	script, err := rt.Load(ctx, "host.lua", `
		function run(req)
			print(upper("hello"))                     -- global
			print(clamp(1.7, 0))                      -- third argument left out, so c.Opt supplies 1
			print(kv.get("user:42"))                  -- module member; the Go side also receives the context
			local head, tail = kv.split("a=b", "=")   -- two results
			print(head, tail)
			print(req:header("x-cost"))               -- method, req goes in as the first argument
			print(pcall(kv.get, ""))                  -- a Go error is a Lua error, a string here
			local total = 0
			kv.each({1, 2, 3}, function(n) total = total + n end)   -- Go calls back into Lua
			print(total)
			print(require("kv") == kv, kv.version)    -- the same table as the global kv
			log_warn("done")
		end`)
	if err != nil {
		panic(err)
	}
	run, _ := script.Func1[*Request, struct{}]("run")
	if _, err := run(ctx, &Request{hdr: map[string]string{"x-cost": "7"}}); err != nil {
		fmt.Println(err)
	}
}
Output:
HELLO
1.0
ann
a	b
7
false	not found
6
true	3
warning: done
Example (OneLiner)

1. A one-liner.

package main

import (
	"context"
	"fmt"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

func main() {
	n, err := lua.Eval[int64](ctx, `return 6 * 7`)
	fmt.Println(n, err)
}
Output:
42 <nil>
Example (Rule)
package main

import (
	"context"
	"fmt"

	lua "github.com/folbricht/glua"
)

var ctx = context.Background()

type Customer struct {
	Tier string
}

type Order struct {
	Total    float64
	Items    int
	Customer *Customer
	Discount float64 `lua:"discount,rw"`
}

func main() {
	rt := lua.MustNew()
	rule, err := rt.Expr[Order, bool]("gold-discount", `total > 100 and customer.tier == "gold"`)
	if err != nil {
		panic(err)
	}
	order := Order{Total: 120, Customer: &Customer{Tier: "gold"}}
	ok, err := rule.Eval(ctx, &order) // safe from any goroutine; order stays on the caller's stack
	fmt.Println(ok, err)

	// a typo fails when the rule is made, not in production
	_, err = rt.Expr[Order, bool]("gold-discount", `totl > 100`)
	fmt.Println(err)

	// in a chunk, a field tagged rw is an output
	tiers, _ := rt.Expr[Order, struct{}]("tiers", `
		if total > 100 then discount = 0.1 end
		if items >= 10 then discount = discount + 0.05 end`)
	order = Order{Total: 150, Items: 12}
	tiers.Eval(ctx, &order)
	fmt.Printf("%.2f\n", order.Discount)
}
Output:
true <nil>
gold-discount:1: unknown name 'totl'
0.15

Index

Examples

Constants

View Source
const NoLimit = -1

NoLimit switches a limit off. The fixed caps still hold.

Variables

View Source
var (
	ErrSteps       = errors.New("glua: step limit exceeded")                      // Limits.Steps ran out
	ErrAlloc       = errors.New("glua: allocation limit exceeded")                // Limits.AllocBytes ran out, or one allocation was over 1 GiB
	ErrHostDepth   = errors.New("glua: too many nested calls between Go and Lua") // Limits.HostDepth was reached
	ErrUndefined   = errors.New("glua: name is not defined")                      // FuncN or Get named nothing
	ErrNotFunction = errors.New("glua: value is not a function")                  // FuncN, Call or CallN got something else
)

Errors that errors.Is finds. The first three are terminal: pcall cannot catch them, and an *Error that wraps them carries the script's position and traceback.

Functions

func Eval

func Eval[R any](ctx context.Context, src string) (R, error)

Eval loads src on a default Runtime (LibSafe, default limits) and decodes the chunk's first result, or, for a Pair, its first two.

func SetHashSeed

func SetHashSeed(seed uint64)

SetHashSeed fixes the process-wide hash seed, and with it the order of pairs and the values tostring shows in place of addresses, for tests and replay. It must be called before any other use of the package; afterwards it panics.

func Terminate added in v0.3.0

func Terminate(err error) error

Terminate wraps err so that a host function returning it ends the whole entry, as running out of steps does: pcall, xpcall and coroutine.resume do not see it, __close handlers run on the grace budget, and the entry returns an *Error that unwraps to err. It must be returned as it is: wrapped again, as by fmt.Errorf with %w, it is an ordinary error. It is for a host that finds the script must not go on, for a permission it lacks or a quota it spent. A terminal error that already stands wins. Terminate(nil) is nil.

Types

type BindError

type BindError struct {
	Name   string
	Type   reflect.Type
	Reason string
}

BindError is a binding that cannot work: a Go value or type that cannot be converted, a duplicate or reserved name, a missing field. New, Load, Expr and FuncN return it, before anything runs.

func (*BindError) Error

func (e *BindError) Error() string

Error names the binding, its type when known, and the reason.

type Call

type Call struct {
	// contains filtered or unexported fields
}

Call is the view a host function has of its invocation. It holds indices, never addresses, so it survives stack growth. It is valid only until the function returns, and only on the goroutine that received it.

func (*Call) Arg

func (c *Call) Arg[T any](i int) T

Arg returns argument i, counting from 0, converted to T. A missing or unconvertible argument raises "bad argument #1 to 'f' (number expected, got nil)", which pcall can catch. It unwinds by panic, so it must run on the goroutine that received c. c.Value(i).As[T]() is the form that returns an error instead.

func (*Call) ArgError added in v0.4.0

func (c *Call) ArgError(i int, msg string) error

ArgError returns the error for a bad argument i, counting from 0 as Arg does, as C Lua's luaL_argerror makes it: "bad argument #1 to 'f' (msg)", the function named as the calling code names it, or by its own name when Go code made the call, a method call not counting self, with the position Errorf gives.

func (*Call) Charge

func (c *Call) Charge(n int) error

Charge counts work a host function does for the script against the entry's budgets: n bytes against AllocBytes and a step per 64 of them, as library functions charge for what they make. Call it before allocating. Running out is a terminal error, which the function should return. A function that calls Charge is taken to charge for everything it makes, so its results are not charged again when it returns.

func (*Call) Context

func (c *Call) Context() context.Context

Context returns the context of the outermost entry from Go. It carries what is left of the entry's budgets: a Script called with it from the host function shares them.

func (*Call) Errorf

func (c *Call) Errorf(format string, args ...any) error

Errorf returns an error with a formatted message, for a host function to return. As C Lua's luaL_error does, the message Lua sees starts with the position of the Lua code that called the function ("policy.lua:4: no such user"), and has none when Go code, such as pcall, made the call. %w wraps an error as fmt.Errorf does, for errors.Is and errors.As, and the error may itself be wrapped: the position starts the whole message.

func (*Call) Invoke

func (c *Call) Invoke(fn Value, args ...Value) (Value, error)

Invoke calls fn with args from inside a host function and returns its first result. It shares the budgets of the outermost entry.

func (*Call) InvokeN

func (c *Call) InvokeN(dst []Value, fn Value, args ...Value) ([]Value, error)

InvokeN calls fn with args from inside a host function and appends its results to dst[:0]. It shares the budgets of the outermost entry.

func (*Call) Len

func (c *Call) Len() int

Len returns the number of arguments.

func (*Call) Opt

func (c *Call) Opt[T any](i int, def T) T

Opt returns argument i converted to T, or def if it is nil or missing.

func (*Call) Renew added in v0.3.0

func (c *Call) Renew()

Renew gives the entry its step and allocation budgets afresh, as they were when it started. It is for an entry that runs long and waits on its host, such as a session that reads input or a worker that waits for events: a host function that renews after each wait bounds the work between waits rather than over the entry's whole life. Renew only after a real wait: a script that loops on a function that renews at once is bounded by its context alone. A limit already exceeded stays exceeded. Renew does nothing in an entry made with another entry's Context, which shares that entry's budgets, nor in closers running after a terminal error.

func (*Call) Return

func (c *Call) Return[T any](v T) error

Return sets the function's single result. The last call wins. It reports only a value that cannot be converted, so `return c.Return(x)` reads well.

func (*Call) Return2

func (c *Call) Return2[T, U any](v T, u U) error

Return2 sets two results.

func (*Call) ReturnValues

func (c *Call) ReturnValues(vs ...Value) error

ReturnValues sets the function's results. The last call wins.

func (*Call) Value

func (c *Call) Value(i int) Value

Value returns argument i, counting from 0, or nil if there is no such argument.

type CallStats

type CallStats struct {
	Script, Func      string
	Steps, AllocBytes int64
	Err               error
}

CallStats is what one entry from Go used, as Observe reports it.

type Chunk added in v0.3.0

type Chunk struct {
	// contains filtered or unexported fields
}

Chunk is compiled Lua source, as Compile makes it; the zero Chunk is not one. It never changes, so one Chunk can serve any number of Runtimes and goroutines; each environment that requires it runs it afresh, and VM.Function makes it a function of a VM.

func Compile added in v0.3.0

func Compile(name, src string) (*Chunk, error)

Compile compiles src under name, which messages and tracebacks show, as Load does. Code that does not compile gives a *SyntaxError.

type Error

type Error struct {
	Value Value // what was raised: for VM errors a message with its position; a string for Go errors
	// contains filtered or unexported fields
}

Error is a runtime error raised by a script or by a host function it called.

func (*Error) Error

func (e *Error) Error() string

Error returns one line, such as "policy.lua:4: boom".

func (*Error) Traceback

func (e *Error) Traceback() []Frame

Traceback returns the frames the error passed through, innermost first. Lines are looked up only now, so scripts that use error for control flow do not pay for them.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the Go error behind e, if any.

type Frame

type Frame struct {
	Source   string // "" for Go code
	Line     int
	Function string // "main chunk", "function 'f'", "function <policy.lua:12>", or "Go function"
}

Frame is one level of a traceback.

type GoFunc

type GoFunc func(c *Call) error

GoFunc is the one native signature for host functions. Everything else adapts to it.

func Do0

func Do0(f func()) GoFunc

Do0 adapts func(), which returns nothing to Lua.

func Do0E

func Do0E(f func() error) GoFunc

Do0E adapts func() error.

func Do1

func Do1[A any](f func(A)) GoFunc

Do1 adapts func(A).

func Do1E

func Do1E[A any](f func(A) error) GoFunc

Do1E adapts func(A) error.

func Do2

func Do2[A, B any](f func(A, B)) GoFunc

Do2 adapts func(A, B).

func Do2E

func Do2E[A, B any](f func(A, B) error) GoFunc

Do2E adapts func(A, B) error.

func Do3

func Do3[A, B, C any](f func(A, B, C)) GoFunc

Do3 adapts func(A, B, C).

func Do3E

func Do3E[A, B, C any](f func(A, B, C) error) GoFunc

Do3E adapts func(A, B, C) error.

func Fn0

func Fn0[R any](f func() R) GoFunc

Fn0 adapts func() R.

func Fn0E

func Fn0E[R any](f func() (R, error)) GoFunc

Fn0E adapts func() (R, error); a non-nil error becomes a Lua error.

func Fn1

func Fn1[A, R any](f func(A) R) GoFunc

Fn1 adapts func(A) R.

func Fn1E

func Fn1E[A, R any](f func(A) (R, error)) GoFunc

Fn1E adapts func(A) (R, error).

func Fn2

func Fn2[A, B, R any](f func(A, B) R) GoFunc

Fn2 adapts func(A, B) R, such as a method expression (*T).M.

func Fn2E

func Fn2E[A, B, R any](f func(A, B) (R, error)) GoFunc

Fn2E adapts func(A, B) (R, error).

func Fn3

func Fn3[A, B, C, R any](f func(A, B, C) R) GoFunc

Fn3 adapts func(A, B, C) R.

func Fn3E

func Fn3E[A, B, C, R any](f func(A, B, C) (R, error)) GoFunc

Fn3E adapts func(A, B, C) (R, error).

func Reflect

func Reflect(fn any) GoFunc

Reflect adapts any Go function by reflection. Convenient and slow; it is never applied implicitly. A first context.Context parameter receives the call's context, a final error result becomes a Lua error, and a variadic function takes the remaining arguments. It panics if fn is not a function or has a type that cannot be converted.

type Kind

type Kind uint8

Kind is the type of a Value as Lua sees it.

const (
	KindNil      Kind = iota // nil
	KindBool                 // boolean
	KindInt                  // integer number
	KindFloat                // float number
	KindString               // string
	KindTable                // table
	KindFunction             // a Lua or Go function
	KindUserdata             // a Go pointer
	KindThread               // a coroutine
)

The kinds of Value. Lua's number type is split into KindInt and KindFloat.

func (Kind) String

func (k Kind) String() string

String returns the Lua type name, with integer and float told apart.

type Lib

type Lib uint

Lib options are OR-ed together. With none given, LibSafe is used.

const (
	LibBase      Lib = 1 << iota // the base library, without dofile and loadfile; also require and package
	LibString                    // string, with pack and unpack
	LibTable                     // table
	LibMath                      // math
	LibUTF8                      // utf8
	LibCoroutine                 // coroutine
	LibOS                        // os: clock, time, date and difftime only
	LibIO                        // io, for trusted scripts only
	LibLoad                      // load: compile text at run time

	LibNone Lib = 1 << 31                                            // an empty environment; with other flags, only those
	LibSafe     = LibBase | LibString | LibTable | LibMath | LibUTF8 // the default
)

func (Lib) String

func (l Lib) String() string

String returns the names of the flags in l, such as "LibLoad|LibCoroutine".

type Limits

type Limits struct {
	Steps          int64 // default 10M. Back-edges, calls, and library work in proportion to its size
	AllocBytes     int64 // default 64 MiB. Charged before each allocation made for the script
	CallDepth      int   // default 10,000 Lua frames. Catchable "stack overflow"
	HostDepth      int   // default 100 nested Go re-entries
	InitSteps      int64 // defaults to Steps. Used by Load and NewVM for the top level
	InitAllocBytes int64 // defaults to AllocBytes
}

Limits apply to one entry from Go, including everything it re-enters, until a host function renews them with Call.Renew. A zero field takes the default. NoLimit switches a limit off.

type Members

type Members map[string]any

Members holds the contents of a module: GoFuncs, scalars, strings, nested Members, pointers to bound types.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is implemented by Lib, Limits and the results of the functions below.

func AutoBind

func AutoBind(on bool) Option

AutoBind chooses whether Go types statically reachable from a binding are bound with the default rules. Without it, only types registered with Type are. It is on by default, and off under Hardened.

func ChunkLoader added in v0.3.0

func ChunkLoader(find func(ctx context.Context, name string) (*Chunk, error)) Option

ChunkLoader is Loader for a host that compiles modules itself: find receives the context of the entry that requires name, so it can choose the module by who asks, and returns it compiled, so it can name it in messages as it likes and give the same Chunk again rather than have each environment compile it. Like Loader, it runs from any goroutine that loads a Script or makes a VM, so it must be safe for concurrent use. An error from find says the module was not found, or did not compile when it is or wraps a *SyntaxError; so does a nil Chunk. A Chunk's source is not charged to the script, as a SourceModule's is not. Loader and ChunkLoader cannot both be given.

func Clock

func Clock(now func() time.Time) Option

Clock is the time source of the os library.

func Convert

func Convert[T any](enc func(T) Value, dec func(Value) (T, error)) Option

Convert gives T a scalar or string form instead of a userdata: netip.Addr, uuid.UUID, decimals. Conversions are process-wide, like type IDs; registering a second conversion for the same type fails.

func Global

func Global(name string, v any) Option

Global defines a global that every script of the Runtime sees, under a reserved name. Tables given here are frozen.

func Hardened

func Hardened() Option

Hardened is the profile for untrusted scripts: the bounds-checked loop, AutoBind(false), no LibLoad, LibCoroutine, LibOS or LibIO, no NoLimit, and maps and slices passed by copy.

func Loader

func Loader(find func(name string) (src string, err error)) Option

Loader gives require the source of modules not defined by Module or SourceModule. It runs only while a script loads, or in a VM. An error it returns, or the *SyntaxError of its source, is behind the error of require, for errors.Is and errors.As.

func Module

func Module(name string, members Members) Option

Module defines a module of Go members. It is a global under its name, and require returns the same table.

func Observe

func Observe(f func(CallStats)) Option

Observe is called after every entry from Go with what it used.

func Output

func Output(w io.Writer) Option

Output is where print and warn write; the default discards. Writes are serialised, so w need not be safe for concurrent use. Of Output and OutputFunc, the last given wins.

func OutputFunc added in v0.3.0

func OutputFunc(f func(ctx context.Context, s string) error) Option

OutputFunc is Output for a host that needs to know which entry wrote: f receives what Output's writer would, one call for each print, warn or io.write, with the entry's context, so it can tag the text with the request or tenant behind it. Calls are not serialised: f must be safe for concurrent use. An error it returns is raised in the script, as a write error is.

func RandSeed

func RandSeed(seed uint64) Option

RandSeed seeds math.random at the start of every entry, for reproducible runs. It also makes math.randomseed() without arguments reproducible: it takes its seed from the generator.

func RecoverPanics

func RecoverPanics() Option

RecoverPanics turns a panic in a host function into a *PanicError returned by the entry, instead of letting it continue.

func SourceModule

func SourceModule(name, src string) Option

SourceModule defines a module written in Lua, which require runs in the environment that loads it.

func StrictGlobals

func StrictGlobals() Option

StrictGlobals makes reading an undefined global an error, instead of nil.

func Type

func Type[T any](opts ...TypeOption) Option

Type customises how *T appears in Lua: methods, metamethods, and which fields are visible, writable and under what name. With AutoBind(false), only types registered here are bound.

func Without added in v0.3.0

func Without(names ...string) Option

Without leaves out names the libraries define: globals such as "collectgarbage" or "require", or library members such as "string.rep". A global left out is free, so the host may define its own under it with Global or Module, and a script may assign it. Strings' methods are the string library's, so leaving out "string.rep" takes s:rep too, while leaving out "string" takes only the global. Naming something the libraries do not define is an error from New.

type Pair

type Pair[A, B any] struct {
	First  A
	Second B
}

Pair as a result type decodes the first two results, as in the idiom return nil, "message".

type PanicError

type PanicError struct {
	Value any
	Stack []byte
}

PanicError is a panic recovered at the entry: one inside the VM itself, which the checked loop of Hardened turns bugs into, or, with RecoverPanics, one in a host function.

func (*PanicError) Error

func (e *PanicError) Error() string

Error returns "glua: panic: " and the panic value.

type Rule

type Rule[E, R any] struct {
	// contains filtered or unexported fields
}

Rule is a compiled Expr. It is a concrete generic type with a method, so a call to Eval is static and escape analysis can see that env does not leak: a func value would force every pointer argument to the heap. Share freely.

func (*Rule[E, R]) Eval

func (ru *Rule[E, R]) Eval(ctx context.Context, env *E) (r R, err error)

Eval copies *env into a slot of the borrowed executor, runs, and on success copies it back, which carries the writable fields out: the others are read-only to the script. env itself does not escape, so a caller-local struct stays on the stack. What its pointers and strings refer to is copied to the heap slot and must live on the heap.

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime is the host side of the sandbox: libraries, globals, modules, bound Go types and limits. It never changes after New, so it is safe to share between goroutines.

func MustNew

func MustNew(opts ...Option) *Runtime

MustNew is New for package-level variables. It panics on error.

func New

func New(opts ...Option) (*Runtime, error)

New makes a Runtime. It reports bad options: a value that cannot be converted, a duplicate name, a syntax error in a source module, both Loader and ChunkLoader.

func (*Runtime) Expr

func (rt *Runtime) Expr[E, R any](name, src string) (*Rule[E, R], error)

Expr compiles an expression, or a chunk, whose free names are the fields of E and then the Runtime's globals. The source is parsed as `return <src>` first, then as a chunk; in a chunk, fields that are writable can be assigned. Unknown names, assignments to read-only fields or to globals, and a field that shadows a global are errors here, not at run time.

func (*Runtime) Load

func (rt *Runtime) Load(ctx context.Context, name, src string) (*Script, error)

Load compiles src, runs its top level once under the init limits, then freezes everything it built: the globals, what the chunk returned, and the modules it required.

type Script

type Script struct {
	// contains filtered or unexported fields
}

Script is a loaded script: verified code plus the frozen state its top level built. Share freely.

func (*Script) Call

func (s *Script) Call(ctx context.Context, fn Value, args ...Value) (Value, error)

Call calls fn, which must be a function of the same Runtime, and returns its first result.

func (*Script) CallN

func (s *Script) CallN(ctx context.Context, dst []Value, fn Value, args ...Value) ([]Value, error)

CallN calls fn and appends its results to dst[:0].

func (*Script) Func0

func (s *Script) Func0[R any](name string) (func(context.Context) (R, error), error)

Func0 returns a Go function backed by the Lua function name: a global, a dotted path ("handlers.allow"), or "" for the function the chunk returned. Safe for concurrent use.

func (*Script) Func1

func (s *Script) Func1[A, R any](name string) (func(context.Context, A) (R, error), error)

Func1 is Func0 for a function of one argument.

func (*Script) Func2

func (s *Script) Func2[A, B, R any](name string) (func(context.Context, A, B) (R, error), error)

Func2 is Func0 for a function of two arguments.

func (*Script) Func3

func (s *Script) Func3[A, B, C, R any](name string) (func(context.Context, A, B, C) (R, error), error)

Func3 is Func0 for a function of three arguments.

func (*Script) Get

func (s *Script) Get[T any](name string) (T, error)

Get reads a global, or a dotted path, from the frozen globals and converts it to T. It runs nothing.

func (*Script) Globals

func (s *Script) Globals() *Table

Globals returns the frozen globals table.

func (*Script) NewVM

func (s *Script) NewVM(ctx context.Context) (*VM, error)

NewVM runs the top level again in a private environment that is not frozen, under the init limits. Lua modules it requires run again too, and host calls the top level makes are repeated.

func (*Script) Result

func (s *Script) Result() Value

Result returns the first value the chunk returned.

func (*Script) WithLimits

func (s *Script) WithLimits(l Limits) (*Script, error)

WithLimits returns the same code and state under other limits. It fails if the profile refuses them.

type SyntaxError

type SyntaxError struct {
	Source    string
	Line, Col int
	Msg       string
}

SyntaxError is a chunk that does not compile.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Error returns "source:line: message", as C Lua reports a syntax error.

type Table

type Table struct {
	// contains filtered or unexported fields
}

Table is a Lua table: an array part for the keys 1..n, which may hold nils, and an open-addressing hash part of our own. Go maps are not used: they rehash the key on every access and cannot support next() without side tables. Table methods are raw: they never run metamethods.

func NewTable

func NewTable(narr, nhash int) *Table

NewTable returns a table with room for narr array items and nhash other keys.

func (*Table) All

func (t *Table) All() iter.Seq2[Value, Value]

All iterates over every key and value of t, raw, in traversal order.

func (*Table) Field

func (t *Table) Field[T any](name string) (T, error)

Field returns t[name], raw, converted to T.

func (*Table) Freeze

func (t *Table) Freeze() error

Freeze makes t and everything reachable from it read-only, so that any goroutine may read it. It fails, freezing nothing, if t reaches a coroutine, a library function with hidden state, such as a gmatch iterator, or a Go value a script can change.

func (*Table) Frozen

func (t *Table) Frozen() bool

Frozen reports whether t is read-only.

func (*Table) Get

func (t *Table) Get(key Value) Value

Get returns t[key] without metamethods. A nil or NaN key reads as nil.

func (*Table) Items

func (t *Table) Items() iter.Seq2[int, Value]

Items iterates over t[1], t[2], ... up to the first nil, raw.

func (*Table) Len

func (t *Table) Len() int

Len returns a border of t, as C Lua's luaH_getn does: an n such that t[n] is not nil and t[n+1] is nil, or 0 if t[1] is nil. It does not run metamethods.

func (*Table) Next

func (t *Table) Next(key Value) (k, v Value, ok bool)

Next returns the key and value after key in traversal order: the array part, then the hash part by node index. A nil key starts the traversal; a nil result key ends it. Setting existing fields, also to nil, during a traversal is allowed. ok is false for a key that is not in the table.

func (*Table) Set

func (t *Table) Set(key, val Value)

Set assigns t[key] = val without metamethods. It panics if t is frozen, or if key is nil or NaN. A short string key that is a name is interned, so that Lua code finds it inline.

func (*Table) SetField

func (t *Table) SetField[T any](name string, v T)

SetField sets t[name] = v, raw, converting v with ValueOf. It panics if t is frozen or v cannot be converted.

type TypeOption

type TypeOption interface {
	// contains filtered or unexported methods
}

TypeOption customises how a Go type appears in Lua, in Type.

func Hide

func Hide(fields ...string) TypeOption

Hide makes the named Go fields invisible to Lua.

func Meta

func Meta(event string, f GoFunc) TypeOption

Meta adds a metamethod, such as "__eq", "__lt", "__tostring" or "__len".

func Method

func Method(name string, f GoFunc) TypeOption

Method adds a method: obj:name(...) calls f with obj as the first argument.

func Mutable added in v0.4.2

func Mutable() TypeOption

Mutable marks a type whose methods or metamethods change its values, such as a __call that counts, so that a shared script cannot hold one. glua cannot tell a function that writes from one that reads. A type with a __newindex metamethod is mutable without it.

func Rename

func Rename(field, luaName string) TypeOption

Rename gives the Go field a Lua name of its own.

func Writable

func Writable(fields ...string) TypeOption

Writable makes the named Go fields writable from Lua.

type VM

type VM struct {
	// contains filtered or unexported fields
}

VM is a private, mutable instance of a Script: its top level run again in an environment of its own, which is not frozen, so state persists between calls. The Runtime's layer, the libraries, host globals and Go modules, is still shared and frozen. One goroutine at a time; values from a VM are mutable and follow that rule wherever they go.

func (*VM) Call

func (vm *VM) Call(ctx context.Context, fn Value, args ...Value) (Value, error)

Call calls fn on this VM and returns its first result.

func (*VM) CallN

func (vm *VM) CallN(ctx context.Context, dst []Value, fn Value, args ...Value) ([]Value, error)

CallN calls fn on this VM and appends its results to dst[:0].

func (*VM) Func0

func (vm *VM) Func0[R any](name string) (func(context.Context) (R, error), error)

Func0 returns a Go function backed by the Lua function name, as Script.Func0 does, running on this VM: not safe for concurrent use. A struct passed by value is copied to the heap.

func (*VM) Func1

func (vm *VM) Func1[A, R any](name string) (func(context.Context, A) (R, error), error)

Func1 is Func0 for a function of one argument.

func (*VM) Func2

func (vm *VM) Func2[A, B, R any](name string) (func(context.Context, A, B) (R, error), error)

Func2 is Func0 for a function of two arguments.

func (*VM) Func3

func (vm *VM) Func3[A, B, C, R any](name string) (func(context.Context, A, B, C) (R, error), error)

Func3 is Func0 for a function of three arguments.

func (*VM) Function added in v0.4.0

func (vm *VM) Function(ch *Chunk) Value

Function returns ch as a function of this VM, as Lua's load gives one: a Value for Call or CallN, which runs ch's top level in the VM's environment, with the call's arguments as its ..., and returns what the chunk returns. Unlike require, each call runs the chunk again. Its _ENV is the function's, as with load: a chunk that assigns it changes it for later calls. A nil or zero Chunk gives nil.

func (*VM) Get

func (vm *VM) Get[T any](name string) (T, error)

Get reads a global, or a dotted path, and converts it to T.

func (*VM) Globals

func (vm *VM) Globals() *Table

Globals returns the VM's globals table, which is not frozen.

func (*VM) Set

func (vm *VM) Set[T any](name string, v T) error

Set assigns a global, or a field at a dotted path, converted by the general rules. Names the host or the libraries define are reserved.

type Value

type Value struct {
	// contains filtered or unexported fields
}

Value is any Lua value. It is 16 bytes, the size of a Go interface, and is passed by value. The zero Value is nil. Values cannot be compared with == or used as map keys; use Equal.

Scalars are identified by p, objects by n:

nil       p == nil                n == 0
boolean   p == &tags[1]           n is 0 or 1
integer   p == &tags[2]           n holds the int64 bits
float     p == &tags[3]           n holds the float64 bits
string    first byte, or emptyStr n is kindString<<56 | length
table     *Table                  n is kindTable<<56
function  *closure or *goFunc     n is kindLuaFn<<56 or kindGoFn<<56
thread    *coroutine              n is kindThread<<56
userdata  the Go pointer itself   n is kindUser<<56 | flags<<48 | type ID

Every constructor keeps four invariants, which checkValue asserts:

  1. p == nil exactly when the Value is {nil, 0}.
  2. Every empty string is {&emptyStr[0], kindString<<56}.
  3. p points inside its allocation, never one past the end.
  4. Type IDs are process-wide and never reused.

func Bool

func Bool(b bool) Value

Bool returns a Lua boolean.

func Float

func Float(f float64) Value

Float returns a Lua float.

func Int

func Int(i int64) Value

Int returns a Lua integer.

func String

func String(s string) Value

String returns a Lua string. It does not copy s.

func ValueOf

func ValueOf[T any](v T) Value

ValueOf converts v to a Value by the built-in rules and registered conversions. It panics on a type that cannot be converted, which is a programming error.

func (Value) As

func (v Value) As[T any]() (T, error)

As converts v to T by the built-in rules and registered conversions.

func (Value) Equal

func (v Value) Equal(w Value) bool

Equal reports whether v and w are equal in Lua without metamethods (rawequal). 1 equals 1.0, 0.0 equals -0.0, and NaN equals nothing.

func (Value) Float

func (v Value) Float() (float64, bool)

Float returns v as a float if v is a number. Strings are not converted.

func (Value) Int

func (v Value) Int() (int64, bool)

Int returns v as an integer if v is an integer, or a float with an exact integer value. Strings are not converted.

func (Value) IsNil

func (v Value) IsNil() bool

IsNil reports whether v is nil.

func (Value) Kind

func (v Value) Kind() Kind

Kind returns the type of v.

func (Value) Str

func (v Value) Str() (string, bool)

Str returns the contents of v if v is a string. Numbers are not converted. The result shares memory with v and must not be modified through unsafe means.

func (Value) String

func (v Value) String() string

String returns what Lua's tostring would return, without running metamethods. Objects show a keyed hash in place of their address, so no heap address is ever revealed.

func (Value) Truthy

func (v Value) Truthy() bool

Truthy reports whether v counts as true in a Lua condition: anything but nil and false.

Directories

Path Synopsis
bench
asmgate command
Command asmgate checks the machine code of the interpreter's hot loop, (*executor).fast.
Command asmgate checks the machine code of the interpreter's hot loop, (*executor).fast.
compare command
Command compare times the spike's VM benchmarks against C Lua in interleaved rounds and reports medians, spread and ratios as a Markdown table.
Command compare times the spike's VM benchmarks against C Lua in interleaved rounds and reports medians, spread and ratios as a Markdown table.
internal
cmd/genloop command
Command genloop generates the checked interpreter loop, checked_gen.go, from the fast loop in fast.go, so that the two can never drift apart.
Command genloop generates the checked interpreter loop, checked_gen.go, from the fast loop in fast.go, so that the two can never drift apart.
syntax
Package syntax turns Lua 5.4 source text into an abstract syntax tree.
Package syntax turns Lua 5.4 source text into an abstract syntax tree.
Package luajson converts between glua values and JSON, straight from tables to JSON text and back without an intermediate tree, and gives scripts a json module.
Package luajson converts between glua values and JSON, straight from tables to JSON text and back without an intermediate tree, and gives scripts a json module.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL