Creating and Using Environments in Lua

Today I want to talk about ‘environments’ in Lua. To do so I will present a real use-case: the LÖVE Visual Novel Engine (LNVL), a free component of the game I’m making, which I’ve talked about previously. LNVL requires LÖVE and LÖVE uses Lua 5.1. This is important to keep in mind because the API for working with environments changed in Lua 5.2. So what I am going to show you will not work in recent versions of Lua, although it does work with LuaJIT as it maintains compatibility with Lua 5.1.

Enough with the links—let’s get started!

The Premise

LNVL is meant to tell stories. To that end we use it for handling all of the dialogue in L’Astra Vojego. The dialogue takes the form of Lua scripts, which are fed into LNVL for processing. Here is an example, a unit test from the project:

-- This script tests the behavior of the leavesTheScene() method,
-- which tells LNVL that a character is no longer activate and that it
-- should stop drawing that character to the screen.

Eric = Character {
    dialogName = "Eric",
    textColor = Color.NavyBlue,
    image = "examples/images/Eric-Normal.png",
    position = "Left",
}

Jeff = Character {
    dialogName = "Jeff",
    textColor = Color.IndianRed4,
    image = "examples/images/Jeff-Normal.png",
    position = "Right",
}

START = Scene {
    dialogName = "Character Deactivation Test",
    Eric "Why is our game not finished yet?",
    Jeff "Hey have you played the new Skyrim DLC?",
    Eric "Well no wonder our game isn't finished...",
    Jeff:leavesTheScene(),
    Eric "...Hey, what?  Stop playing Skyrim and come back and finish our game already!",
}

At first glance this may not appear to be valid Lua. It is a domain-specific language created for LNVL. You can read about its trickery if you like. The important thing for the purpose of this article is to note that this script defines variables: three of them.

LNVL is meant for integration within other games. This creates a serious potential problem: what if a dialogue script defines a variable that clashes with an existing variable elsewhere in the game? We could use extremely verbose names within dialogue scripts to cut down on the chances of a conflict, but that feels messy and would likely hurt readability. And since dialogue scripts are meant to be written by non-programmers we do not want to impose a requirement that they check the game source code for pre-existing variables before defining one.

It would be nice if we could wrap all of the dialogue script variables in a sandbox, a place of their own separate from the namespace of LNVL and the game using it. We can achieve exactly this by using Lua’s mechanism for environments.

The ‘Why?’ of Creating Environments

LNVL has a function for loading dialogue scripts written in Lua, appropriately called LoadScript(). We’re going to write that function in the course of this article. That way we can see why we want to use environments. Here’s our first draft:

function LoadScript(filename)
    local script = love.filesystem.load(filename)
    assert(script, "Could not load script " .. filename)

    -- The variable 'script' is a chunk reperesenting the code from
    -- the file we loaded.  In other words, 'script' is a function we
    -- can execute to run the code from that file.
    script()
end

Note: love.filesystem.load()

If we call this version of LoadScript() on a dialogue file it will execute the contents as Lua code, but then what? Looking back at the example dialogue above, what happens with those variable definitions? Where do they go?

The variables go into the global environment. The table _G represents the global environment, and as we’ll see all environments are tables. Once we load and execute that dialogue script we can access its variables through _G, e.g. _G["Eric"], _G["START"], et cetera. However, global variables in all our Lua code go into that same environment, and therein lies the risk of conflict. Consider this:

Eric = "I'm about to get clobbered."    -- Creates _G["Eric"]
LoadScript("our_example_above.lua")     -- Clobber _G["Eric"]

This is undesirable. In this trivial example we could avoid the problem by carefully using different variable names so as not to overwrite anything. But that becomes unreasonable for a large code-base. It would be nice if LoadScript() gave dialogue scripts their own environment separate from the global one.

With that in mind….

The ‘How?’ of Creating Environments

Note: I want to stress once again that the code you’re about to see does not work with Lua 5.2 and later.

Because environments are tables our first step is to create a table that will act as the environment for the scripts we load.

ScriptEnvironment = {}

Simple enough. But we need to execute the dialogue scripts in the context of that environment. For that we use the standard Lua function setfenv, “set function environment.” It lets us provide a table to act as the environment for a function, and remember that when we load dialogue scripts we get a function to call in order to execute that script. Let’s incorporate this into LoadScript():

ScriptEnvironment = {}

function LoadScript(filename)
    local script = love.filesystem.load(filename)
    assert(script, "Could not load script " .. filename)

    -- We tell Lua to execute the script in an environment represented
    -- by the table above.  We must do this *before* executing the
    -- script.  If we do it after running `script()` then it will have
    -- no effect.
    setfenv(script, ScriptEnvironment)

    -- Now we can execute the script in our custom environment.
    script()
end

Now the variables in our dialogue script do not appear in the global environment. Instead they go into the table we provided. We can access them by using the variable names as keys, e.g. ScriptEnvironment["Eric"].

This solves our problem of dirtying up the global environment but there is an important consequence of this approach which we must consider. When we use setfenv() on our script the code within it can access only what we define as part of ScriptEnvironment. This means, for example, that standard Lua functions are unusable; they are not part of our environment table. That means if we want to call any functions from within our script we must make them part of the environment first. This is exactly what LNVL does to provide the Character and Scene constructors in the example script at the top of this article. It looks like this:

ScriptEnvironment["Character"] = function (...)
    return Character:new(...)
end

Note: If you look at the LNVL source code, the function LNVL.CreateConstructorAlias() performs this.

In Summary

Environments are tables we can use to sandbox Lua code. They give us the ability to control where definitions go, i.e. keeping the global environment clean of conflicts, and allow us to dictate exactly what functions are available in those scripts. For a real-world example of these concepts you can see the main source file for LNVL.

If you have any questions please feel free to ask in the comments below. Happy hacking!

(Update 4 April 2015) Another implementation of sandboxing which is worth reading.

Advertisements

2 thoughts on “Creating and Using Environments in Lua

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s