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 endThese 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 endThe 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 -- functionOnce 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) endshow 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: