Luaの現状

Lua 1.119941.0はリリースしていない
Lua 5.12006ガベージコレクションができたよ
Lua 5.22011gotoのサポート
Lua 5.32015intが64bitになったよ / UTF-8をサポート
Lua 5.3.520192018年の時点で5.4は上がっているが2019年でもまだ公式リリースになっていない
Lua 5.4.02019https://www.lua.org/work/

2019年の状況

4月時点で5.3.5が最新。リビジョンがわずかに上がっているが、すでに安定しているので使い方に違いはなさそう。ベータ版としては5.4.0が上がっている。

2017年の状況

2015年以降は特にアップデートがない。5.3で止まっているので特に情報の更新はしなくてよさそう

2016年の状況

Luaが出てきてから久しいがそういえばここ3~4年くらい触っていない。最新のLua環境ってどうなってるのか調べてみた。1994年に初版がリリースされてからもう20年近く経ってることになる。触りだしたのが2006年以降なので、ちょうどver5.1くらいか。で、そこから10年経つけどメジャーバージョンアップなしに、ちょうど昨年5.3にバージョンアップしたらしい。

大きな変更はないにしろ4年ぶりの更新なのでCライブラリ側でも最新環境にしておくことにする。

Luaのダウンロード
https://www.lua.org/download.html

しかし、Luaの使いみちがよくわからない

便利そうでバグもそれなりに取れて安定してそうなLua。長年使いドコロを考えてきたけど今だに「コレだ!」っていう使い方が見つからない。
「ゲームのステージ構成を作るのに便利ですよ!」っていうのを聞いたり、あの有名タイトルでもこのタイトルでも使われてるよ!っていうのはよく聞くけど、正直言ってどこが便利かわからないところがある。プランナーがリコンパイルなしに、ステージをプログラミングできるところがいいところだという。しかし、プランナーはプログラミングしないしリコンパイルもしないやんか。

結局プログラムが必要なところはプログラマが触るんだったら、きちんとしたデバッガーがあるC言語環境の方がいい。ブレークポイントに変数ウォッチ、ステップ実行、エディット&コンティニューがあればこそ便利なことは多々あるが、それらがないLua環境で色々複雑な仕組みを作った結果「何かわからないけどバグった、プログラマーさん調べて」ってなるよね。

コンパイルしなくていいのは同意。高いコンパイラを買ったりしなくていいし、小難しい環境設定をしなくていいのも魅力。結局環境をLuaで代用できるから安く済む、っていう経営側のメリットはあれど、開発に有効なことってなんだろうな。

なんだかんだ言ってるけどLuaの便利さの恩恵に預かりたいのでずっと使い方を考えてるんだけど、使い古したノートPCの使いみちみたいに「何かありそうで何も思いつかない」のを歯がゆく思っている。

構造体が使えない

Luaの最大の特徴がテーブルらしいんだけど、C言語でいうところの構造体のテーブルを作って配列にして管理っていうのができない?弾幕とかどうやって管理したらいいのか。配列のインデックスが1始まりなのも、移植するのに不便である。for文のループをスキップするためのcontinueがないとか、もうここまで自分とLuaと相性が悪いとどうしてこんな言語仕様にしたのか全く理解に苦しむので、これを読んでみてる。

[[Programming in Lua プログラミング言語Lua公式解説書]>https://www.amazon.co.jp/dp/4048677977]

Luaを作った博士の本なら博士の気持ちが何かわかるかもしれない。

自分でADVゲームのスクリプトエンジンをかくことを考えればLuaでかけるのは便利?

C言語のソースに直接シナリオデータをいれこむといった最悪の事態を考えれば、Luaを使ってLuaスクリプトでできる範囲のことでシナリオを構築するのも悪く無い。しかし、ADV専用のスクリプトエンジンはLuaよりもずっと汎用性が低い分、可読性にすぐれた書式になっていてADVに向いている気もする。スクリプトに入れ込む変数の管理や制御でバグらせてしまうのもスクリプトで複雑なことができすぎるからに他ならない。そういう意味でLuaはオーバースペックだと思う。

メニューを作るのはどうか?

これは悪くなさそうである。プログラムで座標をエディット&コンティニューを駆使してパーツを並べるよりは、Luaで誰かがちまちまパーツを並べながらウインドウの位置調整や文言の中身をいじるほうが効率がいい。実際仕事で使われているのを初めてみたのもメニューだった。これはメニューのプログラムを実行ファイルに含めると実行ファイルサイズが膨れ上がってメモリを圧迫するから、メニュー一般のプログラムを「データとして」外にだしとくことが目的だったが。

ただメニューっていうのは、押されたボタンや表示する内容、また遷移先がプログラムと密接に絡んでいるので、メニュー全般をLuaにすることはオススメできない。あくまでも表示位置の調整のみ、プログラムでいうとウインドウの表示位置のテーブルをCSVじゃなくてLuaで書いてもらう、といったイメージに近い。そうなるとそれはそれで、プランナーに投げられる仕事が少ないので、設計するだけプログラマが面倒な仕事を抱えることになる。
あとLuaのスクリプト中に直接テキストを入れ込むと海外版とかを作るときに、マルチランゲージ対応が難しい。やっぱり現実味がないか・・・?

結局デバッガ

結局できることがたくさんある分、立派なデバッガが必要だ。複雑なことをして効率化するのは悪いことじゃないが、それで出てきたわかりにくいバグも責任持って取ってもらえないと複雑になるのは歓迎できないし、バグらせにくくするかわりにシンプルな設計にするならLuaじゃなくて専用ADVエンジンとかCSVの方がいい。

プログラミングすると必ずバグる、それをどうやって抑えこむか、っていうところにフォーカスしてしまうと、やっぱり使いドコロに悩むなあ。

とりあえず、いつでも使えるようにしとく

で、Luaスクリプトをプログラムで読めるようにしておくわけだけど、特に難しいことはない。ダウンロードしてきたソースをVCに突っ込めば、マルチプラットフォームで簡単にコンパイルできる。1つ注意が必要なのはLuaはそれ自信がコマンドラインで動くアプリなのでライブラリではない。
なので、main関数が存在する。だからLua.cのmain()関数の名前を別のmain2とか適当に変えてあげないと、自分のmain関数とバッティングするのでそこだけ注意。

main()
↓
main2()

includeファイルは?

そして自前のC言語プログラムから使うために「lua.hpp」をincludeすること

#include "lua.hpp"

個別にincludeする場合はこっち。上と同じ。

extern "C" {
 #include "lua.h"
 #include "lualib.h"
 #include "lauxlib.h"
}

Luaの関数を呼び出す

lua_getglobal( LuaState , "Luaの関数名");
lua_pcall(LuaState, 引数の数, 戻り値の数, 0);

最初になんらかのLuaの関数を呼び出さないと何も始まらないのでコレは必須。

Luaから関数を呼び出される

Luaの関数"gxDraw"とCの関数「gxDraw」を関連付ける

lua_register( LuaState , "gxDraw" , &gxDraw );

Cのプログラム的には指定した関連付けられた関数がコールバックされるイメージ。
ただしコールバックされる関数のカタチは決まっているので、引数でやり取りするときにはLua側で配列などにデータをのっけて、C側ではスタックから、データを引っ張ってくる。Luaの面倒なところではある。ただスタックに慣れてくるとコレしかやり取りできない分、それなりに安心感はある。

呼び出されるCの関数のカタチはコレ限定

int Sample( lua_State *L );

あれ?そういえばどうやってリターン値をLuaに返すのか?

Luaから呼び出されたCの関数の戻り値

int c_random( lua_State *L )
{
 Uint32 r = rand();            //ランダム値を取得して
 lua_pushinteger(L, r);        //スタックに積む
 return 1;                     //戻り値の数を返す
}

最後のretuenは「戻り値」ではなく「戻り値の数」を返す。あくまで戻り値はスタックを通じてやりとりすることになる。Lua側では普通に戻り値として受け取ればいい

function test()
 ret = c_random();
end

Luaファイル同士でのinclude

include"gxLib.h"に相当するLuaのコードは「拡張子を書かない」ことに注意

require "gxLib"

関数形式でも使える

require("gxLib")

CとLuaで配列をやり取りする

仮想のVRAM(256x256)の画面を想定した配列を作ってアクセスする

main.cpp

main()
{
 LuaState = luaL_newstate();
 luaL_dofile(LuaState, "sample.lua");
 luaL_openlibs( LuaState );
 lua_getglobal(LuaState, "gameInit");
 lua_pcall(LuaState, 0, 0, 0);
}

sample.lua

function gameInit()
for y=0,159,1 do
 for x=0,159,1 do
	VideoRAM[ y*256 + x + 1 ] = 0xff00ff00;
 end
end
end

これでもいい

VideoRAM = {
 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, ~ 0,0,0,0,0,0,0,0,0,0, --256個書く
    .    
    . 
  256段書く
}:

読み取り(main.cpp)

read()
{
lua_getglobal( LuaState, "VideoRAM" );
for (Sint32 y = 0; y < 256; y++)
{
 for (Sint32 x = 0; x < 256; x++)
 {
  n = x + 1 +y * 160;
  lua_rawgeti(LuaState, -1 , n );
  argb = lua_tonumber(LuaState, -1);
  lua_pop(LuaState , 1 );
 }
}

久しぶりにやったけどはっきり言って色々面倒くさい。ここまでたどり着くのにだいぶ試行錯誤した。ポイントは

  • 256x256の空間を想定しても、空の配列は作れない
  • 連想配列なので定義したデータが3つしかなければ3つしか返ってこない
  • lua_rawgetiで配列は取得できるが、定義した順に取得してしまう
  • lua_rawgetiした後は、スタックが1つ増えてるので参照後にpopしないとテーブルのスタック番号を-1で参照できなくなる
    などなどあって、結局「空」にならないように順番だてて配列の中身を初期化してやる必要がある。なので、最初に全部for文で初期化するか、ベタなテーブルをつくっておいてやることにした。

bit演算

luaではビット演算ができない。それが通説。しかし、5.2でbit32っていうライブラリが追加されてANDやORが取れるようになった。C言語でいうところのAND演算をbit32で書く

a = b&0x01; → a = 32bit.band(b,0x01)

で、さっそくやってみたけど、「STRING sample.lua:15: attempt to index a nil value (global 'bit32')」がでてきて、どうにもうまく使えない。Luaのバージョン5.2で実装されて、今使ってるのが5.3、同じ症状で悩んでいるヒトは5.1だから使えない、という情報があった。5.2で実装されてるはずなのになぁ、と思いつつ、ソースを見ると「LUA_COMPAT_5_2」という定義で、bit32ライブラリがコンパイル対象に「入らない」ようになってた。

え!?5.2互換にする定義があるってことは、さっそく5.3で無くなったってこと???
と思って5.3のリファレンスを眺めてみたら、ビット演算子がサポートされてる・・・。
ただしif( )の中の真偽判定がCと異なるため以下は意図した動きをしない。

if( 0x02&0x01 ) Cだとfalse  → if( 0x02&0x01) Luaだとtrue

この場合明示的に「if((0x02&0x01) ~= 0x00 )」というふうに判定してあげなくてはならない。なぜかというとif(0) がtrueになるからである。全く考え方がわからなかったので「Programming in Lua」を参照すると「falseかnil以外はtrue」となる仕様らしい。ゼロは数値(0)も文字("0")と同じくtrueという扱いになる、という考え方。zero == falseと決め込んでいたC頭ではなかなか直感でココにきづけなかった。

コメント

//ではなくて -- と書く。
/*  ~ */は --[[ ~ --]] と書く。

一時的にコメントを外したい場合には-を一つ足して以下のように使うと便利。

---[[
 コメントが外れている
--]]

この仕様からC言語的な「--」のデクリメントや「++」インクリメントはサポートされないと思われ。「+=」はサポートされると便利なんだけどな。

条件の&&と||

色んな使い方があるっぽいけれどもC言語からの変換であれば単純に「&&」を「and」に置き換えて問題ない。「||」は「or」ね。

if( a && b ) c = 0;   → if( a and b ) then c = 0 end

Lua中の関数をオーバーロードしたらどうなる?

あとで定義された関数で上書きされる。連想配列的に以降もオーバーロードは実装されないかもしれない

三項演算子はあるのか?

ない。しかし http://qiita.com/MOKYN/items/feec4678ee57a0e2c7d9ココが分かりやすかった

A and B : A が false か nil ならAを返し、そうでなければBを返す。
A or B  : A が false か nil 以外ならAを返し、そうでなければBを返す。

「Programming Lua」に書いてあった仕様をうまく使うと三項演算子っぽく使えるらしい。正直仕様だけ読んだ時にはまったくもって「?」だったが使う人が使うと便利に使えるんですね・

function GetAbs( n )

	-- //-----------------------------
	-- //絶対値を求める
	-- //-----------------------------

--	if ( n < 0 ) then
--		return n*-1;
--	else
--		return n;
--	end

	-- 三項演算子っぽく書こうと思うとこうなるらしい
	n = ( n < 0 ) and n*-1 or n*1
	return n;
end

比較演算子 != は「~=」

!= を使いたいときは「~=」をつかうといいニョロ

構造体

テーブルを複製して加工するとき以下のように書くと参照元のテーブルの中身も書き換えてしまう

tbl = {
 a,b,c = 0,0,0;
}
test = tbl;
test.a = 1;
結果
tbl.a  => 1
test.a => 1

これを、以下のように記述するとテーブルの中身をコピーして使うことができる。

tbl = {
 a,b,c = 0,0,0;
}
test = {tbl};
test.a = 1;
結果
tbl.a  => 0
test.a => 1

構造体の中の配列

構造体に配列を作ってそれを参照するとnil参照になる

tbl ={
 a,b;
 c ={0,0,0}
}
test1 = {tbl};
test2 = {tbl};
printf( test1.c[1] ); <-- ここでエラーが出る

こっちだと参照できるが、元のテーブルのデータを参照している

test1 = tbl;
test2 = tbl;
test2.c[1] = 1;
printf( test1.c[1] ); <--これだとエラーにはならず参照できているが結果は1
printf( test2.c[1] );

結果
1
1

しかたがないので、こうやって初期化して領域を確保する

test = {tbl};
test.c={0,0,0};
printf( test.c[1] );

う~ん、構造体の型をコピーしているというよりは、testのc配列を新たに定義している感じがするなあ。イメージ的には元の構造体の配列がポインタになっているイメージだろうか、テーブルの中の配列はコピーされない

構造体そのものの配列

test = {}
 for i=1,10 do
 tbl[i] ={test};
 test.c={0,0,0}; <---ここで初期化してやる
end
printf( test[1].c[1] );

弾幕を作りたい時用に構造体の配列を作りたいときはこうする。弾幕を作ろうと思うとどうしても構造体の配列が必要になるのでコレは助かる。なぜこれでいけるかは、実はまだよくわかっていない。構造体の中の配列は適宜初期化して確保しておく必要があるらしい。

if (条件) { ~ } は、 if ( 条件 )then ~ end

else if は 「elseif」

 条件

( a == b) --> ( a == b )
( a != b) --> ( a ~= b )
( a && b) --> ( a and b )
( a || b) --> ( a or b )
( !a )    --> ( not a )

気をつけ無くてはならないのは、aがゼロだった時にif( a ) がfalseになりそうにみえること。if( 0 ) はLuaではtrueである。falseになるのはif( false )か、if( nil )の時だけと「Programming Lua」に書いてあった。

++ --は使えない

a ++; b --; は使えないので a = a + 1; b = b - 1;に書き換える
 --はコメントなので、きっとこの先もサポートされない。さらにいうと、Lua5.3から「//」が整数除算(※)がサポートされたのでC言語的なコメントも期待してはいけない

※整数除算
10 / 3  ==> 3.333333
10 // 3 ==> 3

for文でcontinueしたい

そもそも、Luaにはcontinueがない。しかし、なんと5.2からgotoが実装されたらしい、助かった!以下のようにすればfor文をスルーできる。ジャンプ先は「::ジャンプ先名前::」
コロンx2で前後を挟む

for ii=0, 10 do
 if( ii< 5 ) then goto CONTINUE; end
   x = x + 1;
 ::CONTINUE::
end

ちなみにforは上記の例だと11回まわるので注意。Cで書くとこうなるのと同じらしい、オリジンが1(配列の添字が0スタートじゃなくて1スタート)なので、iiの初期値を1にしてあげればいつものC言語の間隔に近くなるはず。

for( int ii=1; ii<=10; ii+=2 )
 ↓
for i=1,10,2 do

switchがない

Luaにはswitch ~ case ~default:に該当するものがない。え!そうなんだ。しかたがないのでif ~ elseif ~ elseで代用しよう

a = b = c = 0; とは書けない

こういう書き方はできない。しかしこうかける。

a,b,c = 0,0,0;

複数の戻り値

複数の戻り値をもどせる便利な機能はCのポインタ渡しで値を書き換えてほしい時に有効

gxBool GetStylus( int *px , int *py )
{
 *x = 1;
 *y = 2;
 retun gxTrue;
}
ret = GetStylus( &x , &y );

こう書き換えられる

function GetStylus( x , y )
  return gxTrue , 1,2;
end
ret,px,py = GetStylus( px , py );

returnがないのに受け取った場合は「nil」を受け取ることになる

 謎のエラー

attempt to index 変数名, a nil value(変数がnilかもよ)

って言うエラーをおいかけて、さんざん試行錯誤したけど、結果的にいうと全く関係なかった。どうもエラーメッセージについては、概ね問題なくて、ほとんどあってるけど、場合によりバグることもある。
まあLuaに限らないので他のバグが引き起こした勘違いかもしれないので、他をあたってみる。今回の場合はコレを直すとこの問題が出なくなった

for i=1,i<36-1 do
end

Cのfor文を移植してきた時のバグでコレが正しい。

for i=1,36-1 do

コードがどう考えてもおかしくない時は、その他の場所も疑ってみよう。

false と ゼロ

なんども書いてるけどまたハマった。

function test()
 return 0;
end
if( test() ) then
 printf("こっちにこないで")
else
 printf("こっちにきてほしい")
end

結果

こっちにこないで

答えがわかっていれば当たり前だけどこうなる。0はtrueなので、falseに入らない。だからtest()の戻り値をfalseにするか明示的にこう書かなくてはいけない

if( test() == true ) then

whileの書き方ってどうだっけ?

while (条件) do

  ~

end

可変引数の受け取り

いろんな方法が書かれていたけどなかなかうまくいかない。結局うまくいったのは「Programming Lua」に書いてあった方法

function movableVal( ... )
 dat = ...   <--- これは dat[0] がnilになる
 dat ={...}  <--- これも dat[0] がnilになる
end
function movableVal( ... )
 dat ={}
 dat[0],dat[1],dat[2] = ...   <--- こうするとdat[0]~[2]に値を取得できる
end

最後まで取れなかったnil参照

すべてdoubleの値で扱っているため、テーブルの配列に対して実数でアクセスしていた。つまりこうなっていた。添字が変数だけに発見が遅れた。

id = 10/3;
spr[id] = 0;
 ↓
spr[ 0.3333333...] = 0;

C言語だとintでidを扱っていた場合、自動でint型にキャストされるので大した問題はなかったが、Luaだと全て実数なので、小数点以下を含む値のままになってしまう。こういうアクセスの仕方をすると、配列的にnilを返すことになるらしい。

Luaの5.3から整数に丸める割り算ができるようになったので、こういう時はこうかける。

id = 10//3;

ちょっと気持ち悪いがintに丸めたい時にとても便利であるが、移植性が下がるので推奨しない。

で、結局Luaはどうなのか

機種依存しないCのプログラムを移植してきた結果、苦労もしたが非常に面白かった。この面白いという表現が適切かどうかはわからないが、少なくとも意味不明なバグに悩まされ、とんでもない解決方法によりバグを見えないように封じていくような、なんか理解に苦しむバグ修正は少なくとも面白く感じない。そういった意味で、Luaのバグは100%こちらの理解不足で起こるバグだし、それに悩まされる分には、必ず正しい回答が用意されているパズルみたいでバグ取りも面白かった。9割信頼のできるエラーメッセージも良いヒントになる。いい言語だと思うし、うまくいかせればそれなりに実績を挙げられるポテンシャルもあると思う。なんとかこれを何かに活かしたいけど、やっぱり「コレだ!」っていう使い方が思いつかない。デバッグの仕方や、デバッグ用のライブラリも用意されているので、もう少しこの辺りを煮詰めていきたいが、5.2から5.3で大事な仕様が結構大きく変わることに少し驚いた。以降の5.4で今のスクリプトが動かない原因を探す作業に恐怖しちゃう。

インタープリターの怖さ

最初からわかってることではあるけど、インタープリターの場合はそのプログラムがそこを通らないかぎりエラーを検出できないことが不安になる。単なる記述ミスや関数の定義漏れなんかは速いうちに発見できる感じだったけど、そのコードに到達しないかぎり発覚しない関数名の記述ミスもあった。そうなるとデバッグでスミからスミまでコードを実行しなくてはならないのはなかなか厳しい。しかも発見されるバグは単なる関数名のスペルミスであってもクリティカルなバグになりやすい。Lua内部のVM自体が止まってしまうからである。

バイトコードはどうなのか?

実行前にコンパイルしてエラーを確認することで単純な記述ミスは発見できるかもしれないと思い、LuaCでコンパイルしてみたけど、めちゃくちゃサイレント。なんにも言わない。試しにわざとエラーを作ってみる。あ、ちゃんと文法ミスなんかは検出してくれてる。しかし、ない関数を呼んだり、その関数からリターン値を得たりしても特にエラーにはならなかった。リンカー相当のエラー検出はできないのかもしれない。

改造テーマ

・状態保存
小さなVMとして使いたいので状態保存とかしたい、と考えたけど、reallocで確保されつつづけるメモリとGCに対応したプログラムが複雑で簡単にはできなさそう。。

・メモリからのロード
メモリからのロードしないと場合luaのスクリプトをそのままEXEと同じ階層に配置しなくてはならないので、アプリ的に危険極まりない。とりあえずメモリからのロードはできるけれども、そのLuaが参照するファイル(includeしてるLua)なんかはどうしても勝手にファイルからロードしてくることになるので、あまり意味がない。とはいえ参照しているLuaを階層構造を含めてオンメモリにする仕組みは、それなりに複雑である。