lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]



> One way to fix this would be to require each script to have a unique
> name. For example, if a script file is called "foo.lua", then it might
> define a function called "foo_getData1" instead of just plain
> "getData1". That way each script has its own version of getData1, and
> your app just needs to call the correct one depending on which script is
> being used at the time.


Another way to solve this is for each script to create a table of
functions/methods. You can do this directly, or you can manipulate
the environment table for the compiled script. I think the first
solution is cleaner, and it is certainly easier to write, but it
makes the scripts a little wordier.

Since you mentioned threads, I figured you might end up with
re-entrant tools, so I have tried to allow for that possibility.
It costs very little.

The protocol here is that a script file (or, if you like, a tool
definition) *returns* a function whose return value is the tool
represented as a table of tool-actions. Every time this function is
called, it returns a new tool.

Now, there are two choices: the tools can be one-off's, sort of
like objects, or they can be recyclable. Most of the time, the
one-off implementation will be the best: in that case, the function
returned by the script file is effectively the class "new" operation
so there is no need to define "init". So we can start with that
approach.

//// Warning: it is late and I didn't trying compiling any of this

-- Private class members are possible simply by using
-- closures. But you have to be careful to implement
-- some sort of synchronisation system if you are using
-- (OS-level) pre-emptive threading with mutable state.
-- This example just uses a constant, so there is no problem

local hiddenInflation = 0.05

-- If you have naive script authors, the simplest explanation
-- for the following two lines might just be "do it like this":

return function(init_arg1, init_arg2)
  local tool = {}

  -- Now here we can put private instance methods. There is
  -- no synchronisation issue as long as an instance of the
  -- tool runs in a single thread

  local count, sum = 0, 0
 
  -- Now we can define the tool entry points, again using
  -- a stylised syntax

  -- (I would have thought "receiveData" a better name)
  function tool.getData(d)
    count, sum = count + 1, sum + d
  end

  -- The boss thinks that some data points are more equal
  -- than others
  function tool.getBiasedData(d)
    count, sum = count + 20, sum + 20 * d
  end

  -- done does not actually reset the tool, it just returns
  -- the value. In this case, you could go on adding data,
  -- but that is probably not good design unless it is
  -- documented. Note that this tool is optimistic; the boss
  -- might like that, too.
  function tool.done()
    print("The average of the data set is " .. (sum / count) * (1 + hiddenInflation))
  end

  -- Now, the stylised end: we actually have to return the
  -- object

  return tool
end

------------------------------------------

OK, now we need to load this tool file and stash it somewhere.
To minimise the C code, we can do the grunt work in Lua

---------- file runtool.lua

-- Change this to the tool suffix you prefer
local SUFFIX = ".tool"

-- Memoise is an incredibly useful function, so I will repeat
-- it here. I load it in my standard library.

local function memoise(fn)
  return setmetatable({}, {
    __index = function(t, k)
      local rv = fn(k); t[k] = rv; return rv
    end
  }
end

-- here is a cheap-o split function which simply skips
-- empty fields

local function split(delim, str)
  local fields = {n = 0}
  string.gsub(str, "([^"..delim.."]+)", function(fld) table.insert(fields, fld) end)
  return fields
end

-- There must be a million versions of this one out there, too.

local function mapreplace(fn, vec)
  for i = 1, getn(vec) do
    vec[i] = fn(vec[i])
  end
end

-- Should probably try harder to validate paths; all this does
-- is get rid of leading and trailing spaces, and make sure that
-- there is a / at the end and no // in the middle

local function improve(path)
  path = gsub(path, "^%s*(.-)%s*$", "%1")
  return string.gsub(path, "/+", "/")
end

local path = mapreplace(improve, split(":", os.getenv "TOOLPATH"))
 
-- given a toolname, find the first file which exists
-- and then try to compile it. If the compilation
-- fails or the file doesn't exist, generate an error.
-- otherwise, run the script and return the resulting
-- tooltable

local function pathsearch(tool)
  for i = 1, getn(path) do
    local fn = path[i] .. tool .. SUFFIX
    local f = io.open(fn, "r")
    if f then
      f:close()
      local loader = assert(loadfile(fn))
      return loader()
    end
  end
  -- If we fall through:
  error("Could not find tool '"..tool.."'")
end

-- and finally, we put all the pieces together in a global
-- table:

TOOL = memoise(pathsearch)

-------------------------------------------------------------

Now, the C part is trivial, particularly if there is no
initialisation argument. This function leaves the tooltable
on the top of the stack: (or dies trying)

void tool_get(lua_State *L, const char *toolname) {
  lua_getglobal(L, "TOOL");
  lua_pushstring(L, toolname);
  lua_gettable(L, -2);
}

This function feeds a value into the tool on the top
of the stack. If you do this in a tight loop, you should
just look up the function once, but I'll leave that as
an exercise. Also, some error checking would be a good idea.

void tool_put(lua_State *L, lua_Number val) {
  lua_pushliteral(L, "getData");
  lua_gettable(L, -2);
  lua_pushnumber(L, val);
  lua_call(L, 1, 0);
}

This function calls the tool's done method and cleans up:

void tool_done(lua_State *L) {
  lua_pushliteral(L, "done");
  lua_gettable(L, -2);
  lua_call(L, 0, 0);
  lua_pop(L, 1);
}