Help us shape the future of Go! Take the 2026 Go Developer Survey

glua

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: BSD-3-Clause Imports: 24 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.

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[float64](0), c.Arg[float64](1), c.Arg[float64](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.0
	function shout(s) return upper(s) .. "!" end`)

shout, err := script.Func1[string, string]("shout")
out, err := shout(ctx, "hi") // "HI!"

A Go error returned by a host function becomes a Lua error, which the script can catch with pcall.

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. The faster, unchecked interpreter loop becomes the default after a week of continuous fuzzing; until then every Runtime runs the bounds-checked loop.

Documentation

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 whose type has writable fields. 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 set of fixed caps holds even with NoLimit. Hardened is the profile for untrusted scripts.

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. 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.

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) 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.

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.

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) 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 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. 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 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.

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.

func RandSeed

func RandSeed(seed uint64) Option

RandSeed seeds math.random at the start of every entry, for reproducible runs.

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.

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 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.

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 with writable fields.

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 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) 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