views:

695

answers:

4

I'm lead dev for Bitfighter, and we're using Lua as a scripting language to allow players to program their own custom robot ships.

In Lua, you need not declare variables, and all variables default to global scope, unless declared otherwise. This leads to some problems. Take the following snippet, for example:

loc = bot:getLoc()
items = bot:findItems(ShipType)     -- Find a Ship

minDist = 999999
found = false

for indx, item in ipairs(items) do           
   local d = loc:distSquared(item:getLoc())  

   if(d < minDist) then
      closestItem = item
      minDist = d
   end
end

if(closestItem != nil) then 
   firingAngle = getFiringSolution(closestItem) 
end

In this snippet, if findItems() returns no candidates, then closestItem will still refer to whatever ship it found the last time around, and in the intervening time, that ship could have been killed. If the ship is killed, it no longer exists, and getFiringSolution() will fail.

Did you spot the problem? Well, neither will my users. It's subtle, but with dramatic effect.

One solution would be to require that all variables be declared, and for all variables to default to local scope. While that change would not make it impossible for programmers to refer to objects that no longer exist, it would make it more difficult to do so inadvertently.

Is there any way to tell Lua to default all vars to local scope, and/or to require that they be declared? I know some other languages (e.g. Perl) have this option available.

Thanks!


Lots of good answers here, thanks!

I've decided to go with a slightly modified version of the Lua 'strict' module. This seems to get me where I want to go, and I'll hack it a little to improve the messages and make them more appropriate for my particular context.

+2  A: 

Sorta.

In Lua, globals notionally live in the globals table _G (the reality is a bit more complex, but from the Lua side there's no way to tell AFAIK). As with all other Lua tables, it's possible to attach a __newindex metatable to _G that controls how variables are added to it. Let this __newindex handler do whatever you want to do when someone creates a global: throw an error, permit it but print a warning, etc.

To meddle with _G, it's simplest and cleanest to use setfenv. See the documentation.

David Seiler
+2  A: 

There is no option to set this behavior, but there is a module 'strict' provided with the standard installation, which does exactly that (by modifying the meta-tables). Usage: require 'strict'

For more in-depth info and other solutions: http://lua-users.org/wiki/DetectingUndefinedVariables, but I recommend 'strict'.

This seems to be the simplest answer, and is what I was looking for. I'd like to better understand the other solutions suggested here to see if there are any drawbacks to using 'strict'.
Watusimoto
A: 

Actually, the extra global variable with the stale reference to the ship will be sufficient to keep the GC from discarding the object. So it could be detected at run time by noticing that the ship is now "dead" and refusing to do anything with it. It still isn't the right ship, but at least you don't crash.

One thing you can do is to keep user scripts in a sandbox, probably a sandbox per script. With the right manipulation of either the sandbox's environment table or its metatable, you can arrange to discard all or most global variables from the sandbox before (or just after) calling the user's code.

Cleaning up the sandbox after calls would have the advantage of discarding extra references to things that shouldn't hang around. This could be done by keeping a whitelist of fields that are allowed to remain in the environment, and deleting all the rest.

For example, the following implements a sandboxed call to a user-supplied function with an environment containing only white-listed names behind a fresh scratch table supplied for each call.

-- table of globals that will available to user scripts
local user_G = {
        print=_G.print,
        math=_G.math,
        -- ...
    }
-- metatable for user sandbox
local env_mt = { __index=user_G }


-- call the function in a sandbox with an environment in which new global 
-- variables can be created and modified but they will be discarded when the 
-- user code completes.
function doUserCode(user_code, ...)
    local env = setmetatable({}, env_mt) -- create a fresh user environment with RO globals
    setfenv(user_code, env)        -- hang it on the user code
    local results = {pcall(user_code, ...)}
    setfenv(user_code,{})
    return unpack(results)
end

This could be extended to make the global table read-only by pushing it back behind one more metatable access if you wanted.

Note that a complete sandbox solution would also consider what to do about user code that accidentally (or maliciously) executes an infinite (or merely very long) loop or other operation. General solutions for this are an occasional topic of discussion on the Lua list, but good solutions are difficult.

RBerteig
The problem with the reference to the dead ship object is that the lifespan of the object is controlled by the master C++ program, so the object can be deleted without Lua's consent or control. Lua has a reference to a userdata pointer, which suddenly points at an invalid object. How can I detect that? I need to get a better handle on this, and would be open to any suggestions.I'm working along the lines of a sandbox, but I do want to allow creation of global variables as long as they are created intentionally and deliberately. All of the above suggestions seem to accomplish that.
Watusimoto
+1  A: 

"Local by default is wrong". Please see

http://lua-users.org/wiki/LocalByDefault

http://lua-users.org/wiki/LuaScopingDiscussion

You need to use some kind of global environment protection. There are some static tools to do that (not too mature), but the most common solution is to use runtime protection, based on __index and __newindex in _G's metatable.

Shameles plug: this page may also be useful:

http://code.google.com/p/lua-alchemy/wiki/LuaGlobalEnvironmentProtection

Note that while it discusses Lua embedded into swf, the described technique (see sources) do work for generic Lua. We use something along these lines in our production code at work.

Alexander Gladysh
Local by default is (arguably) wrong, but then so is global by default!I looked over the code you pointed to, and it looks like it does something very similar to using the 'strict' module suggested above. Does they differ in any significant way?
Watusimoto
Global by default is wrong as well, but it is a Lua legacy and hardly can be changed (also it is very useful when writing mini-DSLs in Lua).
Alexander Gladysh
The main difference (between those strict modules) is that mine tries to force explicit and concise declaration of global variables (as I see them as evil things to be getting rid of in my code -- including global functions). Lua's etc/strict.lua is a bit less explicit. But it is a personal preference.
Alexander Gladysh