Resource Acquisition Is Initialization

lua-users home
wiki

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 13, 2012 3:36 am GMT (diff)