Current section
Files
Jump to
Current section
Files
lib/lua.ex
defmodule Lua do
@external_resource "README.md"
@moduledoc @external_resource
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
defstruct [:state]
alias Luerl.New, as: Luerl
alias Lua.Util
@default_sandbox [
[:io],
[:file],
[:os, :execute],
[:os, :exit],
[:os, :getenv],
[:os, :remove],
[:os, :rename],
[:os, :tmpname],
[:package],
[:load],
[:loadfile],
[:require],
[:dofile],
[:load],
[:loadfile],
[:loadstring]
]
defimpl Inspect do
def inspect(_lua, _opts) do
"#Lua<>"
end
end
@doc """
Initializes a Lua VM sandbox. All library functions are stubbed out,
so no access to the filesystem or the execution environment is exposed.
## Options
* `:sandboxed` - list of paths to be sandboxed, e.g. `sandboxed: [[:require], [:os, :exit]]`
"""
def new(opts \\ []) do
sandboxed = Keyword.get(opts, :sandboxed, @default_sandbox)
Enum.reduce(sandboxed, %__MODULE__{state: :luerl.init()}, &sandbox(&2, &1))
end
@doc """
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])
"""
def sandbox(lua, path) do
set!(lua, [:_G | path], fn args ->
raise Lua.RuntimeException,
"#{Lua.Util.format_function(path, Enum.count(args))} is sandboxed"
end)
end
@doc """
Sets the path patterns that Lua will look in when requiring Lua scripts.
"""
def set_lua_paths(%__MODULE__{} = lua, paths) when is_binary(paths) do
set!(lua, ["package", "path"], paths)
end
@doc """
Sets a table value in Lua. Nested keys will create
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!"]
"""
def set!(%__MODULE__{state: state}, keys, value) do
{_keys, state} =
Enum.reduce_while(keys, {[], state}, fn key, {keys, state} ->
keys = keys ++ [key]
case :luerl_new.get_table_keys_dec(keys, state) do
{:ok, nil, state} ->
{:cont, set_keys!(state, keys)}
{:ok, _val, state} ->
{:halt, {keys, state}}
{:lua_error, _err, state} ->
raise Lua.RuntimeException, {:lua_error, illegal_index(keys), state}
end
end)
case :luerl_new.set_table_keys_dec(keys, value, state) do
{:ok, _value, state} ->
wrap(state)
{:lua_error, _error, state} ->
raise Lua.RuntimeException, {:lua_error, illegal_index(keys), state}
end
end
defp set_keys!(state, keys) do
case :luerl_new.set_table_keys_dec(keys, [], state) do
{:ok, _, state} ->
{keys, state}
{:lua_error, _error, state} ->
raise Lua.RuntimeException, {:lua_error, illegal_index(keys), state}
end
end
defp illegal_index([:_G | keys]), do: illegal_index(keys)
defp illegal_index(keys) do
{:illegal_index, nil, Enum.join(keys, ".")}
end
@doc """
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"
"""
def get!(%__MODULE__{state: state}, keys), do: get!(state, keys)
# TODO remove this version
def get!(state, keys) when is_tuple(state) do
case :luerl_new.get_table_keys_dec(keys, state) do
{:ok, value, _state} ->
value
{:lua_error, _, state} ->
error = {:illegal_index, nil, Enum.join(keys, ".")}
raise Lua.RuntimeException, {:lua_error, error, state}
end
end
@doc """
Evalutes the script or chunk, returning the result and
discarding side effects in the state
"""
def eval!(state \\ new(), script)
def eval!(%__MODULE__{state: state} = lua, script) when is_binary(script) do
case Luerl.do_dec(state, script) do
{:ok, result, new_state} ->
{result, %__MODULE__{lua | state: new_state}}
{:lua_error, _e, _state} = error ->
raise Lua.RuntimeException, error
{:error, [error | _], _} ->
raise Lua.CompilerException, error
end
rescue
e in [UndefinedFunctionError] ->
reraise Lua.RuntimeException,
Util.format_function([e.module, e.function], e.arity),
__STACKTRACE__
e in [Lua.RuntimeException, Lua.CompilerException] ->
reraise e, __STACKTRACE__
e ->
reraise Lua.RuntimeException, e, __STACKTRACE__
end
@doc """
Calls a function in Lua's state
iex> {[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")
iex> {[ret], _lua} = Lua.call_function!(lua, ref, ["FUNCTION REF"])
iex> ret
"function ref"
This is also useful for executing Lua function's inside of Elixir APIs
```elixir
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\")")
```
"""
def call_function!(%__MODULE__{} = lua, name, args)
when (is_list(args) and is_tuple(name)) or is_function(name) do
{ref, lua} = encode!(lua, name)
case :luerl_new.call(ref, args, lua.state) do
{:ok, value, state} -> {value, wrap(state)}
{:lua_error, _, _} = error -> raise Lua.RuntimeException, error
end
end
def call_function!(%__MODULE__{} = lua, name, args) when is_list(args) do
keys = List.wrap(name)
func = get!(lua, keys)
case :luerl_new.call_function(func, args, lua.state) do
{:ok, ret, lua} -> {ret, wrap(lua)}
{:lua_error, _, _} = error -> raise Lua.RuntimeException, error
end
end
@doc """
Encodes a Lua value into its internal form
"""
def encode!(%__MODULE__{} = lua, value) do
{encoded, state} = :luerl_new.encode(value, lua.state)
{encoded, wrap(state)}
rescue
ArgumentError ->
reraise Lua.RuntimeException, "Failed to encode #{inspect(value)}", __STACKTRACE__
end
@doc """
Decodes a Lua value from its internal form
"""
def decode!(%__MODULE__{} = lua, value) do
:luerl_new.decode(value, lua.state)
rescue
ArgumentError ->
reraise Lua.RuntimeException, "Failed to decode #{inspect(value)}", __STACKTRACE__
end
@doc """
Loads a lua file into the environment. Any values returned in the globa
scope are thrown away.
Mimics the functionality of lua's [dofile](https://www.lua.org/manual/5.4/manual.html#pdf-dofile)
"""
def load_file!(%__MODULE__{state: state} = lua, path) when is_binary(path) do
case :luerl_new.dofile(String.to_charlist(path), [:return], state) do
{:ok, _, state} ->
%__MODULE__{lua | state: state}
{:lua_error, _error, _lua} = error ->
raise Lua.CompilerException, error
{:error, [{:none, :file, :enoent} | _], _} ->
raise "Cannot load lua file, #{inspect(path <> ".lua")} does not exist"
{:error, [error | _], _} ->
# We just take the first error
raise Lua.CompilerException, error
end
end
@doc """
Inject functions written with the `deflua` macro into the Lua
runtime
"""
def load_api(lua, module, scope \\ nil) do
funcs = :functions |> module.__info__() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
scope = scope || module.scope()
module.__lua_functions__()
|> Enum.reduce(lua, fn {name, with_state?, variadic?}, lua ->
arities = Map.get(funcs, name)
Lua.set!(
lua,
List.wrap(scope) ++ [name],
if variadic? do
wrap_variadic_function(module, name, with_state?)
else
wrap_function(module, name, arities, with_state?)
end
)
end)
end
defp wrap_variadic_function(module, function_name, with_state?) do
# formatted = Util.format_function(module.scope() ++ [function_name], 0)
if with_state? do
fn args, state ->
execute_function(module, function_name, args ++ [wrap(state)])
end
else
fn args ->
execute_function(module, function_name, [args])
end
end
end
# credo:disable-for-lines:25
defp wrap_function(module, function_name, arities, with_state?) do
# formatted = Util.format_function(module.scope() ++ [function_name], 0)
if with_state? do
fn args, state ->
if (length(args) + 1) in arities do
execute_function(module, function_name, args ++ [wrap(state)])
else
arities = Enum.map(arities, &(&1 - 1))
raise Lua.RuntimeException,
function: function_name,
scope: module.scope(),
message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}"
end
end
else
fn args ->
if length(args) in arities do
execute_function(module, function_name, args)
else
raise Lua.RuntimeException,
function: function_name,
scope: module.scope(),
message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}"
end
end
end
end
defp execute_function(module, function_name, args) do
# Luerl mandates lists as return values; this function ensures all results conform.
case apply(module, function_name, args) do
{ret, %Lua{state: state}} -> {ret, state}
other -> List.wrap(other)
end
catch
thrown_value ->
{:error,
"Value thrown during function '#{function_name}' execution: #{inspect(thrown_value)}"}
end
defp wrap(state), do: %__MODULE__{state: state}
end