この初版は Lua 5.0 向けに書かれています。後のバージョンにも概ね該当しますが、いくつかの違いがあります。
第4版は Lua 5.3 を対象としており、Amazon や他の書店で入手できます。
この本を購入することで、Lua プロジェクトの支援にもなります。


29.2 – XMLパーサー

ここでは、Lua と Expat を結びつけるバインディングである lxp の簡略化された実装を見ていきます。Expat は C で書かれたオープンソースの XML 1.0 パーサーです。これは SAX、つまり *Simple API for XML* を実装しています。SAX はイベントベースの API です。つまり、SAX パーサーは XML ドキュメントを読み込み、読み込みながら、コールバックを通じてアプリケーションに検出した内容を報告します。たとえば、Expat に

    <tag cap="5">hi</tag>
のような文字列を解析するように指示すると、3 つのイベントが生成されます。部分文字列 "<tag cap="5">" を読み込むときの *開始要素* イベント、"hi" を読み込むときの *テキスト* イベント(*文字データ* イベントとも呼ばれます)、そして "</tag>" を読み込むときの *終了要素* イベントです。これらのイベントはそれぞれ、アプリケーション内の適切な *コールバックハンドラ* を呼び出します。

ここでは、Expat ライブラリ全体を網羅するわけではありません。Lua との相互作用のための新しいテクニックを示す部分のみに焦点を当てます。このコア機能を実装した後、必要に応じて機能を追加することは容易です。Expat は 12 種類以上のイベントを処理しますが、ここでは前の例で見た 3 つのイベント(開始要素、終了要素、テキスト)のみを考慮します。この例に必要な Expat API の部分はわずかです。まず、Expat パーサーを作成および破棄するための関数が必要です。

    #include <xmlparse.h>
    
    XML_Parser XML_ParserCreate (const char *encoding);
    void XML_ParserFree (XML_Parser p);
引数 encoding は省略可能です。このバインディングでは NULL を使用します。

パーサーを作成したら、そのコールバックハンドラを登録する必要があります。

    XML_SetElementHandler(XML_Parser p,
                          XML_StartElementHandler start,
                          XML_EndElementHandler end);
    
    XML_SetCharacterDataHandler(XML_Parser p,
                                XML_CharacterDataHandler hndl);
最初の関数は、開始要素と終了要素のハンドラを登録します。2 番目の関数は、テキスト(XML の用語では *文字データ*)のハンドラを登録します。

すべてのコールバックハンドラは、最初の引数としてユーザーデータを受け取ります。開始要素ハンドラは、タグ名とその属性も受け取ります。

    typedef void (*XML_StartElementHandler)(void *uData,
                                            const char *name,
                                            const char **atts);
属性は NULL 終端の文字列配列として渡され、連続する 2 つの文字列がそれぞれ属性名とその値を保持します。終了要素ハンドラには、タグ名という追加の引数が 1 つだけあります。
    typedef void (*XML_EndElementHandler)(void *uData,
                                          const char *name);
最後に、テキストハンドラは追加の引数としてテキストのみを受け取ります。このテキスト文字列は NULL 終端ではありません。代わりに、明示的な長さを持ちます。
    typedef void
    (*XML_CharacterDataHandler)(void *uData,
                                const char *s,
                                int len);

Expat にテキストを入力するには、次の関数を使用します。

    int XML_Parse (XML_Parser p,
                   const char *s, int len, int isFinal);
Expat は、XML_Parse を連続して呼び出すことで、解析されるドキュメントを断片的に受け取ります。XML_Parse の最後の引数である isFinal は、その断片がドキュメントの最後の部分であるかどうかを Expat に通知します。テキストの各部分は NULL 終端である必要はなく、代わりに明示的な長さを指定することに注意してください。XML_Parse 関数は、解析エラーを検出した場合にゼロを返します。(Expat はエラー情報を取得するための補助関数を備えていますが、ここでは簡潔にするために無視します。)

Expat から必要な最後の関数は、ハンドラに渡されるユーザーデータを設定できるようにするものです。

    void XML_SetUserData (XML_Parser p, void *uData);

それでは、Lua でこのライブラリをどのように使用できるかを見てみましょう。最初の方法は直接的な方法です。これらの関数をすべて Lua にエクスポートするだけです。より良い方法は、機能を Lua に適応させることです。たとえば、Lua は型指定されていないため、各種のコールバックを設定するために異なる関数を使用する必要はありません。さらに、コールバック登録関数をまったく使用しないようにすることができます。代わりに、パーサーを作成するときに、適切なキーを持つすべてのコールバックハンドラを含むコールバックテーブルを指定します。たとえば、ドキュメントのレイアウトを出力したいだけの場合は、次のコールバックテーブルを使用できます。

    local count = 0
    
    callbacks = {
      StartElement = function (parser, tagname)
        io.write("+ ", string.rep("  ", count), tagname, "\n")
        count = count + 1
      end,
    
      EndElement = function (parser, tagname)
        count = count - 1
        io.write("- ", string.rep("  ", count), tagname, "\n")
      end,
    }
入力 "<to> <yes/> </to>" を与えると、これらのハンドラは次のように出力します。
    + to
    +   yes
    -   yes
    - to
この API では、コールバックを操作するための関数は必要ありません。コールバックテーブルで直接操作します。したがって、API 全体で必要な関数は 3 つだけです。パーサーを作成する関数、テキストの一部を解析する関数、パーサーを閉じる関数です。(実際には、最後の 2 つの関数はパーサーオブジェクトのメソッドとして実装します。)API の典型的な使い方は次のようになります。
    p = lxp.new(callbacks)     -- create new parser
    for l in io.lines() do     -- iterate over input lines
      assert(p:parse(l))               -- parse the line
      assert(p:parse("\n"))            -- add a newline
    end
    assert(p:parse())        -- finish document
    p:close()


それでは、実装に移りましょう。最初の決定は、Lua でパーサーをどのように表現するかです。ユーザーデータを使用するのが自然ですが、その中に何を配置する必要があるでしょうか?少なくとも、実際の Expat パーサーとコールバックテーブルを保持する必要があります。ユーザーデータ(または C 構造体)の中に Lua テーブルを格納することはできません。ただし、テーブルへの参照を作成し、その参照をユーザーデータの中に格納することはできます。(セクション 27.3.2 で、参照はレジストリ内の Lua によって生成された整数キーであることを思い出してください。)最後に、パーサーオブジェクトに Lua 状態を格納できる必要があります。なぜなら、これらのパーサーオブジェクトは Expat コールバックがプログラムから受け取るすべてであり、コールバックは Lua を呼び出す必要があるからです。したがって、パーサーオブジェクトの定義は次のとおりです。

    #include <xmlparse.h>
    
    typedef struct lxp_userdata {
      lua_State *L;
      XML_Parser *parser;          /* associated expat parser */
      int tableref;   /* table with callbacks for this parser */
    } lxp_userdata;

次のステップは、パーサーオブジェクトを作成する関数です。ここに示します。

    static int lxp_make_parser (lua_State *L) {
      XML_Parser p;
      lxp_userdata *xpu;
    
      /* (1) create a parser object */
      xpu = (lxp_userdata *)lua_newuserdata(L,
                                       sizeof(lxp_userdata));
    
      /* pre-initialize it, in case of errors */
      xpu->tableref = LUA_REFNIL;
      xpu->parser = NULL;
    
      /* set its metatable */
      luaL_getmetatable(L, "Expat");
      lua_setmetatable(L, -2);
    
      /* (2) create the Expat parser */
      p = xpu->parser = XML_ParserCreate(NULL);
      if (!p)
        luaL_error(L, "XML_ParserCreate failed");
    
      /* (3) create and store reference to callback table */
      luaL_checktype(L, 1, LUA_TTABLE);
      lua_pushvalue(L, 1);  /* put table on the stack top */
      xpu->tableref = luaL_ref(L, LUA_REGISTRYINDEX);
    
      /* (4) configure Expat parser */
      XML_SetUserData(p, xpu);
      XML_SetElementHandler(p, f_StartElement, f_EndElement);
      XML_SetCharacterDataHandler(p, f_CharData);
      return 1;
    }
lxp_make_parser 関数には、4 つの主要なステップがあります。

次のステップは、XML データの一部を解析する parse メソッドです。これは 2 つの引数を取ります。パーサーオブジェクト(メソッドの *self*)とオプションの XML データの一部です。データなしで呼び出されると、ドキュメントにそれ以上の部分がないことを Expat に通知します。

    static int lxp_parse (lua_State *L) {
      int status;
      size_t len;
      const char *s;
      lxp_userdata *xpu;
    
      /* get and check first argument (should be a parser) */
      xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
      luaL_argcheck(L, xpu, 1, "expat parser expected");
    
      /* get second argument (a string) */
      s = luaL_optlstring(L, 2, NULL, &len);
    
      /* prepare environment for handlers: */
      /* put callback table at stack index 3 */
      lua_settop(L, 2);
      lua_getref(L, xpu->tableref);
      xpu->L = L;  /* set Lua state */
    
      /* call Expat to parse string */
      status = XML_Parse(xpu->parser, s, (int)len, s == NULL);
    
      /* return error code */
      lua_pushboolean(L, status);
      return 1;
    }
lxp_parseXML_Parse を呼び出すと、後者の関数は、指定されたドキュメントの一部で見つかった各関連要素のハンドラを呼び出します。したがって、lxp_parse はまずこれらのハンドラの環境を準備します。XML_Parse の呼び出しには、もう 1 つの詳細があります。この関数の最後の引数は、指定されたテキストが最後のものであるかどうかを Expat に伝えることを思い出してください。引数なしで parse を呼び出すと、sNULL になるので、この最後の引数は true になります。

それでは、コールバック関数 f_StartElementf_EndElementf_CharData に注目しましょう。これら 3 つの関数はすべて同様の構造を持っています。それぞれは、コールバックテーブルが特定のイベントの Lua ハンドラを定義しているかどうかをチェックし、定義されている場合は引数を準備して Lua ハンドラを呼び出します。

まず、f_CharData ハンドラを見てみましょう。そのコードは非常にシンプルです。Lua の対応するハンドラ(存在する場合)を、パーサーと文字データ(文字列)の 2 つの引数だけで呼び出します。

    static void f_CharData (void *ud, const char *s, int len) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      /* get handler */
      lua_pushstring(L, "CharacterData");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushlstring(L, s, len);  /* push Char data */
      lua_call(L, 2, 0);  /* call the handler */
    }
パーサーを作成するときに XML_SetUserData を呼び出したため、これらすべての C ハンドラは最初の引数として lxp_userdata 構造体を受け取ることに注意してください。また、lxp_parse によって設定された環境をどのように使用しているかに注意してください。まず、コールバックテーブルがスタックインデックス 3 にあると仮定します。次に、パーサー自体がスタックインデックス 1 にあると仮定します(lxp_parse の最初の引数である必要があるため、そこに存在する必要があります)。

f_EndElement ハンドラもシンプルで、f_CharData と非常によく似ています。対応する Lua ハンドラを、パーサーとタグ名(これも文字列ですが、NULL 終端)の 2 つの引数で呼び出します。

    static void f_EndElement (void *ud, const char *name) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      lua_pushstring(L, "EndElement");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushstring(L, name);  /* push tag name */
      lua_call(L, 2, 0);  /* call the handler */
    }

最後のハンドラである f_StartElement は、パーサー、タグ名、属性のリストの 3 つの引数で Lua を呼び出します。このハンドラは、タグの属性リストを Lua に変換する必要があるため、他のハンドラよりも少し複雑です。非常に自然な翻訳を使用します。たとえば、次のような開始タグがあるとします。

    <to method="post" priority="high">
は、次の属性テーブルを生成します。
    { method = "post", priority = "high" }
f_StartElement の実装は次のとおりです。
    static void f_StartElement (void *ud,
                                const char *name,
                                const char **atts) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      lua_pushstring(L, "StartElement");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushstring(L, name);  /* push tag name */
    
      /* create and fill the attribute table */
      lua_newtable(L);
      while (*atts) {
        lua_pushstring(L, *atts++);
        lua_pushstring(L, *atts++);
        lua_settable(L, -3);
      }
    
      lua_call(L, 3, 0);  /* call the handler */
    }

パーサーの最後のメソッドは close です。パーサーを閉じるときは、すべてのリソース、つまり Expat 構造体とコールバックテーブルを解放する必要があります。作成中にエラーが発生した場合、パーサーにこれらのリソースがない可能性があることに注意してください。

    static int lxp_close (lua_State *L) {
      lxp_userdata *xpu;
    
      xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
      luaL_argcheck(L, xpu, 1, "expat parser expected");
    
      /* free (unref) callback table */
      luaL_unref(L, LUA_REGISTRYINDEX, xpu->tableref);
      xpu->tableref = LUA_REFNIL;
    
      /* free Expat parser (if there is one) */
      if (xpu->parser)
        XML_ParserFree(xpu->parser);
      xpu->parser = NULL;
      return 0;
    }
パーサーを閉じる際に、パーサーを一貫した状態に保っていることに注意してください。そのため、再度閉じようとしたり、ガベージコレクタがファイナライズしたりしても問題ありません。実際、この関数をファイナライザとして使用します。これにより、プログラマーが閉じなくても、すべてのパーサーが最終的にリソースを解放することが保証されます。

最後のステップは、これらすべての部分をまとめてライブラリを開くことです。ここでは、オブジェクト指向配列の例(セクション 28.3)で使用したのと同じスキームを使用します。メタテーブルを作成し、すべてのメソッドをその中に配置し、その __index フィールドをそれ自体に指すようにします。そのためには、パーサーメソッドのリストが必要です。

    static const struct luaL_reg lxp_meths[] = {
      {"parse", lxp_parse},
      {"close", lxp_close},
      {"__gc", lxp_close},
      {NULL, NULL}
    };
また、このライブラリの関数のリストも必要です。OO ライブラリではよくあることですが、このライブラリには新しいパーサーを作成する関数が 1 つだけあります。
    static const struct luaL_reg lxp_funcs[] = {
      {"new", lxp_make_parser},
      {NULL, NULL}
    };
最後に、open 関数はメタテーブルを作成し、それを(__index を介して)それ自体に指し示し、メソッドと関数を登録する必要があります。
    int luaopen_lxp (lua_State *L) {
      /* create metatable */
      luaL_newmetatable(L, "Expat");
    
      /* metatable.__index = metatable */
      lua_pushliteral(L, "__index");
      lua_pushvalue(L, -2);
      lua_rawset(L, -3);
    
      /* register methods */
      luaL_openlib (L, NULL, lxp_meths, 0);
    
      /* register functions (only lxp.new) */
      luaL_openlib (L, "lxp", lxp_funcs, 0);
      return 1;
    }