Monday, September 28, 2009

Resource Cleanup in Lua

I recently got the Lua Programming Gems book, in part because I was interested in John Belmonte's chapter on Exceptions.

Here's the scope function that John created for the GEM article. Please refer to that article for further explanation of the rationale.

I've made some minor modifications to it to use the coxpcall function from Kepler.

function scope(f)
local function run(list, err)
for _, f in ipairs(list) do f(err) end
end
local function append(list, item)
list[#list+1] = item
end
local success_funcs, failure_funcs, exit_funcs = {}, {}, {}
local manager = {
on_success = function(f) append(success_funcs, f) end,
on_failure = function(f) append(failure_funcs, f) end,
on_exit = function(f) append(exit_funcs, f) end,
}
local old_fenv = getfenv(f)
setmetatable(manager, {__index = old_fenv})
setfenv(f, manager)
local status, err = copcall(f)
setfenv(f, old_fenv)
-- NOTE: behavior undefined if a hook function raises an error
run(status and success_funcs or failure_funcs, err)
run(exit_funcs, err)
if not status then error(err, 2) end
end

David Manura wrote a couple test scripts for his Resource Acquisition Is Initialization proposal. Here they are modified for John's scope() function.

This is a basic test:


-- Define some resource type for testing.
-- In practice, this is a resource we acquire and
-- release (e.g. a file, database handle, Win32 handle, etc.).
local Resource = {}; do
Resource.__index = Resource
function Resource:__tostring() return self.name end
function Resource.open(name)
local self = setmetatable({name=name}, Resource)
print("open", name)
return self
end
function Resource:close() print("close", self.name) end
function Resource:foo() print("hello foo", self.name) end
end

local test3 = function()
scope(function()
local f = Resource.open('D')
on_exit(function() f:close() end)
f:foo()
print("inside test3\n")
error("oops")
end)
end

local test2 = function()
scope(function()
on_failure(function(e) print("leaving test2 ", e) end)
local f = Resource.open('C')
on_exit(function() f:close() end)
test3()
end)
end

local test1 = function()
local retval
scope(function()
local g1 = Resource.open('A')
on_exit(function() g1:close() end)
local g2 = Resource.open('B')
on_exit(function() g2:close() end)
print(copcall(test2))
retval = 55
end)
return retval
end

print ("test1 = ", test1())

And here's a more complex example using coroutines and a little argument-passing:


-- Define some resource type for testing.
-- In practice, this is a resource we acquire and
-- release (e.g. a file, database handle, Win32 handle, etc.).
local Resource = {}; do
Resource.__index = Resource
function Resource:__tostring() return self.name end
function Resource.open(name)
local self = setmetatable({name=name}, Resource)
print("open", name)
return self
end
function Resource:close() print("close", self.name) end
function Resource:foo() print("hello foo", self.name) end
end

local test3 = function(n)
scope(function()
local f = Resource.open('D' .. n)
on_exit(function() f:close() end)
coroutine.yield()
f:foo()
print("inside test3\n")
coroutine.yield()
error("oops happened in test3")
print("this should not print\n")
end)
end

local test2 = function(n)
scope(function()
on_failure(function(e)
print(coroutine.running(),
"test2 failure ", e) end)
local f = Resource.open('C' .. n)
on_exit(function() f:close() end)
test3(n)
end)
end

local test1 = function(n)
local retval
scope(function()
local g1 = Resource.open('A' .. n)
on_exit(function() g1:close() end)
coroutine.yield()
local g2 = Resource.open('B' .. n)
on_exit(function() g2:close() end)
coroutine.yield()
print(coroutine.running(), "test2 = ", copcall(test2, n))
coroutine.yield()
retval = n * 100
end)
return retval
end

local cos = {coroutine.create(test1), coroutine.create(test1)}
local retval, x
while true do
local is_done = true
for n=1,#cos do
if coroutine.status(cos[n]) ~= "dead" then
retval, x = coroutine.resume(cos[n], n)
if retval and x then
print("test1 =", x)
end
is_done = false
end
end
if is_done then break end
end

So nesting the scope() call inside your regular function is a little clunky, but it's really not too bad. Lua's lexical scoping makes arguments to functions like test1() work they way you'd expect.

I hope the Lua developers decide accept a syntax extension to make this nicer. However, I can definitely live with it the way it is now.

I think some kind of scope management is necessary for reliable resource cleanup. The potential for bugs is too great otherwise.

Since all the above code is pure Lua (including coxpcall), there's no reason not to use it for your next project!