技术说明 3

原文:https://www.lua.org/notes/ltn003.html

Technical Note 3 技术说明 3

Last update: Mon Aug 12 15:48:51 EST 2002 by lhf.

Interfacing Lua to an operating system - Lua 与操作系统接口

by Gavin Wraith 作者 Gavin Wraith

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.

​ 本说明解释了如何扩展 Lua 以利用系统调用。尽管我自己的工作仅限于大多数读者可能不知道的操作系统 (RISC OS),但我相信所涉及的原理是相当普遍的。我写这篇说明是为了获得有用的批评。这是我对实现 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:

​ RISC OS 是为特定系列处理器 ARM 设计的。用户程序仅通过特定处理器指令 SWI(软中断)与 RISC OS 交互。每个处理器都有类似的东西,尽管毫无疑问叫法不同(TRAP?)。使用软件中断涉及以下步骤:

  1. Write some processor registers with appropriate data (some of which may be pointers to fixed addresses in the program’s memory area). 用适当的数据写入一些处理器寄存器(其中一些可能是指向程序内存区域中固定地址的指针)。
  2. Call the SWI. 调用 SWI。
  3. Read some registers. 读取一些寄存器。

In practice, only a subset of processor registers is ever used for passing data between program and operating system, namely R0, R1, …, R7. All the registers are 32 bits wide. It requires seven instructions of code to produce a C function

​ 在实践中,处理器寄存器中仅有一部分用于在程序和操作系统之间传递数据,即 R0、R1、…、R7。所有寄存器均为 32 位宽。需要七条指令的代码才能生成一个 C 函数

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 调用。 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}

The following is code for a builtin C function

​ 以下是内置 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;
}

It defines a Lua function swi for system calls.

​ 它定义了一个 Lua 函数 swi 用于系统调用。

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

​ 在软件中断之前写入寄存器的数据和之后从寄存器中读取的数据通常是程序内存区域中固定地址的指针,其中可能保存各种类型的数据。这些数据可能是 32 位整数、字符串或指向其他固定缓冲区的指针。出于 RISC OS 晦涩的过去的原因,这些数组必须是固定的。每个任务负责分配自己的消息缓冲区,然后通知任务管理器该缓冲区的位置。如果要移动缓冲区,就会出现问题。由于 Lua 的数据类型是垃圾回收的,因此我们必须使用 userdata 类型来实现这些固定数组。我们为指向这些数组的 userdata 分配了一个名为“可写”的特定标记。以下是函数 risc_dim `的 C 代码:

      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

用于内置 lua 函数 risc_dim,该函数生成一个 userdatum,其中包含指向固定缓冲区的可写标记,该缓冲区保存 n个字节。此外,我们需要一些函数将数据从固定缓冲区读入 lua 变量,并将数据从 lua 变量写入固定缓冲区。我们必须考虑的数据类型是

  • 32-bit integers to/from lua numbers
  • 32 位整数到/从 lua 数字
  • strings to/from lua strings
  • 字符串到/从 lua 字符串
  • 32-bit pointers to/from lua userdata
  • 32 位指针指向/来自 lua 用户数据

I omit the details of these conversion functions.

​ 我省略了这些转换函数的详细信息。

Of course, the user of RiscLua should be shielded from these details. So I wrap all these functions up as methods for a table

​ 当然,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_xxx 的全局函数。使用“words”方法读取 32 位值,使用“chars”方法读取 8 位值。它们将表作为参数,由整数索引给出固定缓冲区的偏移量。表中的值可以是数字(对于字节值)或字符串(对于多个字节),对于字符,对于“words”,它们可以是数字(对于 32 位整数)、保存在缓冲区中的 C 字符串(对于指向其地址的指针)或由 array 定义的表(对于指向缓冲区的指针)。以下是 lua 代码

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

​ 函数 putword, putbyte 是内置的 C 函数,可以执行显而易见的操作。结果是,如果我们定义,比如说

  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.

将数字 a_num 存储在固定缓冲区中由 x.b 指向的字节 8、9、10、11 处,并将用户数据 y.b 存储在字节 12、13、14、15 处。

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.

​ 其他方法用于读取存储在固定缓冲区中的整数、字符串和指针。因此, x:int(2) 应该再次产生 a_num 的值,而 x:str(0) 应该产生 "hello" 。我希望这描述了读取和写入固定缓冲区的语法。

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:

​ 注意“call”方法如何隐藏上面描述的原始 swi 函数。在预备文件中定义了 arrayswi 后,我们就可以使用 Lua 来利用操作系统提供的所有功能。当然,这个预备文件仍然非常底层,但它提供了足够的功能来构建用于编写使用 RISC OS 图形用户界面的“wimp”(Windows 图标菜单指针)程序的库。在此,作为系统调用如何使用的示例,Lua 代码用于定义一个创建 wimp 任务的函数 w_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

​ 一旦 wimp 任务初始化并设置了其数据,它就会通过调用“poll”方法进入休眠状态,将执行权移交给 RISC OS 内核中的任务管理器。当任务管理器再次唤醒它时,它会在寄存器 R0 中放入一个事件代码。这些代码行

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.

显示任务通过执行由返回的事件代码索引的操作方法来响应。RISC OS 的非抢占式多任务就是这样工作的。当任务初始化时,它会设置自己的错误处理程序以在窗口中输出错误消息,并在关闭之前恢复以前的错误处理程序。 使用 w_task 函数以及用于加载窗口和菜单模板的类似库函数,程序员只需为事件定义处理程序方法,例如:

  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:

​ 尽管示例包含对不熟悉 RISC OS 的人来说意义不大的细节,但基本原理对于其他平台应该大致相同:

  • a C function for system calls, 用于系统调用的 C 函数,
  • a C function for allocating buffers, 一个用于分配缓冲区的 C 函数,
  • C functions for reading and writing Lua variables to buffers, 用于将 Lua 变量读写到缓冲区的 C 函数,
  • wrapper objects in Lua concealing these functions as methods, 将这些函数作为方法隐藏起来的 Lua 中的包装器对象,
  • constructor functions using system calls to create task objects. 使用系统调用创建任务对象的构造函数。
最后修改 January 31, 2024: 更新 (e0896df)