iPhoneアプリにluaを組み込む

SDKの規約が2010年6月7日の変更で多少緩和されたこともあり、
以前から審査に通った例のあるluaの組み込みも大丈夫じゃね?
と考えてまじめに組み込むのを試した。

<10/09/18修正>
nao_19さんから指摘いただきluaの古い関数を修正しました
あと初期化の位置がまずかったので修正


1.LuaソースコードiPhoneのHelloWorldに組み込みコンパイルする。

Lua 5.1のソースコード
http://www.lua.org/versions.html#5.1
からダウンロードする。

iPhone dev centerからsample codeに移動し
hello worldのソースコードをダウンロードする。

  • HelloWorldのビルドを確認

プロジェクトを開き

プロジェクト設定のすべての構成ベースSDK
iPhoneシミュレータx.xにして、

試しにビルド。

これに先ほどダウンロードしたLua5.1のソースコードを追加する。
luaのsrcフォルダ内のソースコードをプロジェクトに追加する。
例としてluaフォルダにコピーした。

このうちlua.cとluac.cファイルはコマンドラインツールのソースなので除外しておく。
このファイルをプロジェクトの"追加>既存のファイルを追加"でプロジェクトに追加する。

たったこれだけでluaの組み込みは完了です。

luaの環境の生成を試します。

  • HelloWorldAppDelegate.h

lua.hをインクルードし、
lua_Stateの変数を追加する。


#import

#import "lua.h"

@class MyViewController;

@interface HelloWorldAppDelegate : NSObject {

IBOutlet UIWindow *window;
MyViewController *myViewController;

lua_State *luaState;
}

@property (nonatomic, retain) UIWindow *window;
@property (nonatomic, retain) MyViewController *myViewController;

@end

  • HelloWorldAppDelegate.m

applicationDidFinishLaunchinglua_Stateを作成するようにし、
luaの標準ライブラリを読み込む。
deallocで解放する。


- (void)applicationDidFinishLaunching:(UIApplication *)application {

// Set up the view controller
MyViewController *aViewController = [[MyViewController alloc] initWithNibName:@"HelloWorld" bundle:[NSBundle mainBundle]];
self.myViewController = aViewController;
[aViewController release];

[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackOpaque];

// Add the view controller's view as a subview of the window
UIView *controllersView = [myViewController view];
[window addSubview:controllersView];
[window makeKeyAndVisible];

//luaを生成
luaState = luaL_newstate();
//luaの標準ライブラリを読み込む
luaL_openlibs(luaState);

}

- (void)dealloc {

//luaの後始末
lua_close(luaState);

[myViewController release];
[window release];
[super dealloc];
}


ここではluaに何も仕事させていないが、
コンパイルluaAPIの動作確認のためビルドし、一度動かしておくといいでしょう。


2.luaスクリプトで動作させる。


まず本来のHelloWorldプログラムの動作をおさらいすると、


UITextFieldの編集によりMyViewController
textFieldShouldReturnが呼ばれ
UITextFieldの文字列をMyViewControllerupdateString
UILabelに設定するというもの。



これを
・1.luaUILabelを変更する関数Funcs.updateText
を追加(MyViewControllerのupdateStringを呼ぶ関数)
・2.textFieldShouldReturn内で1の関数を呼ぶluaスクリプトを実行。
という形にする。



LuaFuncs.h
LuaFuncs.m
というファイルを作成し、
ここにluaに組み込むC言語の関数を宣言します。
組み込む関数はC関数ですが関数内でObjective-Cを使うため、
拡張子は.mにします。

  • LuaFunc.h

Objective-C/UIKitを使うためFouncation.hをインクルードします。


#import <Foundation/Foundation.h>

#import "lua.h"

int luaUpdateText(lua_State *luaState);

luaから使用する関数はlua_CFunction型である必要があるのでluaUpdateTextはそれに準拠しています。
typedef int (lua_CFunction*)(lua_State* L);


LuaFunc.mはいったん置いておき、
この関数をHelloWorldAppDelegate.m
関数テーブル(luaL_Reg型の配列)にします。

  • HelloWorldAppDelegate.m


#import "HelloWorldAppDelegate.h"
#import "MyViewController.h"

#import "LuaFuncs.h"

#import "lauxlib.h"

//関数テーブル
static const luaL_Reg LuaFuncTable[] = {
{"updateText", luaUpdateText},
{NULL, NULL}
};

@implementation HelloWorldAppDelegate

このテーブルはluaUpdateTextlua上でupdateTextという関数で呼び出すという意味になります。


そして下のソース。
第2引数のFuncsLuaFuncTableにある関数の名前空間のようなものになります。
updateTextを呼ぶ時はFuncs.updateTextという形で呼び出す事になります。


この関数テーブルをapplicationDidFinishLaunching:時に登録します。

  • HelloWorldAppDelegate.m


- (void)applicationDidFinishLaunching:(UIApplication *)application {

// Set up the view controller
MyViewController *aViewController = [[MyViewController alloc] initWithNibName:@"HelloWorld" bundle:[NSBundle mainBundle]];
self.myViewController = aViewController;
[aViewController release];

[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackOpaque];

// Add the view controller's view as a subview of the window
UIView *controllersView = [myViewController view];
[window addSubview:controllersView];
[window makeKeyAndVisible];

//luaを生成
luaState = luaL_newstate();
//luaの標準ライブラリを読み込む
luaL_openlibs(luaState);

//関数を登録
luaL_register(luaState, "Funcs", LuaFuncTable);


//myViewControllerのidをグローバルとして登録(本文下記)
lua_pushlightuserdata(luaState, myViewController);
lua_setglobal(luaState, "target");

}

そして、
luaUpdateText関数では
MyViewControllerupdateStringを呼ぶことになりますが
そのためにはMyViewControlleridをこの関数に受け渡す必要があります。

こういった変数はluaに変数として格納しておくと
呼び出し先から自由に参照できます。


プログラムからluaの変数にアクセスする時は常にlua_Stateが持つスタックを使用します。
また、スクリプトから呼ばれたC関数がスクリプトの引数を得る際にも同様にスタックを使用します。

Cからスタックに積む時はlua_push***をつかいます。
上のソースではlua_pushlightuserdata()でポインタをスタックに積んでいます。
その後lua_setglobal()によってスタックトップにある変数に名前を付けてlua環境のグローバル変数にしています。

こうして登録した変数は当然ながらスクリプトからも使用できます。

  • LuaFuncs.m

luaUpdateText()を定義します。


#import "LuaFuncs.h"

#import "MyViewController.h"


int luaUpdateText(lua_State *luaState)
{
//スタックに積む
lua_getglobal(luaState, "target");
//チェック
if(!lua_islightuserdata(luaState, -1)){
NSLog(@"getting target failed");
lua_pushstring(luaState, "getting target failed");
lua_error(luaState);
return 0;
}
//スタックから取得
MyViewController *viewController =
(id)lua_topointer(luaState, -1);

//updateStringを呼ぶ
[viewController updateString];

return 1;
}

さき程登録したグローバル変数をスタックに積み、
スタック末尾(-1)にアクセスしてポインタとして取り出しています。
そのポインタ(id)でupdateStringを呼んでいます。



あとはUITextField編集時にスクリプトを呼ぶだけです。

  • MyViewController.h


#import

#import "lua.h"


@interface MyViewController : UIViewController {

IBOutlet UITextField *textField;
IBOutlet UILabel *label;
NSString *string;

//luaStateの変数を追加
lua_State *luaState;

}

@property (nonatomic, retain) UITextField *textField;
@property (nonatomic, retain) UILabel *label;
@property (nonatomic, copy) NSString *string;

//プロパティにしておく
@property (nonatomic, assign) lua_State *luaState;

- (void)updateString;

@end

  • HelloWorldAppDelegate.m


- (void)applicationDidFinishLaunching:(UIApplication *)application {

// Set up the view controller
MyViewController *aViewController = [[MyViewController alloc] initWithNibName:@"HelloWorld" bundle:[NSBundle mainBundle]];
self.myViewController = aViewController;
[aViewController release];

[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackOpaque];

// Add the view controller's view as a subview of the window
UIView *controllersView = [myViewController view];
[window addSubview:controllersView];
[window makeKeyAndVisible];

//luaを生成
luaState = luaL_newstate();
//luaの標準ライブラリを読み込む
luaL_openlibs(luaState);

//関数を登録
luaL_register(luaState, "Funcs", LuaFuncTable);

//myViewControllerのidをグローバルとして登録
lua_pushlightuserdata(luaState, myViewController);
lua_setglobal(luaState, "target");

//MyViewControllerにlua_stateを渡す
myViewController.luaState = luaState;

}

  • MyViewController.m

//プロパティ
@synthesize luaState;

- (BOOL)textFieldShouldReturn:(UITextField *)theTextField {

if (theTextField == textField) {
[textField resignFirstResponder];
//[self updateString];
//スクリプト実行
luaL_dostring(luaState, "Funcs.updateText()");

}
return YES;
}

元の処理updateStringコメントアウトして
替わりにluaL_dostring()で文字列のスクリプト
Funcs.updateText())を実行しています。
このスクリプトからさっきのC関数luaUpdateText()が呼ばれます。

直接文字列で渡すのではなくスクリプトファイルを読み込んで使用する場合は
luaL_dofile()です。



動作を確認して、以前と変わらぬ動作をすれば成功です。
見た目はかわりませんが、内部ではしっかりとluaスクリプトが動いています。