Bound Scalar Globals Two

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 allows you to set a global variable to the result of a constructor function. See BoundScalarGlobalsOne for a different implementation.

Disclaimer: I take responsibility for writing this stuff, but I wouldn't use it in my own code, not because it is slow, but because it is too wierd. In the end, it is just as easy to use getter and setter functions. However, I put it here because I think that it is an interesting example of the possibilities of metamethods, and it demonstrates a useful way of working around the absence of gettable and settable metamethods. This would be useful in constructing a proxy for a C structure.

To pull this off, you need to at least associate a known metatable with each different type of C object. This metatable is effectively the "type" of the object. The metatable must have the keys __get and __set whose values are (possibly C) functions to get and set the values of the C scalars. If __get is the identity function, there would be nothing stopping you from populating the type/metamethod table other metamethods, such as arithmetic or comparators, or even __call, __index, or __newindex.

The constructor function needs to associate the C object with the metatable, and it is necessary to register the metatable with register_type (see below). (This mechanism is not very different from the mechanism in lauxlib, now that I look at it two years after writing it.)

It is not necessary to use C and userdatas; the example below uses Lua functions and tables but hopefully the C interface is reasonably obvious.

Once the result of the constructor is assigned to a global, future sets and gets to that global are deferred to the __get and __set pseudometamethods so you can never get rid of the binding (well, there could be an unbind function but I didn't write that.)

In BoundScalarGlobalsOne, I tried to deal with globals tables with existing metatables. This time I'm not -- although the same strategy would work in this case, it just makes the code harder to follow and it is hard enough to follow as it is :)

For efficiency, I assume that you are not going to want to redefine existing globals as C-values and that there will not be a lot of new non C-value globals after this procedure is executed. That means that system library functions will still be looked up without interference.

-- BoundScalar2
-- Version 0.1
-- RiciLake
--
do
  local types = {} -- table of metatables we're going to deal with
  local global_meta = {} -- metatable for globals
  local sub_meta = {} -- metatable for new globals
  local c_proxied = {} -- table of proxied values.
  local new_globals = {} -- normal globals

  -- use this function to register your metatables (types)
  function register_type(meta)
    types[meta] = true
  end

  -- __index metamethod for globals table
  --
  -- We know the key doesn't exist in the globals table,
  -- which contains pre-existing globals. It
  -- might be new global, or it might be a proxied c-value.
  -- We defer the lookup to the new_globals
  -- table which has its own __index metamethod

  function global_meta.__index(t, k)
    return new_globals[k]
  end

  -- __index metamethod for new_globals table
  --
  -- This gets invoked if the key wasn't found in the
  -- new_globals table. So it is either a
  -- proxied c-value or an undefined global

  function sub_meta.__index(t, k)
    local val = c_proxied[k]
    if val then return getmetatable(val).__get(val) end
  end

  -- __newindex metamethod for globals table
  --
  -- With set, we need to follow a different strategy, because we need to
  -- intercept every attempt to set a global (this doesn't happen
  -- with pre-existing globals).

  function global_meta.__newindex(t, k, v)
    -- first we see if we're proxying this key, and if so we just pass on
    -- the value. c_proxied does not have metamethods, so we don't need
    -- rawget.
    local cval = c_proxied[k]
    if cval then
        getmetatable(cval).__set(cval, v)
      else
        -- get the *value*'s type (well, metatable)
        -- and see if it is one of ours.
        local meta = getmetatable(v)
        if meta and types[meta] then
            -- It is a c_val, so we need to get rid of it from regular
            -- globals, in case it exists, and put it into the proxied
            -- table
            new_globals[k] = nil
            c_proxied[k] = v
          else
            -- it is neither an existing c_value nor a new one. So we just
            -- stash it away
            new_globals[k] = v
        end
    end
  end

  setmetatable(getfenv(), global_meta)
  setmetatable(new_globals, sub_meta)
end

Now we can define our bindable types. Here is a quick example. See sample output for how it works

do
  local date_meta = {}
  register_type(date_meta)

  -- __get returns the date according to the object's format
  function date_meta.__get(date_obj)
    return os.date(date_obj.fmt)
  end

  -- __set changes the object's format
  function date_meta.__set(date_obj, fmt)
    date_obj.fmt = fmt
  end

  -- constructor
  function Date(fmt)
    return setmetatable({fmt = fmt or "%c"}, date_meta)
  end
end

Sample output

> now = Date()
> print(now)
Thu Jan 16 16:41:13 2003
> gmt = Date("!%c")
> print(gmt)
Thu Jan 16 21:41:22 2003
> gmt = "!%F %R"
> print(gmt)
2003-01-16 21:41

RecentChanges · preferences
edit · history
Last edited November 4, 2004 5:53 pm GMT (diff)