Sandboxing Chunks of Lua Code With Environments

Before I continue my articles on Lua domain-specific languages I want to explain a useful technique, one which I will be using in my next entry in that series. The game I am working on uses files of Lua code to define data such as narrative scenes. For example, in one file we may create characters for the game and then write a scene of dialog between them. But when we load these scripts we do not want the variables we create within them to clobber any variables within the game engine itself. How do we get around this?

Lua allows us to create a sandbox in which to run our external chunks of code, as this article will explain.

An Important Note

Our game uses the LÖVE engine, which uses Lua 5.1. The code in this article uses the setfenv() function. Lua 5.2 removes this function and replaces it with the _ENV table. Therefore it is important to note that the code presented in this article will not work with Lua 5.2 becausing I am basing this article on code from our game, and that uses Lua 5.1.

Loading Scripts of Code

Let’s begin with an example script, example.lua, that we could use in our story engine, one which defines a character that we can later make speak dialog in scenes.

Player = Character {
    name = "Lobby",
    textColor = Color.Blue,
}

There is nothing complicated about this script. It creates a character and assigns it to the variable Player. This is how we can load the script and make that variable available to the rest of the game.

local chunk = loadfile("example.lua")
chunk()

The loadfile() function returns what Lua calls a ‘chunk’, a function that, when invoked, executes the code from the loaded file. That is why we must explicitly invoke the chunk. After doing this we have full access to the Player variable we defined in our script.

But what if our game already has a Player variable? For example, we may have a Player variable in global scope that stores data about the player such as score, remaining lives, and so on. When we load our script the Player in it will replace any existing variable of the same name.

We could try to keep track of all variable names to make sure we do not re-use any by accident. But that approach is error-prone since everyone forgets things. A better approach is to make it impossible for variables in scripts to write over existing variables with the same name. And to do that we use ‘environments’.

Guess What Environments Are?

Environments are tables. That should be no surprise considering how any remotely complex data structure in Lua is a table.

When we create global variables Lua inserts them into the ‘global environment’, which has the name _G. That means those variables become keys in that table, like so:

Foo = 10
print(_G["Foo"])

This will print the value of Foo. This is not useful though. It only serves to highlight that environments are the tables we know and love.

Now how can we use environments to sandbox our example script from earlier? Lua allows us to run functions in different environments. So we can create a new environment for the function. Then when we execute the function Lua restricts any variables we create to that environment, preventing us from tampering with the global environment. Here is an example:

local ScriptEnvironment = {}

function LoadScript(filename)
    local chunk = loadfile(filename)
    setfenv(chunk, ScriptEnvironment)
    chunk()
end

First note how we created a new, empty table for our environment. The rest of the code is almost identical to what we wrote before. Except we use a new function: setfenv(). This is a standard Lua function which changes environments for a function, hence the name, ‘set function environment’. Remember that loadfile() returns a function for us to execute. However, before we do that we use setfenv() to tell Lua to use our specific table for the environment.

Let’s think about the original example script where we created the Player variable. By using the LoadScript() function above we can write this:

Player = "Lobby"
LoadScript("example.lua")
print(Player)
print(ScriptEnvironment["Player"])

This will first print "Lobby" and then the table we created in the example script. The assignment of Player within the example script does not affect the Player outside the script since we execute that example code inside its own environment. Now we can define any variables we want inside a script and not worry about those variables clobbering pre-existing ones with the same name.

Pretty simple isn’t it? In the future I will explain more ways we can use Lua environments to our advantage. In the mean time this code from our game is a real-world example that may help reinforce the technique.

Advertisements

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