Lua 技術備註 3

Lua 與作業系統介接

作者 Gavin Wraith

本備註說明如何擴充 Lua 以利用系統呼叫。儘管我自己的努力僅限於大多數讀者可能不知道的作業系統 (RISC OS),但我相信所涉及的原理相當普遍。我撰寫本備註是希望獲得有用的批評。這是我在實作 RiscLua 時所做的摘要。

RISC OS 是為特定處理器系列 ARM 設計的。使用者程式僅透過特定處理器指令 SWI(軟體中斷)與 RISC OS 互動。每個處理器都有此指令的類比,儘管名稱可能不同(TRAP?)。使用軟體中斷涉及以下步驟

  1. 使用適當資料撰寫一些處理器暫存器(其中一些可能是程式記憶體區域中固定位址的指標)。
  2. 呼叫 SWI。
  3. 讀取一些暫存器。
實際上,處理器暫存器的子集僅用於程式與作業系統之間傳遞資料,即 R0、R1、...、R7。所有暫存器都是 32 位元寬。需要七個指令碼才能產生 C 函式
 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 的資料類型是垃圾收集的,因此我們必須使用使用者資料類型來實作這些固定陣列。我們為指向這些陣列的使用者資料指定一個特定標籤,稱為「可寫入」。以下是函式 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;   
      }

對於內建的 lua 函式 dim(n),它會產生一個使用者資料,其可寫入標籤指向一個固定緩衝區,其中包含 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:
最後更新:2002 年 8 月 12 日星期一下午 3:48:51 美國東部時間,由 lhf 執行。