9
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

I am not even remotely familiar with or how it works with Objective-C, so I can't really comment on any of these aspects of your code.

What I can do of course, is comment on the Objective-C bits, starting with your header file.

Why is there anything in your header file at all?

#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

The only parts I can see that need to be in your header file are these:

#import <UIKit/UIKit.h>

@interface LTViewController : UIViewController

@end

Everything in here is either an interface builder hook (which should be in the class extension in the .m), or an instance variable. The only thing here that's not an interface builder hook is the lua_State instance variable. Again, I'm not familiar with or how it works with Objective-C. This seems like it's probably fine as a private instance variable so we can put it in the .m, and if it needs to be public, let's use a @property so outsiders go through the accessors.

Meanwhile, these #include statements, for one, should go to the .m. Otherwise, any file that imports this file is also including these three files, and they probably don't necessarily want to do this. Also, we can just #import them instead, I believe, and that's almost certainly what we should do. I'm not aware of any instance in which you must #include rather than #import, and #import is definitely better than #include.


@synthesize output; @synthesize input;

Xcode has been auto-synthesizing for a few versions now, and we're used to the autosynthesis to the underscored variable name. These two lines can just be eliminated.


input.font = [UIFont systemFontOfSize:12];
output.font = [UIFont systemFontOfSize:12];

Given that these variables are references to IB, why not just set the font size in IB?

Barring that, we don't have to instantiate two UIFont objects--we can do one.

UIFont *systemFont12 = [UIFont systemFontOfSize:12.0f];
input.font = systemFont12;
output.font = systemFont12;

cur_path = [cur_path stringByAppendingString:@";"];
cur_path = [cur_path stringByAppendingString:path];
cur_path = [cur_path stringByAppendingString:@"/?.lua"];

This can be replaced with this single line:

cur_path = [cur_path stringByAppendingFormat:@";%@/?.lua", path];

NSString *outputNS = [NSString string];
for (int i = nresults; i > 0; i--) {
    outputNS = [outputNS stringByAppendingFormat:@"%s ", lua_tostring(luaStack, -1 * i)];
}

This should be replaced with NSMutableString:

NSMutableString *outputString = [NSMutableString string];
for (int i = nresults; i > 0; --i) {
    [outputString appendFormat:@"%s ", lua_tostring(luaStack, -1 * i)];
}

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;
}

Okay, first of all, this:

[input.text cStringUsingEncoding:NSASCIIStringEncoding]

is way too big to embed in the function call. Let's save it to a local variable.

char *inputText = [input.text cStringUsingEncoding:NSASCIIStringEncoding];

Now, let's do our early returns something more like this:

if (luaL_loadstring(luaStack,inputText) != 0) {
    [self displayError];
    return;
}

if (lua_pcall(luaStack, 0, LUA_MULTRET, 0) != 0) {
    [self displayError];
    return;
}

In fact, we could even drop the != 0 as any non-zero will evaluate to YES here and enter the if block.


- (BOOL)shouldAutorotate {
    return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskAll;
    }
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

These three can be completely eliminated. The first two can be set up in your project settings easily and it's unlikely that this controller has special rotation rules relative to the rest of the project. The last does nothing but call super, so literally nothing it wouldn't do already without being there.


- (void)dealloc {
    lua_close(luaStack);
}

In the days of ARC, it's rare that we need to do much of anything at all in dealloc any more. I'm going to assume that this code is appropriate, but I'm going to say that because of both the rarity of needing dealloc yet the importance of it being perfect when it is needed, this should be at the very top of the class implementation section. This should be the first method in the file.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ +1 for a thorough review. But this made me do a double-take: "an interface builder hooker"? Shouldn't that just be hook? :) \$\endgroup\$
    – Flambino
    Aug 12, 2014 at 0:16
  • \$\begingroup\$ No, they're hookers to the IB pimp \$\endgroup\$
    – nhgrif
    Aug 12, 2014 at 0:22
  • \$\begingroup\$ I thought IB would be the trick in this particular analogy; the code's doing the pimpin' \$\endgroup\$
    – Flambino
    Aug 12, 2014 at 1:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.