FlowingDev

Lua徹底解説:あの大人気ゲームを動かす「接着剤」スクリプト

Roblox、World of Warcraft、Angry Birdsといった有名ゲームを支える、軽量で組み込みやすいスクリプト言語、Luaの世界へようこそ。

ツールを試す: Lua Playground

一言で言うと

Luaは、羽のように軽く、高速で、簡単に組み込めるスクリプト言語。これを使えば、開発者はもっと大きくて複雑なアプリケーションに柔軟なロジックを追加するのに、いちいち全体を再コンパイルする手間から解放されるんだ。

こいつが解決する問題

想像してみてほしい。舞台は1990年代初頭のブラジル、リオデジャネイロ。国営石油会社ペトロブラスのチームが、複雑なエンジニアリングや地質学のシミュレーションソフトウェアを設定する必要に迫られていた。このソフトウェアは、CやFortranのようなコンパイル言語で書かれたモノリス(一枚岩)だ。地質学者がパラメータをちょっといじったり、ビジネスルールを変更したりするたびに、プログラマーがソースコードに潜り込み、変更を加え、巨大なアプリケーション全体を再コンパイル(これが死ぬほど時間がかかる)し、再デプロイしなければならなかった。このプロセスは苦痛なほど遅く、エラーも起きやすく、関係者全員の時間をひどく無駄にしていた。

エンジニアたちはもっと良い方法を必要としていた。彼らが欲しかったのは、専門家(この場合は地質学者)がプログラマーに泣きつくことなく、自分たちのルールや設定を定義できる、シンプルで宣言的な「データ記述」言語だった。

既存のツールを探してみたが、結果は空振り。そこで、優れたプログラマーの常として、彼らは自分たちで作ることに決めた。リオ・デ・ジャネイロ・カトリック大学(PUC-Rio)で、必要に迫られて生まれたこのプロジェクトは、Lua――ポルトガル語で「月」を意味する――と名付けられた。

単純な設定言語として始まったLuaは、すぐに本格的なスクリプト言語へと進化した。しかし、その中心的なアイデアは変わらなかった。つまり、単独で動くのではなく、より大きな「ホスト」アプリケーションに組み込まれることを前提とした、小さく、速く、ポータブルな言語を作ることだ。ホストプログラム(C/C++で書かれる)が重い処理やパフォーマンスが重要なエンジン部分を担い、一方でLuaが柔軟で簡単に変更できる「頭脳」や「接着剤」の役割を果たす。この分離により、開発者はコンパイルされたコアに触れることなく、アプリケーションのロジックをその場で変更したり、ユーザーが独自の拡張機能を書けるようにしたり、動作を設定したりできるようになった。

内部の仕組み

Luaの魔法は、そのミニマリストな設計と、ホスト言語(ほとんどの場合CかC++)との緊密な連携から生まれている。これは単独で動く怪物じゃなくて、自分のプロジェクトに招待するライブラリなんだ。

C API:双方向コミュニケーション

Luaの組み込み能力の心臓部が、C APIだ。これは、C/C++のホストアプリケーションとLuaスクリプトが互いに会話するための橋渡し役となる。美しく設計された、双方向のコミュニケーションチャネルだ。

  • CからLuaを呼ぶ: ホストアプリケーションは、ファイルや文字列からLuaスクリプトを読み込み、それを実行したり、その中の特定の関数を呼び出したりできる。
  • LuaからCを呼ぶ: Luaスクリプトは、実際にはCで書かれ、ホストによってLuaステートに登録された関数を呼び出すことができる。

このコミュニケーションは直接行われるわけではない。仮想スタックと呼ばれる特別なデータ構造を介して行われる。

君がCのホスト側で、Luaの関数 add(x, y) を呼び出したいとしよう。ただ呼び出すわけじゃない。代わりに、ちょっとした儀式を行うんだ。

  1. add 関数をスタックにプッシュする。
  2. 最初の引数 (x) をスタックにプッシュする。
  3. 2番目の引数 (y) をスタックにプッシュする。
  4. Luaに、スタックのトップにある関数を、その下にある2つの引数で呼び出すよう指示する。
  5. Luaが関数を実行し、結果をスタックにプッシュして返す。
  6. 君(Cホスト側)が、スタックから結果をポップして使用する。
// C側の簡略化された例
lua_getglobal(L, "add");      // Luaの 'add' 関数をプッシュ
lua_pushnumber(L, 10);        // 最初の引数をプッシュ
lua_pushnumber(L, 5);         // 2番目の引数をプッシュ
lua_pcall(L, 2, 1, 0);        // 2つの引数、1つの戻り値で関数を呼び出す
int sum = lua_tonumber(L, -1); // スタックのトップから結果を取得
lua_pop(L, 1);                // スタックをクリーンアップ

このスタックベースのアプローチは、言語の内部実装を疎結合にし、APIがバージョンを越えて安定かつ堅牢であることを保証している。

すべてを統べる唯一のデータ構造:テーブル

Luaプログラマーにその決定的な特徴は何かと尋ねれば、きっと「テーブル」と答えるだろう。徹底的なシンプルさの追求の末に、Luaには組み込みのデータ構造がただ一つしかない。それがテーブルだ。テーブルは驚くほど多機能な連想配列で、配列、辞書(ハッシュマップ)、集合、レコード、さらにはメソッドを持つオブジェクトまで作れてしまう。

  • 配列として: Luaは整数のキーを使う。慣例として、0からではなく1から始まる。
    local colors = { "red", "green", "blue" } -- colors[1] は "red"
    
  • 辞書/マップとして: 文字列や(nil以外の)他の値をキーとして使う。
    local user = {
      name = "Alice",
      id = 123,
      is_active = true
    }
    print(user.name) -- user["name"] のシンタックスシュガー
    
  • メソッドを持つオブジェクトとして: 関数は第一級オブジェクトなので、テーブルに格納することもできる。
    local counter = { value = 0 }
    function counter:increment() -- ':' を使うと自動的に 'self' が提供される
      self.value = self.value + 1
    end
    counter:increment()
    print(counter.value) -- 1 が表示される
    

このテーブルへの一本化が、言語を信じられないほどスリムで一貫性のあるものにしている。5つの異なるデータ構造を学ぶ必要はない。1つをマスターし、その多くのパターンを学ぶだけだ。

VMとガベージコレクション

Luaスクリプトを実行するとき、それは昔ながらの遅い方法で一行ずつ解釈されるわけではない。代わりに、まずバイトコードにコンパイルされる。これは低レベルで、プラットフォームに依存しない命令セットだ。このバイトコードが、Lua仮想マシン(VM)によって実行される。

LuaのVMは、ミニマリスト工学の傑作だ。これはレジスタベースのVMで、多くの場合スタックベースのVMよりも高速だ。そして、コアとなるインタプリタ、コンパイラ、標準ライブラリ全体を合わせても、わずか数百キロバイトしかない。この極小のフットプリントこそが、Luaが巨大なゲームサーバーから小さなマイクロコントローラーまで、あらゆるもので動作する理由だ。

メモリ管理のために、Luaはインクリメンタル・ガベージコレクタ(GC)を使用する。多くの言語では、ガベージコレクションが目に見える「世界を止める(stop-the-world)」一時停止を引き起こし、アプリケーションがメモリをクリーンアップするために一瞬フリーズすることがある。これは、突然のカクつきがラグやフレーム落ちを意味するゲームやリアルタイムシステムにとっては致命的だ。LuaのGCは、小さなステップで段階的に動作するように設計されており、短時間だけ実行された後、メインプログラムの続行を許可する。これによりクリーンアップ作業が時間的に分散され、はるかにスムーズで中断のない体験が実現される。

実世界でのストーリー

ゲームMOD制作者の秘密兵器

あるプレイヤーがWorld of Warcraftにどっぷりハマっている。混沌としたボス戦の最中、複数の呪文のクールダウンや敵のデバフを把握しようと四苦八苦していた。デフォルトのUIではもう限界だ。しかし、プレイヤーはそこで立ち往生することなく、WeakAurasという人気のUIアドオンをダウンロードした。グラフィカルなインターフェースを使うか、あるいは少量のLuaコードを直接書くことで、彼は自分が必要とする情報を、画面上のまさにその場所に表示するカスタムディスプレイを作成した。ゲームを再コンパイルしたり、C++を知っている必要はなかった。Blizzardがまさにこの目的のために公開した、安全なサンドボックス化されたLua環境を使っただけだ。

ここから得られる教訓: Luaを組み込むことで、強力で安全なプラグインアーキテクチャが生まれ、アプリケーションの寿命と魅力を、元の開発者が単独で構築できたであろう範囲をはるかに超えて延ばす、巨大なユーザー生成コンテンツのコミュニティを育むことができる。

Redisの高速化

あるeコマース企業のバックエンド開発者が、「クーポン獲得」機能を実装する必要があった。ロジックはこうだ:クーポンコードが存在するか確認し、その使用回数が上限未満か確認し、両方が真であれば使用回数を1増やす。これをウェブサーバーから行うには、Redisデータベースに対して3つの別々のコマンドが必要で、それぞれにネットワークの往復が発生する。これは遅いし、さらに悪いことに、2人のユーザーが最後のクーポンを同時に獲得してしまう競合状態を生み出す。解決策は?彼らは3つのステップすべてを実行する小さなLuaスクリプトを書き、それを実行のためにRedisに送信した。組み込みのLuaインタプリタを持つRedisは、スクリプト全体をアトミックに(中断されることなく)、データのすぐそばにあるデータベース内部で実行する。これは超高速で、競合状態からも完全に安全だ。

ここから得られる教訓: Luaのような組み込み言語を使ってロジックをデータ側に移動させることで、ネットワーク遅延を劇的に削減し、複雑な並行処理の問題をシンプルでアトミックな操作で解決できる。

よくある間違いと落とし穴

  • 1ベースのインデックスは誰もが通る道。 C、Java、Python、JavaScriptなど、ほとんどのポピュラーな言語は0ベースの配列(最初の要素がインデックス0にある)を使用する。Luaは1ベースのインデックスを使用する。これを忘れて for i = 0, #my_table - 1 do ... end と書くのは、nil値と混乱につながる通過儀礼だ。正しいLuaのイディオムは for i = 1, #my_table do ... end か、ipairs を使うことだ。
  • localを忘れると、グローバル変数の幽霊が出る。 関数内で my_var = "hello" のように変数を宣言すると、その関数にローカルだと思うかもしれない。だが、そうではない。デフォルトでは、Luaの変数はすべてグローバルだ。これは、コードのある部分が、まったく関係のない別の部分で使われている変数を誤って上書きしてしまうという、恐ろしいバグにつながりかねない。グローバル変数を作成する明確な理由がない限り、常に変数は local キーワードで宣言しよう。
  • nilの力を誤解する。 Luaでは、nilは単なる「null」じゃない。値が存在しないことを表す。存在しないテーブルキーにアクセスすると nil が返る。しかし決定的に重要なのは、テーブルキーに nil を代入すると、そのキーがテーブルから削除されるということだ。my_table.key = nil は key を削除する方法なのだ。これは効率的だが、キーが nil の値とともに残ると期待していると驚くかもしれない。
  • 文字列連結は..。 文字列連結に + を使う言語から来た多くの初心者は、"hello " + "world" と書いてしまう。これはLuaではエラーを吐く。+ は厳密に算術用だからだ。文字列連結演算子は2つのドット、.. だ: "hello " .. "world"。

なぜLuaに注目すべきか

スタンドアロンのデスクトップアプリやウェブサイトを完全にLuaだけで作ることはないだろう。それはLuaの目的ではないからだ。Luaについて考えるべきなのは、こんなときだ:

  • アプリケーション(特にC、C++、Rust製)を構築していて、ユーザーにスクリプト機能やカスタマイズ方法を提供したいとき。
  • Roblox、LÖVE、Defoldといったゲームエンジンでゲーム開発を始めるとき。
  • Redisデータベース、Nginxウェブサーバー、Wiresharkネットワークアナライザ、Neovimテキストエディタなど、Luaを組み込んでいるツール用のスクリプトを書く必要があるとき。
  • リソースに制約のある組み込みシステムで作業していて、小さなメモリフットプリントを持つ高レベルなスクリプト言語が必要なとき。
  • シンプルさ、ポータビリティ、そしてエレガンスを重視する設計を研究したい言語オタクであるとき。

Luaは究極の「相棒」言語だ。強力なコンパイル済みコアに対する、柔軟でダイナミックなパートナーなんだ。

もっと深く知るには

  • Official Lua Website: すべての真実、ダウンロード、ドキュメントの源泉。https://www.lua.org/
  • Programming in Lua (first edition): Luaの主任設計者であるRoberto Ierusalimschyによって書かれた、Luaに関する決定版の書籍。初版はオンラインで無料で入手でき、素晴らしいリソースだ。https://www.lua.org/pil/
  • Lua 5.4 Reference Manual: 公式仕様書。何かが正確にどのように機能するかを知る必要があるとき、ここが目的地だ。https://www.lua.org/manual/5.4/
  • Lua: a lightweight, embeddable scripting language (Wikipedia): その歴史、機能、応用に関する優れた高レベルの概要。https://en.wikipedia.org/wiki/Lua_(programming_language)
  • The Evolution of Lua (paper): 1993年から2018年にかけて言語を形作った歴史と設計上の決定について、作成者たちによる魅力的な論文。https://www.lua.org/doc/hopl.pdf

エディターはどうコードを理解するのか

色分けはプログラムの実行ではなく、言語パーサーによるものです。CodeMirror と Lezer がブラウザー内で記号と構文を解析します。候補にはキーワード、対応する場合のローカル名、スニペット、小さな組み込みリファレンスが含まれます。登録済みのシンボルにカーソルを重ねるとシグネチャが表示されます。候補が出ても、その API が実行環境に存在する保証はありません。対応するパーサーは構文エラーを検出しますが、型検査や実行の代わりにはなりません。Ruby と Lua はトークンベースのモードで、診断は限定的です。リモートの言語サーバーは使わず、コードは端末内に留まります。

CodeMirror & Lezer: MIT.

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: Lua Playground