Packages

lua

Embed Lua 5.1 in an OSL program, from one-off expressions to long-lived interpreters that share data and functions with your code.

import "std:lua"

std:lua runs Lua 5.1 code (with gopher-lua, a Lua interpreter written in Go). Use it to give your program a scripting language for rules, configuration, plugins or game logic that people can change without recompiling. You can run a snippet and take its value, or keep an interpreter alive, share data with it and call functions in both directions.

One-off code#

lua.eval(code) runs code in a fresh interpreter and returns a result holding its value. The value is that of a single expression, or what a chunk returns:

main.osl
import "std:lua"

log lua.eval("1 + 2").unwrap()
log lua.eval("local x = 2 return x * 3").unwrap()
log lua.version()
Click the code to edit it

Lua tables convert to OSL values: a table with keys 1 to n becomes an array, any other table an object.

main.osl
import "std:lua"

log lua.eval("{1, 2, 3}").unwrap()
log lua.eval("{name = 'Ada', n = 3}").unwrap()
Click the code to edit it

lua.run(code) runs code for its effects and returns nothing, and lua.evalFile(path) runs a file.

Failures are results#

Every call returns a result. The error kinds are ParseError for a syntax error, RuntimeError for an error the code raises, TimeoutError when it runs too long, and ValueError for source over 16 MiB:

main.osl
import "std:lua"

auto syntax = lua.run("x = ")
log syntax.unwrapErr().kind

auto raised = lua.run("error('boom')")
log raised.unwrapErr().kind
log raised.unwrapErr().message

auto endless = lua.run("while true do end", {timeout: 100})
log endless.unwrapErr().kind
Click the code to edit it

The default timeout is 5000 milliseconds. Set it per call, or for a whole interpreter in lua.new.

An interpreter that keeps its state#

lua.new() starts an interpreter whose globals survive between calls. state.run defines things, state.eval evaluates, state.get reads a global, and state.call(name, args...) calls a Lua function with each argument as its own value and returns its first result:

main.osl
import "std:lua"

*lua.State state = lua.new()
state.run("count = 0 function add(a, b) count = count + 1 return a + b end").unwrap()

log state.call("add", 2, 3).unwrap()
log state.call("add", 10, 5).unwrap()
log state.get("count")

log state.call("missing").unwrapErr().kind
state.close()
Click the code to edit it

Close a state when you're done. Using a closed state throws a StateError.

Sending data in#

state.set(name, value) sets a global to a copy of an OSL value: arrays become tables with keys 1 to n, and objects become tables with string keys. Changes Lua makes to its copy don't reach your variable:

main.osl
import "std:lua"

*lua.State state = lua.new()
state.set("items", [10, 20, 30])
state.set("config", {mode: "fast", limit: 5})

log state.eval("#items").unwrap()
log state.eval("config.mode").unwrap()

state.run("total = 0 for _, n in ipairs(items) do total = total + n end").unwrap()
log state.get("total")
state.close()
Click the code to edit it

Lua arrays are 1-based like OSL's, so items[1] is 10 in both.

Letting Lua call your functions#

state.register(name, fn) makes an OSL function callable from Lua. Arguments and return values are converted both ways. This is how a script reaches the rest of your program, and the way to limit what it can do: offer only the functions you choose.

main.osl
import "std:lua"

def discount(number price, number percent) number -> (price * (100 - percent)) / 100

*lua.State state = lua.new()
state.register("discount", discount)

log state.eval("discount(80, 25)").unwrap()
state.run("function price(base) return discount(base, 10) end").unwrap()
log state.call("price", 200).unwrap()
state.close()
Click the code to edit it

An error that a registered function throws becomes a Lua error. Lua's pcall can catch it, and otherwise it fails the run, so the caller sees it as an error result:

main.osl
import "std:lua"

*lua.State state = lua.new()
state.register("fail", def() ( throw "nope" ))

auto outcome = state.run("fail()")
log outcome.isErr()

log state.eval("select(2, pcall(fail))").unwrap()
state.close()
Click the code to edit it

A rule engine#

Rules written in Lua (kept in a string here; a real program would read them with fs.readFile) applied to your data one at a time:

main.osl
import "std:lua"

string rules = `
function shipping(order)
  if order.total >= 100 then return 0 end
  if order.country == "NZ" then return 5 end
  return 12
end
`

*lua.State state = lua.new()
state.run(rules).unwrap()

auto orders = [
  {total: 120, country: "NZ"},
  {total: 40, country: "NZ"},
  {total: 40, country: "AU"}
]
for order in orders (
  log state.call("shipping", order).unwrap()
)
state.close()
Click the code to edit it
Quick check

Lua code runs with lua.eval. What can it do?

Your turn

Call a Lua function

Define a Lua function square and call it with 7 from OSL.

main.osl
Ctrl Enter runs
Expected output
49

API reference#

29 members, generated from the std:lua source of OSL 9.3.0.

Functions

#lua.create() *lua.State

Deprecated. Deprecated: use lua.new(options) instead.

create starts a Lua interpreter.

#lua.doString(any code) *lua.State

Deprecated. Deprecated: use lua.new() and state.run(code), which reports failure.

doString starts an interpreter and runs code in it, returning the state even when the code failed.

#lua.eval(string code, object? options) result<any, error>

eval runs Lua code in a fresh interpreter and returns its value: the value of code that is a single expression, as in lua.eval("1 + 2"), or what a chunk returns, as in "local x = 2 return x * 3", or null. Code that returns several values gives the last one. Tables with keys 1 to n become arrays and other tables objects. Option and errors as for lua.run.

#lua.evalFile(string path, object? options) result<any, error>

evalFile runs a Lua file in a fresh interpreter and returns what it returns, or null. Option: timeout in milliseconds (default 5000). Fails with NotFoundError or IOError when the file can't be read, and otherwise like lua.run.

#lua.get(any code, any name) any

Deprecated. Deprecated: run the code in a lua.new() state with state.run(code), then read state.get(name).

get runs code and returns the global name afterwards, or null when the code fails.

#lua.new(object? options) *lua.State

new starts a Lua interpreter that keeps its globals between calls. The code it runs has the whole Lua standard library, including os and io, so don't give it untrusted code; std:js runs JavaScript in a sandbox. Option: timeout, the default limit in milliseconds for each run, eval and call (5000). Close the State when done with it.

#lua.newTable() any

Deprecated. Deprecated: use {} instead.

newTable returns an empty object.

#lua.run(string code, object? options) result<unit, error>

run runs Lua code in a fresh interpreter. Option: timeout in milliseconds (default 5000). Fails with ParseError for a syntax error, TimeoutError when the code runs too long, ValueError for source over 16 MiB and RuntimeError when the code raises an error.

#lua.runFile(any path) object

Deprecated. Deprecated: use lua.evalFile(path), which fails with a typed error.

runFile runs a Lua file and returns {success, error}.

#lua.runTimeout(any code, any timeout) result

Deprecated. Deprecated: use lua.run(code, {timeout: ms}) instead; note that the timeout there is in milliseconds.

runTimeout runs code with a timeout in seconds.

#lua.version() string

version returns the Lua version the interpreter implements.

*lua.State

#state.call(string name, any ...args) result<any, error>

call calls the global Lua function name with args and returns its first result, or null. Uses the state's timeout. Fails with NotFoundError when name isn't a function, and otherwise like state.run. Throws StateError after close.

#state.callTimeout(any funcName, any timeout, any ...args) any

Deprecated. Deprecated: use state.call(name, args...) with the timeout set by lua.new({timeout: ms}).

callTimeout calls a global Lua function with a timeout in seconds, returning null when it fails.

#state.close()

close shuts the interpreter down. Closing twice does nothing; any other method throws StateError afterwards.

#state.doFile(any path) boolean

Deprecated. Deprecated: use state.runFile(path) instead, which fails with a typed error.

doFile runs a Lua file, returning false when it fails or the state is closed.

#state.doString(any code) boolean

Deprecated. Deprecated: use state.run(code) instead, which fails with a typed error.

doString runs code, returning false when it fails or the state is closed.

#state.doStringTimeout(any code, any timeout) boolean

Deprecated. Deprecated: use state.run(code, {timeout: ms}) instead; note that the timeout there is in milliseconds.

doStringTimeout runs code with a timeout in seconds, returning false when it fails or the state is closed.

#state.eval(string code, object? options) result<any, error>

eval runs Lua code in this state and returns its value, like lua.eval. Throws StateError after close.

#state.evalFile(string path, object? options) result<any, error>

evalFile runs a Lua file in this state and returns what it returns, or null. Fails like state.runFile. Throws StateError after close.

#state.get(string name) any

get returns the value of a global, converted to OSL, or null when it is nil. Throws StateError after close.

#state.getError() string

Deprecated. Deprecated: the new methods return their error, as in state.run(code) catch (err) ( ... ).

getError returns the error of the last failed deprecated doString, doStringTimeout, doFile, callTimeout, loadString or loadFile, "" after one that succeeded, or "state not initialized" once closed.

#state.getGlobal(any name) any

Deprecated. Deprecated: use state.get(name) instead.

getGlobal returns a global's value, or null.

#state.loadFile(any path) boolean

Deprecated. Deprecated: use state.runFile(path), which fails with a typed error.

loadFile reports whether a Lua file can be read and compiles, without running it.

#state.loadString(any code) boolean

Deprecated. Deprecated: use state.run(code), which fails with ParseError for a syntax error.

loadString reports whether code compiles, without running it.

#state.register(string name, any fn)

register makes fn callable from Lua as the global function name. Lua arguments are converted to OSL values and fn's result back to Lua; an error fn throws becomes a Lua error, which Lua's pcall can catch and which otherwise fails the run with the error's own kind. Throws StateError after close.

#state.run(string code, object? options) result<unit, error>

run runs Lua code in this state, so the globals it sets stay for later calls. Option: timeout in milliseconds, by default the state's. Fails with ParseError, TimeoutError, ValueError or RuntimeError like lua.run; an error thrown by a registered OSL function keeps its kind. Throws StateError after close.

#state.runFile(string path, object? options) result<unit, error>

runFile runs a Lua file in this state. Option: timeout in milliseconds, by default the state's. Fails with NotFoundError or IOError when the file can't be read, and otherwise like state.run. Throws StateError after close.

#state.set(string name, any value)

set sets a global to a copy of value: arrays become tables with keys 1 to n and objects tables with string keys. Throws StateError after close.

#state.setGlobal(any name, any value)

Deprecated. Deprecated: use state.set(name, value) instead.

setGlobal sets a global's value.