This note explains how to extend Lua to take advantage of system calls. Although my own efforts have been confined to an operating system that may be unknown to most readers (RISC OS), I believe that the principles involved are fairly universal. I write this note in the hope of getting useful criticism. It is an abstract of what I have done in implementing RiscLua.
RISC OS was designed for a specific family of processors, the ARM. User programs interact with RISC OS only via a specific processor instruction, SWI (SoftWare Interrupt). Every processor has an analogue of this, though doubtless called something different (TRAP?). Using a software interrupt involves the following steps:
extern void swi_call(int swi_number, void * regbuffer);for doing the SWI call. The regbuffer argument points to a 32-byte array for writing and reading the register values. For those who are familiar with the ARM's instruction set, here is the relevant assembler fragment:
swi_call: STMFD sp!, {R4-R8,R12,link} MOV R12,R0 ; SWI number MOV R8,R1 ; base of register values LDMIA R8,{R0-R7} SWI &71 ; OS_CallASWIR12 STMIA R8,{R0-R7} LDMFD sp!, {R4-R8,R12,PC}The following is code for a builtin C function
static int risc_swi (lua_State *L) { int swinum; void *r; if (lua_isstring(L,1)) swinum = swi_str2num(luaL_check_string(L,1)); /* convert string to number */ else if (lua_isnumber(L,1)) swinum = luaL_check_int(L,1); else lua_error(L,"swi: arg1 should be a string or a number."); if (!lua_isuserdata(L,2)) lua_error(L,"swi: arg2 should be userdata"); r = lua_touserdata(L,2); swi_call(swinum,r); lua_pushnil(L); return 1; }It defines a Lua function swi for system calls.
The data written to before and read from the registers after a software
interrupt are frequently pointers to fixed addresses in the program's
memory area, where various kinds of data may be held. These data may
be 32-bit integers, strings or pointers to other fixed buffers. It is
necessary that these arrays be fixed, for reasons hidden in the murky
past of RISC OS. Each task is responsible for allocating its own message
buffer and then it informs the task manager where it is. If the buffer
were to be moved, there would be trouble. Since Lua's datatypes are garbage
collected, we have to implement these fixed arrays using the userdata type.
We assign a particular tag, called "writeable", for userdata pointing to these
arrays. Here is C code for a function risc_dim
static int writeable_tag;
static int risc_dim (lua_State *L)
{
void *p;
if ((p = malloc((size_t) luaL_check_int(L,1))) != (void *)0)
lua_pushusertag(L,p, writeable_tag);
else
lua_pushnil(L);
return 1;
}
for a builtin lua function dim(n) which produces a
userdatum with the writeable tag pointing to a fixed buffer
holding n bytes. In addition we need functions to
read data from a fixed buffer into a lua variable, and to
write data to a fixed buffer from a lua variable. The types
of data we have to consider are
Of course, the user of RiscLua should be shielded from these
details. So I wrap all these functions up as methods for a
table
array = function (n)
local a = {}
a.n = n -- size of array
a.b = dim(n) -- bottom of array (address of first byte)
a.after = { b = disp(a.b,a.n) } -- next byte
a.words = array_words
a.chars = array_chars
a.int = array_int
a.ptr = array_ptr
a.strp = array_strp
a.char = array_char
a.str = array_str
return a
end
These methods have values which are global functions named
array_xxx. The "words" method is used to read 32-bit
values, and the "chars" method to read in 8-bit values. They
take tables as arguments, indexed by integers giving offsets
into the fixed buffer. The values in the tables can be numbers
(for byte values) or strings (for multiple bytes) in the case
of chars, and in the case of "words" they can be numbers (for
32-bit integers), C-strings held in a buffer (for pointers
to their address), or tables of the kind defined by array
(for pointers to buffers). Here is the lua code
array_words = function (self,t)
if (tag(self.b) ~= writeable) then
error("words: arg1 not an array") end
if (type(t) ~= "table") then
error("words: arg2 must be a table") end
local fns = {
number = function (i,v) putword(%self.b,i,v) end,
table = function (i,v)
if (tag(v.b) ~= writeable) then
error("words: arg not an array") end
putword(%self.b,i,v.b) end,
string = function (i,v) putword(%self.b,i,str2ptr(v)) end,
default = function () error("words: bad type") end
}
for i,v in t do
if (fns[type(v)]) then
fns[type(v)](i,v)
else
fns.default()
end
end
end
array_chars = function (self,t)
if (tag(self.b) ~= writeable) then
error("chars: arg1 not an array") end
if (type(t) ~= "table") then
error("chars: arg2 must be a table") end
local fns = {
number = function (i,v) putbyte(%self.b,i,v) end,
string = function (i,v)
local len,k = strlen(v),1
while (k <= len) do
putbyte(%self.b,i,strbyte(v,k))
k = k + 1; i = i + 1;
end
end,
default = function () error("chars: bad type") end
}
for i,v in t do
if (fns[type(v)]) then
fns[type(v)](i,v)
else
fns.default()
end
end
end
The functions putword, putbyte are builtin C-functions that
do the obvious things.
The result is that if we define, say
x,y = array(n),array(m)
we can do
x:chars { [0] = "hello".."\0" } -- only 6 bytes taken up so far
x:words { [2] = a_num, [3] = y }
storing a number a_num at bytes 8,9,10,11 and the
userdatum y.b at bytes 12,13,14,15 of the fixed buffer
pointed to by x.b.
The other methods are for reading integers, strings and pointers stored in fixed buffers. So x:int(2) should yield the value of a_num again, and x:str(0) should yield "hello". This, I hope, describes the syntax of reading and writing fixed buffers.
The actual interface to the operating system is given by
swi = {
regs = array(32),
call = function (self,x)
%swi(x,self.regs.b)
end
}
Note how the "call" method hides the raw swi function
described above. With array and swi defined
in a prelude file, we are in a position to use Lua to exploit
everything that the operating system offers. Of course, this
prelude is still very low level, but it offers enough to build
libraries for writing "wimp" (Windows Icons Menus Pointers) programs
that use RISC OS's graphical user interface. Here, as an example
of how the system calls can be used, is Lua code
to define a function w_task that creates a wimp task:
w_task = function (taskname,version,mesgs)
assert(type(taskname) == "string", " taskname not a string")
assert(type(version) == "number", " version not a number")
assert(type(mesgs) == "table", " mesgs not a table")
local title = _(taskname)
local wt = { err = _ERRORMESSAGE,
title = title,
action = {}, -- table of action methods indexed by events
block = array(256),
msgs = array(4+4*getn(mesgs)),
pollword = array(4),
poll = function (self,uservar)
local f,quit
self.mask = self.mask or 0
repeat
swi.regs:words {
[0] = self.mask,
[1] = self.block,
[3] = self.pollword }
swi:call("Wimp_Poll")
f = self.action[swi.regs:int(0)]
if f then quit = f(self,uservar) end
until quit
swi.regs:words {
[0] = self.handle,
[1] = TASK }
swi:call("Wimp_CloseDown")
_ERRORMESSAGE = self.err
end -- function
}
wt.msgs:words(mesgs) -- load messages buffer
swi.regs:words {
[0] = version,
[1] = TASK,
[2] = wt.title,
[3] = wt.msgs }
swi:call("Wimp_Initialise")
wt.handle = swi.regs:int(1)
_ERRORMESSAGE = function (errm) -- set error handler
local b = %wt.block
b:words { [0] = LUA_ERROR }
b:chars { [4] = errm .."\0" }
swi.regs:words { [0] = b, [1] = 16, [2] = %title }
swi:call("Wimp_ReportError")
end -- function
return wt
end -- function
Once a wimp task has been initialised and has set up its data it goes
to sleep by calling the "poll" method, handing over execution to the
task manager in the RISC OS kernel. When the task manager wakes it up
again it puts an event code in register R0. The lines
f = self.action[swi.regs:int(0)]
if f then quit = f(self,uservar) end
show that the task responds by executing an action method
indexed by the returned event code. This is how the non-preemptive
multitasking of RISC OS works. When the task is initialised it
sets up its own error handler to output error messages in a
window, and before closing down it restores the previous error
handler. Using the w_task function, and similar library
functions for loading templates for windows and menus, all the
programmer has to do is define handler methods for events, e.g.
mytask = w_task("MyTask",310, { [0] = M_DataLoad, [1] = M_Quit })
.....................
mytask.action[Mouse_Click] = function (self) ........ end
.....................
mytask:poll()
Although the examples contain detail that will not mean much to
those unfamiliar with RISC OS, the basic principles should be
much the same for other platforms: