Do you want to pick up from where you left of?
Take me there

Lua

The Lua library provides an ergonomic interface to Luerl, enabling safe execution of sandboxed Lua scripts directly on the BEAM VM. In this lesson, we’ll explore how to embed Lua scripting capabilities into our Elixir applications for user-defined logic, configuration, and extensibility.

The Lua library for Elixir is an ergonomic wrapper around Luerl, Robert Virding’s pure Erlang implementation of Lua 5.3. Unlike other approaches that rely on NIFs or other mechanisms, this implementation runs entirely on the BEAM VM while providing excellent sandboxing and integration capabilities.

Why Use Lua in Elixir?

So why might we want to use Lua when Elixir itself is such a powerful language? While Elixir is incredibly powerful, it comes with certain risks when executing user-provided code. Elixir’s hot code loading feature, which allows modules to be replaced at runtime, means that evaluating untrusted Elixir code could potentially overwrite existing modules in our running application or introduce malicious code that persists beyond the evaluation context! This makes direct evaluation of user-provided Elixir code extremely dangerous in production environments and strongly discouraged.

Lua provides a safer alternative that allows us to execute user code which in turn enriches our applications with features like user defined business logic and complex system configuration.

Installation

Add the Lua library to our mix.exs dependencies:

defp deps do
  [
    {:lua, "~> 0.1.0"}
  ]
end

Then run:

mix deps.get

Basic Usage

Let’s start with the simplest possible example evaluating Lua code:

iex> {result, _state} = Lua.eval!("return 2 + 3")
{[5], #PID<0.123.0>}
iex> result
[5]

The Lua.eval!/2 function returns a tuple containing the results (as a list) and the Lua state. Even simple expressions return results as lists because Lua functions can return multiple values.

The ~LUA Sigil

The Lua library provides a ~LUA sigil that validates Lua syntax at compile time:

iex> import Lua, only: [sigil_LUA: 2]
iex> {[7], _state} = Lua.eval!(~LUA[return 3 + 4])
{[7], #PID<0.124.0>}

If we try to use invalid Lua syntax, we’ll get a compile-time error:

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

Compile-time Optimization

Using the c modifier with the sigil compiles our Lua code into a Lua.Chunk.t() at compile-time, improving runtime performance:

iex> import Lua, only: [sigil_LUA: 2]
iex> {[42], _state} = Lua.eval!(~LUA[return 6 * 7]c)
{[42], #PID<0.125.0>}

Working with Lua State

Each Lua execution environment maintains its own state, including variables, functions, and data. We can create and manipulate this state:

iex> lua = Lua.new()
#PID<0.126.0>

# Set a variable
iex> lua = Lua.set!(lua, [:my_var], 42)
#PID<0.126.0>

# Read it back
iex> {[42], _state} = Lua.eval!(lua, "return my_var")
{[42], #PID<0.126.0>}

We can also work with nested data structures:

iex> lua = Lua.new()
iex> lua = Lua.set!(lua, [:config, :database, :port], 5432)
iex> {[5432], _state} = Lua.eval!(lua, "return config.database.port")
{[5432], #PID<0.127.0>}

Exposing Elixir Functions to Lua

Simple Function Exposure

The most straightforward way to expose an Elixir function to Lua is using Lua.set!/3:

iex> import Lua, only: [sigil_LUA: 2]
iex> 
iex> lua = 
...>   Lua.new()
...>   |> Lua.set!([:sum], fn args -> [Enum.sum(args)] end)
#PID<0.128.0>

iex> {[10], _state} = Lua.eval!(lua, ~LUA[return sum(1, 2, 3, 4)]c)
{[10], #PID<0.128.0>}

Note that Elixir functions exposed to Lua should:

Using the deflua Macro

For more complex APIs, the deflua macro provides a cleaner syntax:

defmodule MathAPI do
  use Lua.API

  deflua add(a, b), do: a + b
  deflua multiply(a, b), do: a * b
  deflua power(base, exponent), do: :math.pow(base, exponent)
end

# Load the API into a Lua state
iex> lua = Lua.new() |> Lua.load_api(MathAPI)
iex> {[16.0], _state} = Lua.eval!(lua, ~LUA[return power(2, 4)])
{[16.0], #PID<0.129.0>}

Scoped APIs

We can organize functions under namespaces using the :scope option:

defmodule StringAPI do
  use Lua.API, scope: "str"

  deflua upper(text), do: String.upcase(text)
  deflua lower(text), do: String.downcase(text)
  deflua length(text), do: String.length(text)
end

iex> lua = Lua.new() |> Lua.load_api(StringAPI)
iex> {["HELLO"], _state} = Lua.eval!(lua, ~LUA[return str.upper("hello")])
{["HELLO"], #PID<0.130.0>}

Advanced API Patterns

Working with Lua Tables

When working with complex Lua data structures, we can use Lua.Table.as_list/1 to convert Lua tables back to Elixir lists:

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

iex> lua = Lua.new() |> Lua.load_api(Queue)
iex> {[queue], _} = Lua.eval!(lua, """
...> my_queue = {}
...> q.push("first")
...> q.push("second")
...> return my_queue
...> """)
iex> Lua.Table.as_list(queue)
["first", "second"]
defmodule CounterAPI do
  use Lua.API, scope: "counter"

  deflua increment(), state do
    current = Lua.get(state, [:count], 0)
    new_count = current + 1
    state = Lua.set!(state, [:count], new_count)
    {[new_count], state}
  end

  deflua get_count(), state do
    count = Lua.get(state, [:count], 0)
    {[count], state}
  end
end

iex> lua = Lua.new() |> Lua.load_api(CounterAPI)
iex> {[1], lua} = Lua.eval!(lua, ~LUA[return counter.increment()])
iex> {[2], lua} = Lua.eval!(lua, ~LUA[return counter.increment()])
iex> {[2], _state} = Lua.eval!(lua, ~LUA[return counter.get_count()])

Calling Lua Functions from Elixir

We can also call Lua functions from our Elixir code using Lua.call_function!/3:

defmodule StringProcessorAPI do
  use Lua.API, scope: "processor"

  deflua process_with_lua(text), state do
    # Call a Lua function to process the text
    Lua.call_function!(state, [:string, :upper], [text])
  end
end

iex> lua = Lua.new() |> Lua.load_api(StringProcessorAPI)
iex> {["PROCESSED"], _state} = Lua.eval!(lua, ~LUA[return processor.process_with_lua("processed")])

Data Types and Encoding

When working with Lua, understanding how Elixir data types map to Lua is crucial:

Elixir Type Lua Type Encoding Required?
nil nil No
boolean() boolean No
number() number No
binary() string No
atom() string Yes
map() table Yes
list() table Maybe*
{:userdata, any()} userdata Yes

*Lists require encoding only if they contain elements that need encoding.

Working with Maps and Tables

Elixir maps become Lua tables when encoded:

iex> config = %{database: %{host: "localhost", port: 5432}, debug: true}
iex> {encoded_config, lua} = Lua.encode!(Lua.new(), config)
iex> lua = Lua.set!(lua, [:config], encoded_config)
iex> {[5432], _state} = Lua.eval!(lua, "return config.database.port")
{[5432], #PID<0.131.0>}

User Data for Complex Structures

For passing around complex Elixir data structures that we don’t want Lua to modify:

defmodule User do
  defstruct [:id, :name, :email]
end

iex> user = %User{id: 1, name: "Alice", email: "alice@example.com"}
iex> {encoded_user, lua} = Lua.encode!(Lua.new(), {:userdata, user})
iex> lua = Lua.set!(lua, [:current_user], encoded_user)
iex> {[{:userdata, %User{id: 1, name: "Alice", email: "alice@example.com"}}], _state} = 
...>   Lua.eval!(lua, "return current_user")
{[{:userdata, %User{id: 1, name: "Alice", email: "alice@example.com"}}], #PID<0.132.0>}

Private Context and Security

One of the most powerful features is the ability to maintain private context that’s accessible to our Elixir code but hidden from Lua scripts:

defmodule UserAPI do
  use Lua.API, scope: "user"

  deflua get_name(), state do
    user = Lua.get_private!(state, :current_user)
    {[user.name], state}
  end

  deflua get_permission(resource), state do
    user = Lua.get_private!(state, :current_user)
    permissions = Lua.get_private!(state, :permissions)
    
    has_permission = resource in Map.get(permissions, user.id, [])
    {[has_permission], state}
  end
end

# Set up the execution context
user = %{id: 1, name: "Alice"}
permissions = %{1 => ["read_posts", "write_comments"]}

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

# User can only access their name and check permissions
{["Alice"], _state} = Lua.eval!(lua, ~LUA[return user.get_name()])
{[true], _state} = Lua.eval!(lua, ~LUA[return user.get_permission("read_posts")])
{[false], _state} = Lua.eval!(lua, ~LUA[return user.get_permission("admin_panel")])

Real-World Example: Configuration Engine

Let’s build a practical example that allows users to define complex business rules:

defmodule PricingAPI do
  use Lua.API, scope: "pricing"

  deflua get_base_price(product_type), state do
    prices = Lua.get_private!(state, :base_prices)
    price = Map.get(prices, product_type, 0)
    {[price], state}
  end

  deflua calculate_discount(user_tier, order_amount), _state do
    discount = case user_tier do
      "premium" when order_amount >= 100 -> 0.2
      "premium" -> 0.1
      "standard" when order_amount >= 50 -> 0.05
      _ -> 0.0
    end
    {[discount], state}
  end

  deflua apply_seasonal_modifier(month), _state do
    modifier = case month do
      12 -> 0.9  # December discount
      1 -> 0.95  # January discount
      _ -> 1.0
    end
    {[modifier], state}
  end
end

defmodule ConfigEngine do
  def calculate_price(product_type, quantity, user_tier, lua_script) do
    base_prices = %{
      "widget" => 10.0,
      "gadget" => 25.0,
      "premium_item" => 100.0
    }

    lua = 
      Lua.new()
      |> Lua.put_private(:base_prices, base_prices)
      |> Lua.load_api(PricingAPI)
      |> Lua.set!([:product_type], product_type)
      |> Lua.set!([:quantity], quantity)
      |> Lua.set!([:user_tier], user_tier)
      |> Lua.set!([:current_month], Date.utc_today().month)

    {[final_price], _state} = Lua.eval!(lua, lua_script)
    final_price
  end
end

Now users can define complex pricing logic unique to their needs without us having to code many different scenarios into our application:

pricing_script = ~LUA"""
base_price = pricing.get_base_price(product_type)
subtotal = base_price * quantity

discount = pricing.calculate_discount(user_tier, subtotal)
seasonal_modifier = pricing.apply_seasonal_modifier(current_month)

final_price = subtotal * (1 - discount) * seasonal_modifier
return final_price
"""c

# Calculate price for a premium user buying 5 widgets in December
price = ConfigEngine.calculate_price("widget", 5, "premium", pricing_script)
# Result: 50 * 0.8 * 0.9 = 36.0

Error Handling and Debugging

The Lua library provides improved error messages compared to raw Luerl:

iex> try do
...>   Lua.eval!("return undefined_function()")
...> rescue
...>   e -> IO.puts("Lua error: #{inspect(e)}")
...> end

For compile-time validation errors:

iex> import Lua, only: [sigil_LUA: 2]
iex> try do
...>   ~LUA[return 2 +]
...> rescue
...>   e in Lua.CompilerException -> IO.puts("Compile error: #{e.message}")
...> end
Compile error: Failed to compile Lua!

For debugging we can inspect the Lua state:

iex> lua = Lua.new() |> Lua.set!([:debug_var], "debugging")
iex> variables = Lua.get_globals(lua)
iex> IO.inspect(variables)

Testing Lua Integration

When testing code that uses Lua we can provide controlled Lua scripts:

defmodule MyAppTest do
  use ExUnit.Case
  import Lua, only: [sigil_LUA: 2]

  test "pricing calculation with lua script" do
    script = ~LUA[return base_price * quantity * 0.9]c
    
    lua = 
      Lua.new()
      |> Lua.set!([:base_price], 10.0)
      |> Lua.set!([:quantity], 3)

    {[result], _state} = Lua.eval!(lua, script)
    assert result == 27.0
  end

  test "error handling for invalid lua" do
    assert_raise Lua.CompilerException, fn ->
      Lua.eval!(~LUA[return invalid syntax])
    end
  end
end

Some Things to Consider

When integrating Lua into our Elixir applications there are several important considerations for performance optimization and security that will help ensure efficient and secure execution.

Performance

For performance one of the most impactful optimization is using compile-time chunks with the c modifier in ~LUA sigils, eliminating parsing overhead on every execution. It is also advisable to reuse Lua state when possible since creating new states involves expensive initialization of the entire Lua environment.

Data conversion may become a bottleneck with large datasets so keeping data in compatible formats reduces conversion overhead. Another consideration that impacts performance and security is to expose only the functions your scripts actually need, each exposed function increases memory footprint and potential security risks.

Security

When evaluating user code be sure to never expose dangerous functions that access file systems, networks, or processes, as malicious scripts could compromise your entire application. If you must store sensitive data in your Lua state use private context rather than Lua variables since private context remains isolated from the Lua execution environment.

Lastly, follow the principle of least privilege by exposing only necessary APIs, as each function represents a potential attack vector for malicious scripts.

Conclusion

The Lua library for Elixir provides a powerful and safe way to add user-defined scripting capabilities to our applications. By leveraging the BEAM VM’s strengths and Luerl’s sandboxing capabilities, we can create flexible, extensible systems that allow users to customize behavior without compromising security.

The seamless integration between Elixir and Lua, combined with the safety guarantees of running everything on the BEAM VM, makes this library an excellent choice for applications that need to execute user-defined logic safely and efficiently.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!