Bound Scalar Globals One

lua-users home
wiki

The problem: how can one define a global variable such that accessing and setting its value actually calls a C function which manipulates an internal object. This is easy in Lua 4 but Lua 5 lacks most of the metamethods which would be used for a naive Lua 4 implementation.

Here is one solution, which requires that the globals be bound by name. See BoundScalarGlobalsTwo for a different implementation.

The idea here is simple: the bound scalars are not present in the globals table; instead, getter and setter functions are placed into two other tables, and these functions are called by the global table's __index and __newindex metamethods. This has no performance impact on global variables which have values, and hopefully reasonably limited impact on bound variables. (Proxied would be a better name, probably -- the phrase "bound scalar" comes from Perl. [1])

Here, I take care to use any existing __index and __newindex metamethods from the existing globals table. Hopefully, other existing metamethods don't matter, but they could be handled too.

-- BindScalar1
-- Version 0.2
-- RiciLake
--
-- Change history
-- --------------
-- Fixed the case where the globals table didn't have a metatable
-- Corrected the behaviour on set where there was no existing
--   __newindex metamethod so that it now rawsets the table
-- Check to see if the old __index method is not a function
--   to mimic the default behaviour
-- Wrote a couple of quick example getters and setters
-- Actually made sure it compiles and runs
--
-- TODO
-- ----
-- Actually debug it with real metatables
-- Think of a setter that lets you set something
--
-- BUGS
-- ----
-- If you specify a getter and don't specify a setter, the binding stops
-- working. It should be necessary to specify both.
--
-- The API needs to be improved

do
  local meta, getters, setters = {}, {}, {}
  local old_meta = getmetatable(getfenv())
  local old_index, old_newindex
  if old_meta then
    old_index, old_newindex = old_meta.__index, old_meta.__newindex
  end
  
  -- at this point you have to populate the getters and setters table
  -- somehow, probably by getting them from your C code.
  -- Here is an example without C:

  -- the getter receives the name of the global as an argument

  local function get_time(k)
    if k == "gmt"
      then return os.date("!%c")
      else return os.date("%c")
    end
  end

  -- the setter actually receives the name and the proposed value
  -- but in this example we don't need them.

  local function set_time()
    error "You cannot change the time"
  end

  -- now put them into getters and setters. There should probably
  -- be a function to do that, something like:
  --   bind_scalar("now", get_time, set_time)

  getters.now = get_time
  getters.gmt = get_time
  setters.now = set_time
  setters.gmt = set_time

  -- Another example. Particular environment variables are made
  -- into globals. (Change this to USERNAME for Windows NT.)

  local function get_env(k)
    return os.getenv(k)
  end

  local function set_env(k, v)
    if os.setenv
      then
        os.setenv(k, v)
      else
        error "You cannot change environment variables on this platform."
    end
  end

  getters.USER = get_env
  setters.USER = set_env -- hmm? it's just an example

  -- It might be nice to change the calls below to object calls,
  -- such as getters[k](getters[k], k)
  -- For efficiency, you probably only want to do that lookup once.

  -- Here is the actual implementation of the metamethods.

  meta = {}

  if type(old_index) == "function" then
      function meta.__index(t, k)
        if getters[k]
          then return getters[k](k)
          else return old_index(t, k)
        end
      end
    elseif type(old_index) == "nil" then
      function meta.__index(t, k)
        if getters[k] then return getters[k](k) end
      end
    else
      function meta.__index(t, k)
        if getters[k]
          then return getters[k](k)
          else return old_index[k]
        end
      end
  end

  if old_newindex
    then
      function meta.__newindex(t, k, v)
        if setters[k]
          then setters[k](k, v)
          else old_newindex(t, k, v)
        end
      end
    else
      function meta.__newindex(t, k, v)
        if setters[k]
          then
            setters[k](k, v)
          else
            rawset(t, k, v)
          end
      end
  end

  setmetatable(getfenv(), meta)
end

Sample output:

-- now is deferred to a function.
> print(now)
Thu Jan 16 12:34:40 2003
-- so is gmt
> print(gmt)
Thu Jan 16 17:35:34 2003
-- setting "works"; the variable is read-only
> now = "tomorrow"
glue.lua:27: You cannot change the time
stack traceback:
        [C]: in function `error'
        glue.lua:27: in function `?'
        glue.lua:91: in function <glue.lua:88>
        stdin:1: in main chunk
        [C]:[C]

-- This mechanism might be useful in a CGI script, for example
> print(USER)
rlake
-- Most platforms implement setenv but it's not ANSI standard. This
-- would work if you patched the os library.
> USER="root"
glue.lua:44: You cannot change environment variables on this platform.
stack traceback:
        [C]: in function `error'
        glue.lua:44: in function `?'
        glue.lua:91: in function <glue.lua:88>
        stdin:1: in main chunk
        [C]:[C]
-- Ordinary globals continue to be ordinary.
> print(j)
nil
> j = 7
> print(j)
7

Footnotes:

[1] In Perl, these are formally called [tied variables] (see also [Cultured Perl: Tied variables]), which are defined as a type of binding. You're tying (binding) the identity (name or address) of a variable to a particular implementation defined in a class. This class could be considered a proxy to something else, but there may be something to be said about the binding of the proxy to an identifier in the language, which may require special language support. See [Wikipedia Binding_(computer_science)] (particularly name binding) and [Wikipedia Proxy Pattern]. See also the term as in [Wikiepdia Free variables and bound variables]. In particular, Perl's TIEHASH and TIEARRAY are somewhat analogous to Lua's metatable mechanism. Perl's TIESCALAR seems to have no such analog in Lua, but a similar effect, as described in this page, is to modify the metatable of the global environment (possibly a disadvantage unless some general framework as given above is developed to prevent clashes). --DavidManura

This has only one problem: __newindex is only called on values that were not set before, e.g. x = "bla"; x = "more bla" will call __newindex only once. --Anon

Yes, that's why it doesn't actually set the value if it users the setter function. However, when you create the setter functions, you must first ensure that the corresponding global is not set; if you want to add a setter function later on, the same thing applies. Give it a try :) --RiciLake


RecentChanges · preferences
edit · history
Last edited March 22, 2007 10:52 pm GMT (diff)