Lua 技術ノート 3

Lua とオペレーティングシステムとのインターフェース

執筆: ギャビン・レイス

このノートでは、システムコールを利用するために Lua を拡張する方法を説明します。私の個人的な取り組みは、ほとんどの読者には知られていないかもしれないオペレーティングシステム(RISC OS)に限られていますが、関係する原則はかなり普遍的であると信じています。私は、有用な批判を得ることを期待して、このノートを書いています。これは、RiscLua の実装において私が行ったことの概要です。

RISC OS は、特定のプロセッサファミリーである ARM 向けに設計されました。ユーザープログラムは、特定のプロセッサ命令である SWI (SoftWare Interrupt) を介してのみ RISC OS と対話します。すべてのプロセッサには、これに類似したものがありますが、おそらく異なる名前 (TRAP?) で呼ばれています。ソフトウェア割り込みの使用には、以下の手順が含まれます。

  1. 適切なデータ(プログラムのメモリ領域内の固定アドレスへのポインタを含む場合がある)をいくつかのプロセッサレジスタに書き込みます。
  2. SWI を呼び出します。
  3. いくつかのレジスタを読み取ります。
実際には、プログラムとオペレーティングシステムの間でデータを渡すために使用されるのは、プロセッサレジスタのサブセット、すなわち R0、R1、...、R7 のみです。すべてのレジスタは 32 ビット幅です。SWI 呼び出しを行う C 関数を作成するには、7 つの命令が必要です。
extern void swi_call(int swi_number, void * regbuffer);

次の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 のデータ型はガベージコレクションされるため、これらの固定配列を userdata 型を使用して実装する必要があります。これらの配列を指す userdata 用に、「writeable」と呼ばれる特定のタグを割り当てます。以下は、関数用の 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;   
      }

組み込み lua 関数用dim(n)これは、nバイトを保持する固定バッファを指す writeable タグを持つ userdatum を生成します。さらに、固定バッファからデータを 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

これらのメソッドは、グローバル関数array_xxxという名前の値を持っています。「words」メソッドは 32 ビット値を読み取るために使用され、「chars」メソッドは 8 ビット値を読み取るために使用されます。これらは、固定バッファへのオフセットを与える整数でインデックス付けされたテーブルを引数として取ります。テーブルの値は、chars の場合は数値 (バイト値の場合) または文字列 (複数バイトの場合) にすることができ、"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

関数putword, putbyteは、明白なことを行う組み込み C 関数です。結果として、たとえば
  x,y = array(n),array(m)

を定義すると、
  x:chars { [0] = "hello".."\0" } -- only 6 bytes taken up so far
  x:words { [2] = a_num, [3] = y }

数値を格納するa_numをバイト 8、9、10、11 に格納し、userdatumy.bx.b.

が指す固定バッファのバイト 12、13、14、15 に格納できます。他のメソッドは、固定バッファに格納されている整数、文字列、およびポインタを読み取るためのものです。したがって、x:int(2)a_numは、x:str(0)は、"hello"を生成する必要があります。これが、固定バッファの読み取りと書き込みの構文を説明するものです。

オペレーティングシステムへの実際のインターフェースは、以下によって提供されます。


swi = {
        regs = array(32),
        call = function (self,x)
                 %swi(x,self.regs.b)
                end
      }
    
「call」メソッドが、上記の生のswi関数をどのように隠しているかに注目してください。プレリュードファイルでarrayswiが定義されていれば、Lua を使用してオペレーティングシステムが提供するすべてを利用することができます。もちろん、このプレリュードはまだ非常に低レベルですが、RISC OS のグラフィカルユーザーインターフェイスを使用する「wimp」(Windows Icons Menus Pointers)プログラムを作成するためのライブラリを構築するのに十分な機能を提供します。ここで、システムコールをどのように使用できるかの例として、wimp タスクを作成する関数w_taskを定義する Lua コードを以下に示します。
 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  

wimp タスクが初期化され、そのデータを設定すると、「poll」メソッドを呼び出してスリープ状態になり、RISC OS カーネルのタスクマネージャーに実行を引き渡します。タスクマネージャーが再びタスクを起こすと、レジスタ R0 にイベントコードを配置します。
 f = self.action[swi.regs:int(0)]
      if f then quit = f(self,uservar) end

行は、タスクが返されたイベントコードでインデックス付けされたアクションメソッドを実行することで応答することを示しています。これが、RISC OS の非プリエンプティブマルチタスクが機能する方法です。タスクが初期化されると、ウィンドウにエラーメッセージを出力するための独自のエラーハンドラーを設定し、シャットダウンする前に以前のエラーハンドラーを復元します。w_task関数、およびウィンドウとメニューのテンプレートをロードするための同様のライブラリ関数を使用すると、プログラマーが行う必要があるのは、イベントのハンドラーメソッドを定義することだけです。例:
  mytask = w_task("MyTask",310, { [0] = M_DataLoad, [1] = M_Quit })
  .....................
    
  mytask.action[Mouse_Click] = function (self) ........ end
  .....................
                                 
  mytask:poll()

例には、RISC OS に不慣れな人にとってはあまり意味のない詳細が含まれていますが、基本的な原則は他のプラットフォームでもほとんど同じであるはずです。
最終更新日: 2002 年 8 月 12 日 月曜日 15:48:51 EST (執筆者: lhf)。