Resource Acquisition Is Initialization |
|
A very typical problem well suited to RAII is this:
function dostuff() local f = assert(io.open("out", "w")) domorestuff() -- this may raise an error f:close() -- this is not called if that error was raised end dostuff()
If an error is raised, the file is not immediately closed (as RAII would ensure). Yes, the garbage collector will eventually close the file, but we don't know when. The program success or correctness may depend on the lock on the file being immediately released. Explicitly calling collectgarbage('collect')
outside a pcall
may help here though, in which case Lua calls the __gc
(finalizer) metamethod, which closes the file, though you may have to call collectgarbage
more than once [*1]. Furthermore, Lua doesn't allow objects implemented in pure Lua (without the help of C userdata) to define their own __gc
metamethods.
Here is one approach in pure Lua that maintains a stack of all objects that need to be reclaimed. On scope exit or upon handling an exception, the objects to be reclaimed are removed from the stack and finalized (i.e. close
, if exists, is called; otherwise, it is called as a function) to release their resources.
-- raii.lua local M = {} local frame_marker = {} -- unique value delimiting stack frames local running = coroutine.running -- Close current stack frame for RAII, releasing all objects. local function close_frame(stack, e) assert(#stack ~= 0, 'RAII stack empty') for i=#stack,1,-1 do -- release in reverse order of acquire local v; v, stack[i] = stack[i], nil if v == frame_marker then break else -- note: assume finalizer never raises error if type(v) == "table" and v.close then v:close() else v(e) end end end end local function helper1(stack, ...) close_frame(stack); return ... end -- Allow self to be used as a function modifier -- to add RAII support to function. function M.__call(self, f) return function(...) local stack, co = self, running() if co then -- each coroutine gets its own stack stack = self[co] if not stack then stack = {} self[co] = stack end end stack[#stack+1] = frame_marker -- new frame return helper1(stack, f(...)) end end -- Show variables in all stack frames. function M.__tostring(self) local stack, co = self, running() if co then stack = stack[co] end local ss = {} local level = 0 for i,val in ipairs(stack) do if val == frame_marker then level = level + 1 else ss[#ss+1] = string.format('[%s][%d] %s', tostring(co), level, tostring(val)) end end return table.concat(ss, '\n') end local function helper2(stack, level, ok, ...) local e; if not ok then e = select(1, ...) end while #stack > level do close_frame(stack, e) end return ... end -- Construct new RAII stack set. function M.new() local self = setmetatable({}, M) -- Register new resource(s), preserving order of registration. function self.scoped(...) local stack, co = self, running() if co then stack = stack[co] end for n=1,select('#', ...) do stack[#stack+1] = select(n, ...) end return ... end -- a variant of pcall -- that ensures the RAII stack is unwound. function self.pcall(f, ...) local stack, co = self, running() if co then stack = stack[co] end local level = #stack return helper2(stack, level, pcall(f, ...)) end -- Note: it's somewhat convenient having scoped and pcall be -- closures.... local scoped = raii.scoped return self end -- singleton. local raii = M.new() return raii
Example usage:
local raii = require "raii" local scoped, pcall = raii.scoped, raii.pcall -- 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", self.name) end end local test3 = raii(function() local f = scoped(Resource.open('D')) f:foo() print(raii) error("opps") end) local test2 = raii(function() scoped(function(e) print("leaving", e) end) local f = scoped(Resource.open('C')) test3(st) end) local test1 = raii(function() local g1 = scoped(Resource.open('A')) local g2 = scoped(Resource.open('B')) print(pcall(test2)) end) test1() --[[ OUTPUT: open A open B open C open D hello D [nil][1] A [nil][1] B [nil][2] function: 0x68a818 [nil][2] C [nil][3] D close D close C leaving complex2.lua:23: opps complex2.lua:23: opps close B close A ]]
Example using coroutines:
local raii = require "raii" local scoped, pcall = raii.scoped, raii.pcall -- 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 local running = coroutine.running function Resource:__tostring() return self.name end function Resource.open(name) local self = setmetatable({name=name}, Resource) print(running(), "open", self.name) return self end function Resource:close() print(running(), "close", self.name) end function Resource:foo() print(running(), "hello", self.name) end end local test3 = raii(function(n) local f = scoped(Resource.open('D' .. n)) f:foo() print(raii) error("opps") end) local test2 = raii(function(n) scoped(function(e) print(coroutine.running(), "leaving", e) end) local f = scoped(Resource.open('C' .. n)) test3(n) end) local test1 = raii(function(n) local g1 = scoped(Resource.open('A' .. n)) coroutine.yield() local g2 = scoped(Resource.open('B' .. n)) coroutine.yield() print(coroutine.running(), pcall(test2, n)) coroutine.yield() end) local cos = {coroutine.create(test1), coroutine.create(test1)} while true do local is_done = true for n=1,#cos do if coroutine.status(cos[n]) ~= "dead" then coroutine.resume(cos[n], n) is_done = false end end if is_done then break end end -- Note: all coroutines must terminate for RAII to work. --[[ OUTPUT: thread: 0x68a7f0 open A1 thread: 0x68ac10 open A2 thread: 0x68a7f0 open B1 thread: 0x68ac10 open B2 thread: 0x68a7f0 open C1 thread: 0x68a7f0 open D1 thread: 0x68a7f0 hello D1 [thread: 0x68a7f0][1] A1 [thread: 0x68a7f0][1] B1 [thread: 0x68a7f0][2] function: 0x68ada0 [thread: 0x68a7f0][2] C1 [thread: 0x68a7f0][3] D1 thread: 0x68a7f0 close D1 thread: 0x68a7f0 close C1 thread: 0x68a7f0 leaving complex3.lua:24: opps thread: 0x68a7f0 complex3.lua:24: opps thread: 0x68ac10 open C2 thread: 0x68ac10 open D2 thread: 0x68ac10 hello D2 [thread: 0x68ac10][1] A2 [thread: 0x68ac10][1] B2 [thread: 0x68ac10][2] function: 0x684258 [thread: 0x68ac10][2] C2 [thread: 0x68ac10][3] D2 thread: 0x68ac10 close D2 thread: 0x68ac10 close C2 thread: 0x68ac10 leaving complex3.lua:24: opps thread: 0x68ac10 complex3.lua:24: opps thread: 0x68a7f0 close B1 thread: 0x68a7f0 close A1 thread: 0x68ac10 close B2 thread: 0x68ac10 close A2 ]]
JohnBelmonte suggested in LuaList:2007-05/msg00354.html [*2] implementing something like a D scope
guard statement [3][4] construct in Lua. The idea was for variable class (like local
) named scoped
that when provided a function (or callable table), it would call it on scope exit:
function test() local fh = io:open() scoped function() fh:close() end foo() end
It is possible to implement this in plain Lua. This is described in Lua Programming Gems, Gem #13 "Exceptions in Lua" [5] to permit something like this:
function dostuff() scope(function() local fh1 = assert(io.open('file1')) on_exit(function() fh1:close() end) ... local fh2 = assert(io.open('file2')) on_exit(function() fh2:close() end) ... end) end
This requires the construction of an anonymous function, but there are advantages to avoid that from an efficiency standpoint.
Here's another idea ("finally ... end" construct) that is very basic:
function load(filename) local h = io.open (filename) finally if h then h:close() end end ... end
Note that the scope
construct as implemented in D syntactically resembles an if
statement that executes at the end of the scope. That is, provided we consider exit
, success
, and failure
to be real conditional expressions; in fact, it might be useful to make that generalization. I had proposed the following syntax extension for Lua:
stat :: scopeif exp then block {elseif exp then block} [else block] end
where err
is an implicit variable (like self
) that can be used inside exp or block and represents the error being raised, or nil
if no error was raised. (Comment: after revisiting that syntax again many months later, I found the semantics not very intuitive, particularly concerning the special usage of err
.)
The examples in "Exception Safe Programming" [3] translate into Lua as
function abc() local f = dofoo(); scopeif err then dofoo_undo(f) end local b = dobar(); scopeif err then dobar_undo(b) end local d = dodef(); return Transaction(f, b, d) end ----- function bar() local verbose_save = verbose verbose = false scopeif true then verbose = verbose_save end ...lots of code... end ----- function send(msg) do local origTitle = msg.Title() scopeif true then msg.SetTitle(origTitle) end msg.SetTitle("[Sending] " .. origTitle) Copy(msg, "Sent") end scopeif err then Remove(msg.ID(), "Sent") else SetTitle(msg.ID(), "Sent", msg.Title) end SmtpSend(msg) -- do the least reliable part last end
The scopeif true then ... end
is somewhat verbose though not unlike while true do ... end
. The use of scopeif
rather than scope if
follows the pattern of elseif
.
JohnBelmonte's database example becomes shortened to
function Database:commit() for attempt = 1, MAX_TRIES do scopeif instance_of(err, DatabaseConflictError) then if attempt < MAX_TRIES then log('Database conflict (attempt '..attempt..')') else error('Commit failed after '..attempt..' tries.') end end -- note: else no-op self.commit() return end end
Here's how a regular RAII would be simulated (the D article doesn't say that RAII is never useful):
function test() local resource = Resource(); scope if true then resource:close() end foo() end
That is, however, more verbose than the proposed
function test() scoped resource = Resource() foo() end
Perhaps this can be prototyped in Metalua [6].
A few patches to Lua have been posted to handle this type of thing:
(2008-01-31) PATCH: for try/catch/finally support posted by Hu Qiwei [10][11][12]. return
and break
are prohibited in the try
block.
finalize
and guard
blocks for RAII.
MetaLua 0.4 offers an RAII extension called "withdo". It works with every resources that are released by calling a method :close(). It protects against normal termination of the protected block, returns from within the block, and errors. The following would return the sum of the sizes of files filename1 and filename2 *after* having closed their handles:
with h1, h2 = io.open 'filename1', io.open 'filename2' do local total = #h1:read'*a' + #h2:read'*a' return total end
Note that the Metalua design is limited by the requirement that resource objects have a certain method ("close()" in this case). In Python it was rejected in favor of "with ... as ..." syntax allowing a resource management object separate from the resource itself [7]. Furthermore the Python statement allows the assignment to be elided, since in many situations the resource variable is not needed-- for example if you just want to hold a lock during the block.
[*1] It could be twice or more, depending on how intertwined the userdata are, but it definitely takes two collects to get rid of a userdata with a __gc
meta if the userdata is the last reference to some other object which itself is/refers to a userdata, then the cycle continues. (noted by RiciLake)
[*2] The RAII pattern suffers from the need to create ad-hoc classes to manage resources, and from the clunky nesting needed when acquiring sequential resources. See http://www.digitalmars.com/d/exception-safe.html. A better pattern is in Lua gem #13 "Exceptions in Lua" [5]. --JohnBelmonte
Here's my proposed example code illustrating a Google Go defer
and D scope(exit)
like syntax. --DavidManura
-- Lua 5.1, example without exceptions local function readfile(filename) local fh, err = io.open(filename) if not fh then return false, err end local data, err = fh:read'*a' -- note: in this case, the two fh:close()'s may be moved here, but in general that is not possible if not data then fh:close(); return false, err end fh:close() return data end -- Lua 5.1, example with exceptions, under suitable definitions of given functions. local function readfile(filename) return scoped(function(onexit) -- based on pcall local fh = assert(io.open(filename)); onexit(function() fh:close() end) return assert(fh:read'*a') end) end -- proposal, example without exceptions local function readfile(filename) local fh, err = io.open(filename); if not fh then return false, err end defer fh:close() local data, err = fh:read'*a'; if not data then return false, err end return data end -- note: "local val, err = io.open(filename); if not val then return false, err end" is a common -- pattern and perhaps warrants a syntax like "local val = returnunless io.open(filename)". -- proposal, example with exceptions local function readfile(filename) local fh = assert(io.open(filename)); defer fh:close() return assert(fh:read'*a') end -- proposal, example catching exceptions do defer if class(err) == 'FileError' then print(err) err:suppress() end print(readfile("test.txt")) end -- alternate proposal - cleanup code by metamechanism local function readfile(filename) scoped fh = assert(io.open(filename)) -- note: fh:close() or getmetatable(fh).__close(fh) called on scope exit return assert(fh:read'*a') end