技术说明 3
本说明解释如何扩展 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
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.其他方法用于读取存储在固定缓冲区中的整数、字符串和指针。因此,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 -- 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: