I have been experimenting with integrating Lua into Objective-C. I have tried to do the bare minimum to get Lua to compute values and return them. I did not want to use any external libraries so I have avoided those. All that is required to make this work is to add the Lua files to the project directory. Then all the scripts go in one folder, and when the Lua stuff is initialized, that folder is set up as the root path so it can find the scripts.
I started with some example code that takes input from a UITextField and outputs the result to a UITextField. You can type things into the TextField, and when you hit the evaluate button, it tries to compute the Lua and return a value. The way it is currently configured, it only accepts and returns strings.
LTViewController.h
#import <UIKit/UIKit.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
@interface LTViewController : UIViewController {
UITextView *output;
UITextView *input;
lua_State *luaStack;
}
@property (nonatomic, retain) IBOutlet UITextView *output;
@property (nonatomic, retain) IBOutlet UITextView *input;
-(IBAction) evaluate;
-(IBAction) clearInput;
-(IBAction) clearOutput;
-(IBAction) testFunction;
@end
LTViewController.m
#import "LTViewController.h"
#import "LTMyScene.h"
@implementation LTViewController
@synthesize output;
@synthesize input;
#pragma mark - Initialization
- (void)viewDidLoad {
[super viewDidLoad];
input.font = [UIFont systemFontOfSize:12];
output.font = [UIFont systemFontOfSize:12];
//initialize lua
luaStack = luaL_newstate();
luaL_openlibs(luaStack);
//this can be set to any file in the directory where the scripts are going to be loaded from
NSString *luaPath = [[NSBundle mainBundle] pathForResource:@"testscript01" ofType:@"lua"];
[self configureLuaState:luaStack forPath:[luaPath stringByDeletingLastPathComponent]];
[self loadScripts];
}
-(void) configureLuaState:(lua_State*)luaState forPath:(NSString*)path {
lua_getglobal(luaState, "package");
lua_getfield(luaState, -1, "path"); // get field "path" from table at top of stack (-1)
NSString * cur_path = [NSString stringWithUTF8String:lua_tostring(luaStack, -1)]; // grab path string from top of stack
cur_path = [cur_path stringByAppendingString:@";"];
cur_path = [cur_path stringByAppendingString:path];
cur_path = [cur_path stringByAppendingString:@"/?.lua"]; //this last part is required for it to work
lua_pop(luaState, 1); // get rid of the string on the stack
lua_pushstring( luaState, [cur_path UTF8String]); // push the new one
lua_setfield( luaState, -2, "path" ); // set the field "path" in table at -2 with value at top of stack
lua_pop( luaState, 1 ); // get rid of package table from top of stack
}
-(void) loadScripts {
[self loadScript:@"testscript01"];
[self loadScript:@"testscript02"];
[self loadScript:@"testscript03"];
}
-(void) loadScript:(NSString *)scriptName {
NSString *scriptCommand = [NSString stringWithFormat:@"require(\"%@\")", scriptName];
[self loadTextIntoLua:scriptCommand];
}
#pragma mark - Interaction with UI
-(void)clearInput {
input.text = @"";
}
-(void)clearOutput {
output.text = @"";
}
-(void)displayError {
output.text = [output.text stringByAppendingFormat:@"%@\n",
[NSString stringWithCString:lua_tostring(luaStack, -1)
encoding:NSASCIIStringEncoding]];
lua_pop(luaStack, 1);
}
-(void) testFunction {
[self loadTextIntoLua:@"return repair(10)"]; //repair is the name of a lua function from a loaded script
[self evaluateLuaStack];
}
#pragma mark - Send Data To Lua
-(void) loadTextIntoLua:(NSString *)text {
lua_settop(luaStack, 0);
luaL_loadstring(luaStack, [text cStringUsingEncoding:NSASCIIStringEncoding]);
lua_pcall(luaStack, 0, LUA_MULTRET, 0);
}
-(void) evaluateLuaStack {
int nresults = lua_gettop(luaStack);
if (nresults == 0) {
output.text = [output.text stringByAppendingFormat:@"<no results>\n"];
} else {
NSString *outputNS = [NSString string];
for (int i = nresults; i > 0; i--) {
outputNS = [outputNS stringByAppendingFormat:@"%s ", lua_tostring(luaStack, -1 * i)];
}
lua_pop(luaStack, nresults);
output.text = [output.text stringByAppendingFormat:@"%@\n", outputNS];
}
}
//this is the full runtime interpreter
-(void)evaluate {
[input resignFirstResponder];
lua_settop(luaStack, 0);
int error = luaL_loadstring(luaStack, [input.text
cStringUsingEncoding:NSASCIIStringEncoding]); //first part needed to load string
//return early if anything went wrong with the first part of loading
if (error != 0) {
[self displayError];
return;
}
error = lua_pcall(luaStack, 0, LUA_MULTRET, 0); //second part needed to load string
//return early if anything went wrong with the second part of loading
if (error != 0) {
[self displayError];
return;
}
//get result strings from the stack and output them to the UI
int numberOfResults = lua_gettop(luaStack);
if (numberOfResults == 0) {
output.text = [output.text stringByAppendingFormat:@"<no results>\n"];
} else {
NSString *outputNS = [NSString string];
for (int i = numberOfResults; i > 0; i--) {
outputNS = [outputNS stringByAppendingFormat:@"%s ", lua_tostring(luaStack, -1 * i)];
}
lua_pop(luaStack, numberOfResults); //remove all results from the stack
output.text = [output.text stringByAppendingFormat:@"%@\n", outputNS];
}
}
#pragma mark - Boilerplate Setup Code
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return UIInterfaceOrientationMaskAllButUpsideDown;
} else {
return UIInterfaceOrientationMaskAll;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)dealloc {
lua_close(luaStack);
}
@end
Here are two example scripts:
testscript01.lua
repair = function(amount)
return amount * 10
end
testscript02.lua
mymodule = {}
function mymodule.foo()
return "Hello World!"
end
return my module
As long as the scripts are loaded at the start, they can be used in two ways. Values can be typed into the UITextView, and when the user hits evaluate, it will return the result. So if the user types return repair(10)
and hits the evaluate button, lua will look for the return method, input the value of 10, and output the value of 100. If they punch in return mymodule.foo()
it will output Hello World!
The second way to access lua is to use the format shown in the testFunction method. You pass in as a string the same thing that you would otherwise type into the live interpreter. This will still load a value into the UI, but could easily be altered to be called from anywhere, accept whatever required values, and return the result of the lua string converted to the proper format. It is just a proof of concept at this time.
-(void) testFunction {
[self loadTextIntoLua:@"return repair(10)"]; //repair is the name of a lua function from a loaded script
[self evaluateLuaStack];
}
The logical next step is to create a wrapper that "converts" Objective-C objects into Lua tables, but that is not really necessary at this stage. I want to make sure I am doing everything properly and avoiding any pitfalls before I continue.