Lua (Lua v0.3.0)

View Source

Lua is an ergonomic interface to Luerl, aiming to be the best way to use Luerl from Elixir.

Features

  • ~LUA sigil for validating Lua code at compile-time
  • deflua macro for exposing Elixir functions to Lua
  • Improved error messages and sandboxing
  • Deep setting/getting variables and state
  • Excellent documentation and guides for working with Luerl

Lua the Elixir library vs Lua the language

When referring to this library, Lua will be stylized as a link.

References to Lua the language will be in plaintext and not linked.

Executing Lua

Lua can be run using the eval!/2 function

iex> {[4], _} = Lua.eval!("return 2 + 2")

Compile-time validation

Use the ~LUA sigil to parse and validate your Lua code at compile time

iex> import Lua, only: [sigil_LUA: 2]

#iex> {[4], _} = Lua.eval!(~LUA[return 2 +])
** (Lua.CompilerException) Failed to compile Lua!

Using the c modifier transforms your Lua code into a Lua.Chunk.t/0 at compile-time, which will speed up execution at runtime since the Lua no longer needs to be parsed

iex> import Lua, only: [sigil_LUA: 2]
iex> {[4], _} = Lua.eval!(~LUA[return 2 + 2]c)

Exposing Elixir functions to Lua

The simplest way to expose an Elixir function to Lua is using the Lua.set!/3 function

import Lua, only: [sigil_LUA: 2]

lua = 
  Lua.set!(Lua.new(), [:sum], fn args ->
    [Enum.sum(args)]
  end)

{[10], _} = Lua.eval!(lua, ~LUA[return sum(1, 2, 3, 4)]c)

For easily expressing APIs, Lua provides the deflua macro for exposing Elixir functions to Lua

defmodule MyAPI do
  use Lua.API
      
  deflua double(v), do: 2 * v
end

import Lua, only: [sigil_LUA: 2]
    
lua = Lua.new() |> Lua.load_api(MyAPI)

{[10], _} = Lua.eval!(lua, ~LUA[return double(5)])

Calling Lua functions from Elixir

Lua can be used to expose complex functions written in Elixir. In some cases, you may want to call Lua functions from Elixir. This can be achieved with the Lua.call_function!/3 function

defmodule MyAPI do
  use Lua.API, scope: "example"

  deflua foo(value), state do
    Lua.call_function!(state, [:string, :lower], [value])
  end
end

import Lua, only: [sigil_LUA: 2]

lua = Lua.new() |> Lua.load_api(MyAPI)

{["wow"], _} = Lua.eval!(lua, ~LUA[return example.foo("WOW")])

Modify Lua state from Elixir

You can also use Lua to modify the state of the lua environment inside your Elixir code. Imagine you have a queue module that you want to implement in Elixir, with the queue stored in a global variable

defmodule Queue do
  use Lua.API, scope: "q"
  
  deflua push(v), state do
    # Pull out the global variable "my_queue" from lua
    queue = Lua.get!(state, [:my_queue])
    
    # Call the Lua function table.insert(table, value)
    {[], state} = Lua.call_function!(state, [:table, :insert], [queue, v])
    
    # Return the modified lua state with no return values
    {[], state}
  end
end

import Lua, only: [sigil_LUA: 2]

lua = Lua.new() |> Lua.load_api(Queue)

{[queue], _} =
  Lua.eval!(lua, """
  my_queue = {}

  q.push("first")
  q.push("second")

  return my_queue
  """)
  
["first", "second"] = Lua.Table.as_list(queue)

Accessing private state from Elixir

When building applications with Lua, you may find yourself in need of propagating extra context for use in your APIs. For instance, you may want to access information about the current user who executed the Lua script, an API key, or something else that is private and should not be available to the Lua code. For this, we have the Lua.put_private/3, Lua.get_private/2, and Lua.delete_private/2 functions.

For example, imagine you wanted to allow the user to access information about themselves

defmodule User do
  defstruct [:name]
end

defmodule UserAPI do
  use Lua.API, scope: "user"
  
  deflua name(), state do
    user = Lua.get_private!(state, :user) 
    
    {[user.name], state}
  end
end

user = %User{name: "Robert Virding"}

lua = Lua.new() |> Lua.put_private(:user, user) |> Lua.load_api(UserAPI)

{["Hello Robert Virding"], _lua} = Lua.eval!(lua, ~LUA"""
  return "Hello " .. user.name()
""")

This allows you to have simple, expressive APIs that access context that is unavailable to the Lua code.

Encoding and Decoding data

When working with Lua, you may want inject data of various types into the runtime. Some values, such as integers, have the same representation inside of the runtime as they do in Elixir, they do not require encoding. Other values, such as maps, are represented inside of Lua as tables, and must be encoded first. Values not listed are not valid and cannot be encoded by Lua and Luerl, however, they can be passed using a {:userdata, any()} tuple and encoding them.

Values may be encoded with Lua.encode!/2

Elixir typeLuerl typeRequires encoding?
nilnilno
boolean()boolean()no
number()number()no
binary()binary()no
atom()binary()yes
map():luerl.tref()yes
{:userdata, any()}:luerl.usdref()yes
(any()) -> any():luerl.erl_func()yes
(any(), Lua.t()) -> any():luerl.erl_func()yes
{module(), atom(), list():luerl.erl_mfa()yes
list(any())list(luerl type)maybe (if any of its values require encoding)

Userdata

There are situations where you want to pass around a reference to some Elixir datastructure, such as a struct. In these situations, you can use a {:userdata, any()} tuple.

defmodule Thing do
  defstruct [:value]
end

{encoded, lua} = Lua.encode!(Lua.new(), {:userdata, %Thing{value: "1234"}})

lua = Lua.set!(lua, [:foo], encoded)

{[{:userdata, %Thing{value: "1234"}}], _} = Lua.eval!(lua, "return foo")

Trying to deference userdata inside a Lua program will result in an exception.

Credits

Lua piggy-backs off of Robert Virding's Luerl project, which implements a Lua lexer, parser, and full-blown Lua virtual machine that runs inside the BEAM.

Summary

Functions

Calls a function in Lua's state

Decodes a Lua value from its internal form

Decodes a list of encoded values

Deletes a key from private storage

Encodes a Lua value into its internal form

Encodes a list of values into a list of encoded value

Evaluates the Lua script, returning any returned values and the updated Lua environment

Gets a table value in Lua

Gets a private value in storage for use in Elixir functions

Gets a private value in storage for use in Elixir functions, raises if the key doesn't exist

Inject functions written with the deflua macro into the Lua runtime.

Loads string or Lua.Chunk.t/0 into state so that it can be evaluated via eval!/2

Loads a Lua file into the environment. Any values returned in the global scope are thrown away.

Initializes a Lua VM sandbox

Parses a chunk of Lua code into a Lua.Chunk.t/0, which then can be loaded via load_chunk!/2 or run via eval!.

Puts a private value in storage for use in Elixir functions

Sandboxes the given path, swapping out the implementation with a function that raises when called

Sets a table value in Lua. Nested keys will allocate intermediate tables

Sets the path patterns that Luerl will look in when requiring Lua scripts. For example, if you store Lua files in your application's priv directory

Write Lua code that is parsed at compile-time.

Types

t()

@type t() :: %Lua{state: term()}

Functions

call_function(lua, ref, args)

Calls a function in Lua's state

iex> {:ok, [ret], _lua} = Lua.call_function(Lua.new(), [:string, :lower], ["HELLO ROBERT"])
iex> ret
"hello robert"

References to functions can also be passed

iex> {[ref], lua} = Lua.eval!("return string.lower", decode: false)
iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, ["FUNCTION REF"])
iex> ret
"function ref"

call_function!(lua, func, args)

The raising variant of call_function/3

This is also useful for executing Lua function's inside of Elixir APIs

defmodule MyAPI do
  use Lua.API, scope: "example"

  deflua foo(value), state do
    Lua.call_function!(state, [:string, :lower], [value])
  end
end

lua = Lua.new() |> Lua.load_api(MyAPI)

{["wow"], _} = Lua.eval!(lua, "return example.foo("WOW")")

decode!(lua, value)

Decodes a Lua value from its internal form

iex> {encoded, lua} = Lua.encode!(Lua.new(), %{a: 1})
iex> Lua.decode!(lua, encoded)
[{"a", 1}]

decode_list!(lua, list)

Decodes a list of encoded values

Useful for decoding all function arguments in a deflua

iex> {encoded, lua} = Lua.encode_list!(Lua.new(), [1, %{a: 2}, true])
iex> Lua.decode_list!(lua, encoded)
[1, [{"a", 2}], true]

delete_private(lua, key)

Deletes a key from private storage

iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
iex> lua = Lua.delete_private(lua, :api_key)
iex> Lua.get_private(lua, :api_key)
:error

encode!(lua, value)

Encodes a Lua value into its internal form

iex> {encoded, _} = Lua.encode!(Lua.new(), %{a: 1})
iex> encoded
{:tref, 14}

encode_list!(lua, list)

Encodes a list of values into a list of encoded value

Useful for encoding lists of return values

iex> {[1, {:tref, _}, true], _} = Lua.encode_list!(Lua.new(), [1, %{a: 2}, true])

eval!(script)

Evaluates the Lua script, returning any returned values and the updated Lua environment

iex> {[42], _} = Lua.eval!(Lua.new(), "return 42")

eval!/2 can also evaluate chunks by passing instead of a script. As a performance optimization, it is recommended to call load_chunk!/2 if you will be executing a chunk many times, but it is not necessary.

iex> {[4], _} = Lua.eval!(~LUA[return 2 + 2]c)

Options

  • :decode - (default true) By default, all values returned from Lua scripts are decoded.
            This may not be desirable if you need to modify a table reference or access a function call.
            Pass `decode: false` as an option to return encoded values

eval!(script, opts)

eval!(lua, script, opts)

get!(lua, keys, opts \\ [])

Gets a table value in Lua

iex> state = Lua.set!(Lua.new(), [:hello], "world")
iex> Lua.get!(state, [:hello])
"world"

When a value doesn't exist, it returns nil

iex> Lua.get!(Lua.new(), [:nope])
nil

It can also get nested values

iex> state = Lua.set!(Lua.new(), [:a, :b, :c], "nested")
iex> Lua.get!(state, [:a, :b, :c])
"nested"

Options

  • :decode - (default true) - By default, values are decoded

get_private(lua, key)

Gets a private value in storage for use in Elixir functions

iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
iex> Lua.get_private(lua, :api_key)
{:ok, "1234"}

get_private!(lua, key)

Gets a private value in storage for use in Elixir functions, raises if the key doesn't exist

iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
iex> Lua.get_private!(lua, :api_key)
"1234"

load_api(lua, module, opts \\ [])

Inject functions written with the deflua macro into the Lua runtime.

See Lua.API for more information on writing api modules

Options

  • :scope - (optional) scope, overriding whatever is provided in use Lua.API, scope: ...
  • :data - (optional) - data to be passed to the Lua.API.install/3 callback

load_chunk!(lua, code)

Loads string or Lua.Chunk.t/0 into state so that it can be evaluated via eval!/2

Strings can be loaded as chunks, which are parsed and loaded

iex> {%Lua.Chunk{}, %Lua{}} = Lua.load_chunk!(Lua.new(), "return 2 + 2")

Or a pre-compiled chunk can be loaded as well. Loaded chunks will be marked as loaded, otherwise they will be re-loaded everytime eval!/2 is called

iex> {%Lua.Chunk{}, %Lua{}} = Lua.load_chunk!(Lua.new(), ~LUA[return 2 + 2]c)

load_file!(lua, path)

Loads a Lua file into the environment. Any values returned in the global scope are thrown away.

Mimics the functionality of Lua's dofile

new(opts \\ [])

Initializes a Lua VM sandbox

iex> Lua.new()

By default, the following Lua functions are sandboxed.

  • [:io]
  • [:file]
  • [:os, :execute]
  • [:os, :exit]
  • [:os, :getenv]
  • [:os, :remove]
  • [:os, :rename]
  • [:os, :tmpname]
  • [:package]
  • [:load]
  • [:loadfile]
  • [:require]
  • [:dofile]
  • [:load]
  • [:loadfile]
  • [:loadstring]

To disable, use the sandboxed option, passing an empty list

iex> Lua.new(sandboxed: [])

Alternatively, you can pass your own list of functions to sandbox. This is equivalent to calling Lua.sandbox/2.

iex> Lua.new(sandboxed: [[:os, :exit]])

Options

  • :sandboxed - list of paths to be sandboxed, e.g. sandboxed: [[:require], [:os, :exit]]
  • :exclude - list of paths to exclude from the sandbox, e.g. exclude: [[:require], [:package]]

parse_chunk(code)

Parses a chunk of Lua code into a Lua.Chunk.t/0, which then can be loaded via load_chunk!/2 or run via eval!.

This function is particularly useful for checking Lua code for syntax erorrs and warnings at runtime. If you would like to just load a chunk, use load_chunk!/1 instead.

iex> {:ok, %Lua.Chunk{}} = Lua.parse_chunk("local foo = 1")

Errors found during parsing will be returned as a list of formatted strings

iex> Lua.parse_chunk("local foo =;")
{:error, ["Line 1: syntax error before: ';'"]}

put_private(lua, key, value)

Puts a private value in storage for use in Elixir functions

iex> Lua.new() |> Lua.put_private(:api_key, "1234")

sandbox(lua, path)

Sandboxes the given path, swapping out the implementation with a function that raises when called

iex> lua = Lua.new(sandboxed: [])
iex> Lua.sandbox(lua, [:os, :exit])

set!(lua, keys, func)

Sets a table value in Lua. Nested keys will allocate intermediate tables

iex> Lua.set!(Lua.new(), [:hello], "World")

It can also set nested values

iex> Lua.set!(Lua.new(), [:a, :b, :c], [])

These table values are availble in Lua scripts

iex> lua = Lua.set!(Lua.new(), [:a, :b, :c], "nested!")
iex> {result, _} = Lua.eval!(lua, "return a.b.c")
iex> result
["nested!"]

Lua.set!/3 can also be used to expose Elixir functions

iex> lua = Lua.set!(Lua.new(), [:sum], fn args -> [Enum.sum(args)] end)
iex> {[10], _lua} = Lua.eval!(lua, "return sum(1, 2, 3, 4)")

Functions can also take a second argument for the state of Lua

iex> lua =
...>   Lua.set!(Lua.new(), [:set_count], fn args, state ->
...>     {[], Lua.set!(state, :count, Enum.count(args))}
...>   end)
iex> {[3], _} = Lua.eval!(lua, "set_count(1, 2, 3); return count")

set_lua_paths(lua, paths)

Sets the path patterns that Luerl will look in when requiring Lua scripts. For example, if you store Lua files in your application's priv directory:

iex> lua = Lua.new(exclude: [[:package], [:require]])
iex> Lua.set_lua_paths(lua, ["myapp/priv/lua/?.lua", "myapp/lua/?/init.lua"])

Now you can use the Lua require function to import these scripts

Warning

In order to use Lua.set_lua_paths/2, the following functions cannot be sandboxed:

  • [:package]
  • [:require]

By default these are sandboxed, see the :exclude option in Lua.new/1 to allow them.

sigil_LUA(code, opts)

(macro)

Write Lua code that is parsed at compile-time.

iex> ~LUA"return 2 + 2"
"return 2 + 2"

If the code cannot be lexed and parsed, it raises a Lua.CompilerException

#iex> ~LUA":not_lua"
** (Lua.CompilerException) Failed to compile Lua!

As an optimization, the c modifier can be used to return a pre-compiled Lua chunk

iex> ~LUA"return 2 + 2"c