本说明解释如何扩展 Lua 以利用系统调用。尽管我自己的努力局限于大多数读者可能不熟悉的操作系统 (RISC OS),但我相信所涉及的原理是相当通用的。我写这篇说明是为了得到有用的批评。这是我实现 RiscLua 时所做工作的摘要。
RISC OS 是为特定处理器系列 ARM 设计的。用户程序仅通过特定处理器指令 SWI(软件中断)与 RISC OS 交互。每个处理器都有类似的指令,尽管毫无疑问名称不同(TRAP?)。使用软件中断涉及以下步骤
extern void swi_call(int swi_number, void * regbuffer);以执行 SWI 调用。regbuffer 参数指向一个 32 字节的数组,用于写入和读取寄存器值。对于熟悉 ARM 指令集的人,这里有一个相关的汇编片段:
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}以下是内置 C 函数
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; }的代码。它定义了一个用于系统调用的 Lua 函数 swi。
在软件中断之前写入到寄存器的数据和之后从寄存器中读取的数据通常是程序内存区域中固定地址的指针,其中可能保存各种类型的数据。这些数据可能是 32 位整数、字符串或其他固定缓冲区的指针。出于 RISC OS 晦暗历史中的原因,必须将这些数组固定。每个任务负责分配自己的消息缓冲区,然后通知任务管理器缓冲区的位置。如果缓冲区被移动,将会出现问题。由于 Lua 的数据类型是垃圾回收的,因此我们必须使用用户数据类型实现这些固定数组。我们为指向这些数组的用户数据分配了一个特定的标记,称为“可写”。以下是内置 lua 函数 dim(n) 的 C 代码函数 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; },该函数生成一个用户数据,其中包含指向固定缓冲区的可写标记,该缓冲区保存 n 个字节。此外,我们需要函数将数据从固定缓冲区读入 lua 变量,并将数据从 lua 变量写入固定缓冲区。我们必须考虑的数据类型是
当然,RiscLua 的用户应该免受这些细节的影响。因此,我将所有这些函数包装为表 的方法
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.
其他方法用于读取存储在固定缓冲区中的整数、字符串和指针。因此,x:int(2) 应该再次产生 a_num 的值,而 x:str(0) 应该产生 "hello"。我希望这描述了读取和写入固定缓冲区的语法。
与操作系统的实际接口由以下内容提供
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: