A Mutable Log

A blog by Devendra Tewari


Project maintained by tewarid Hosted on GitHub Pages — Theme by mattgraham

Lua dofile and globals

I have been doing dissector development for Wireshark in Lua for a while now. Lua’s mechanism for reusing code organized across multiple files comes in two flavors, dofile and require. For historical reasons, we’ve been using dofile quite exclusively.

Our dissector files mostly contain global functions, and local or global tables. The tables are used to determine which function should be called to dissect a particular message, to look-up names that map to particular key values, and so on. If a script file needs a function or table in another file, we simply dofile the other file at the top. Lua interpreter will recognize all globals (functions and variables without local) of the other file from that point onwards.

Let’s consider an example. Here’s file1.lua

function f1()
  print("f1 called")
  f2()
end

dofile("file2.lua")

f1()

Here’s file2.lua, located in the same folder as file1.lua

function f2()
  print("f2 called")
end

When you run file1.lua, this is what you get

$ lua file1.lua
f1 called
f2 called

Lua only encounters call to global f2 inside function f1 when we call function f1 towards the end of file1.lua.

If we call f1 right at the beginning of file1.lua we get

$ lua file1.lua
lua: file1.lua:2: attempt to call global 'f1' (a nil value)
stack traceback:
    file1.lua:2: in main chunk
    [C]: in ?

If we move dofile towards the end of file1.lua we get

$ lua file1.lua
f1 called
lua: file1.lua:3: attempt to call global 'f2' (a nil value)
stack traceback:
    file1.lua:3: in function 'f1'
    file1.lua:7: in main chunk
    [C]: in ?

Summing it up, a global exists only when Lua has encountered it, whether within the same or another script file.

Let us explore one other characteristic of dofile with another example.

This is file1.lua

dofile("file2.lua")
hello['world'] = 'hello!'
print(hello['world'])

function f1()
  print(hello['world'])
  f3()
end

dofile("file3.lua")

f1()

It requires a certain global table in file2.lua, which it updates before using. It also requires a certain function f3 in file3.lua.

This is file2.lua

hello = {['hello']='world!'}

function f2()
  print("f2 called")
end

This is file3.lua

dofile('file2.lua')

function f3()
  print('f3 called')
  f2()
end

It needs function f2 in file2.lua. You shouldn’t need dofile because file1.lua has already loaded file2.lua, but whoever coded file3.lua probably doesn’t know that.

Let’s execute file1.lua and see what happens

$ lua file1.lua
hello!
nil
f3 called
f2 called

By the time function f1 gets called, the value of key 'world' in the hello table ceases to exist.

Why?

Because, right before f1 is called, when we dofile file3.lua it will dofile file2.lua. Since file2.lua gets interpreted again, the global hello is replaced by a new table.

Summing it up, Lua will load and execute the same file again when it encounters dofile, redefining all globals in it.

That is why, for reusing code in other script files, use require. Lua will not reinterpret a script file that has already been encountered before. Modifying the example scripts above is straightforward. Replace all dofile with require and drop the lua extension. Thus dofile('file2.lua') will become require('file2').

Execute file1.lua thus

$ export LUA_PATH=$PWD/?.lua
$ lua file1.lua
hello!
hello!
f3 called
f2 called

Globals now work without side-effects!

Note the use of LUA_PATH environment variable to specify the search path for Lua scripts. You can also set search path inside a Lua script by modifying package.path.

What if you have already invested in dofile and don’t want to change things for now?

In the previous example, modifying file2.lua thus, eliminates our problem

if hello == nil then hello = { } end
hello['hello']='world!'

function f2()
  print("f2 called")
end