Resource Acquisition Is Initialization


This page shall address approaches for achieving the effect of Resource Acquisition Is Initialization (RAII) [1] in Lua. RAII is a quite useful paradigm that is not directly supported in Lua 5.1, though there are some ways to approximate it. Some discussions and proposed solutions are on the Lua list:

The Problem

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.

Simulating RAII by maintaining a stack of destructible objects

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
]]

--DavidManura

Scope Manager

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

A Possible Syntax Extension for Scope Guard Statement

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].

--DavidManura

Try/finally/scoped-guard patches

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.

(2008年01月07日) PATCH: finalization of objects posted by Nodir Temirhodzhaev. [13] This is an alternative to the above "try/catch/finally" exception handling mechanism and is related to [3] and ResourceAcquisitionIsInitialization.

Update 2009年02月14日: LuaList:2009-02/msg00258.html ; LuaList:2009-03/msg00418.html

(2008年02月12日) PATCH: experimental finalize/guard posted by Alex Mania [14] supports finalize and guard blocks for RAII.

LuaList:2009-03/msg00418.html

with statement (Metalua, Python)

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.

Additional Comments

[*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

See Also


RecentChanges · preferences
edit · history
Last edited March 12, 2012 11:36 pm GMT (diff)

AltStyle によって変換されたページ (->オリジナル) /