技術ノート 3このノートでは、システムコールを利用するために Lua を拡張する方法を説明します。私の個人的な取り組みは、ほとんどの読者には知られていないかもしれないオペレーティングシステム(RISC OS)に限られていますが、関係する原則はかなり普遍的であると信じています。私は、有用な批判を得ることを期待して、このノートを書いています。これは、RiscLua の実装において私が行ったことの概要です。
RISC OS は、特定のプロセッサファミリーである ARM 向けに設計されました。ユーザープログラムは、特定のプロセッサ命令である SWI (SoftWare Interrupt) を介してのみ RISC OS と対話します。すべてのプロセッサには、これに類似したものがありますが、おそらく異なる名前 (TRAP?) で呼ばれています。ソフトウェア割り込みの使用には、以下の手順が含まれます。
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.bをx.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関数をどのように隠しているかに注目してください。プレリュードファイルでarrayとswiが定義されていれば、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 に不慣れな人にとってはあまり意味のない詳細が含まれていますが、基本的な原則は他のプラットフォームでもほとんど同じであるはずです。