Lua and Swift in iOS

image credits

I looked at Lua which it’s pretty easy to integrate and is highly optimized, but I really hate the syntax. There is just too much that is goofy about Lua. I like my { braces }. – Ron Gilbert: Engine

This year I found the time to play Thimbleweed Park. A point-and-click adventure game by Ron Gilbert and Gary Winnick, who got famous for their 1987 game Maniac Mansion. Ron Gilbert was also involved in The Secret of Monkey Island and the SCUMM game engine.

In the blog post Engine he writes “I’m a game engine snob” and that he uses Squirrel instead of Lua. But due to the Advent of Code 2019 (AoC) I wanted to have another look into Lua and I had already embedded Lua before. Like Ron said: “it’s pretty easy to integrate”.

For these reasons I decided to integrate Lua into an iOS app, because if I ever develop a game 🤣, I already have a game engine. There is no need to discuss the syntax, because this is a matter of taste. But the question I couldn’t really answer is: Why should I bring an additional language into a project? However, in a game project with more than one or two people, this can make sense, but I’m not there yet.

Lua is available as C code and can be compiled and integrated easily within Swift. Lua is under the MIT license and can be used everywhere. On the internet you can find examples how to integrate C into Swift under swift objective-c bridging. And I’m not the first who had the idea to integrate Lua into Swift.

In the next blog posts I will explain how to call Swift from Lua and then call Lua code again. The whole thing will be very recursive. But first we will write a Hello World application as a base and then we will build on that. You can find the source code on Github.

Hello World

For the Hello World example following steps are required:

  • create a Swift project
  • create the bridging files
  • download and integrate the Lua source code
  • create a Lua Hello World file
  • read and execute the Lua file

Create a Swift project

I don’t think it should be a big deal. Open Xcode, create a new project and choose Swift as your programming language. For the User Interface, select Storyboard.

Create the bridging files

After you setup a new Swift project, you create the wrapper and bridging files.

LuaWrapper.m

First you create the LuaWrapper.m file with following steps:

  • File / New / File …
  • Objective-C
  • Dialog
    • File: LuaWrapper.m
    • File Type: Empty File
  • Would you like to configure an Objective-C bridging header?
    • Create Bridging Header

This will create the LuaWrapper.m file and the project-name-Bridging-Header.h file. Next you create the LuaWrapper.h file.

LuaWrapper.h

  • File / New / File …
  • Header File
  • Dialog
    • LuaWrapper.h
    • select Targets checkbox
    • Create

Include header file to bridging file

Add the following code which includes LuaWrapper.h into following files:

  • LuaWrapper.m
  • project-name-Bridging-Header.h
#include "LuaWrapper.h"

Download Lua source

You find the source on the Lua Download page (version 5.3.5 works for me).

  • Download the source code and unpack the file.
  • Drag and drop the src folder in the project.
  • Create external build system project
    • unselect Create external build system project
    • Next
  • Choose options for adding these files:
    • select Copy items if needed
    • select Add to targets
    • Finish

Adjust targets

The following files need to be deselected from the targets. Therefore select these files in the src folder. The files can be sorted by right clicking on the src folder and selecting Sort by Name.

  • loslib.c
  • lua.c
  • luac.c

In the file inspector on the right side of Xcode, uncheck Target Membership.

add luaopen_os to Objective-C file

The loslib.c file contains the luaopen_os function which was deselected. Therefore you need to add this adjusted function into the LuaWrapper.m file at the end.

LUAMOD_API int luaopen_os (lua_State *L) {
  return 1;
}

The loslib.c file contains some system calls which aren’t available in iOS but also defines the luaopen_os function which is used by other functions.

Lua include files

Add after the #define LuaWrapper_h the following .h files includes:

#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

Create Lua file

Create the script.lua file:

  • File / New / File
  • Empty file (in Other group)
  • script.lua
  • Create

… and add the following Lua code into the script.lua file:

print("hello world")

Load Lua script and execute the code

Next, implement some Objective-C wrapper functions that Swift uses. The LuaWrapper.h file defines functions, the LuaWrapper.m file implements them, and ViewController.swift uses them by loading the Lua script file and executing the code.

LuaWrapper.h file

Now you need to add some Objective-C code in the LuaWrapper.h file (after the lua includes and before the #endif):

#import <Foundation/Foundation.h>

@interface Lua : NSObject {
    lua_State * luaState;
}

- (void) setup;
- (void) script: (const char *) script;
- (void) destruct;

@end

LuaWrapper.m file

The LuaWrapper.h defines some methods and the LuaWrapper.m file implements them. Add the following code into the LuaWrapper.m file:

@implementation Lua

- (void) setup {
    luaState = luaL_newstate();
    luaL_openlibs(luaState);
}

- (void) script: (const char *) script {
    luaL_loadstring(luaState, script);
    lua_pcall(luaState, 0, 0, 0);
}

- (void) destruct {
    lua_close(luaState);
}

@end

The setup function creates a new Lua state. The script function loads a string into this state and just runs the loaded code. In part two of this series you will see that the lua_pcall method calls Lua functions.

The destruct function closes the Lua state and frees internally some stuff.

ViewController.swift

To load the script file and execute the Lua code you need to add the following code into the ViewController.swift in the viewDidLoad() function after super.viewDidLoad():

let filename = Bundle.main.path(forResource: "script",
                                ofType: "lua")!
do {
    let lua = Lua()
    lua.setup()

    let luaScript = try String(contentsOfFile: filename)
    let ptrScript = strdup(luaScript)
    lua.script(ptrScript)
    free(ptrScript)

    lua.destruct()
} catch let error {
    print("can not read file", filename, error)
}

First a Lua instance is created and the setup method creates a new state. Then the script.lua file is loaded as String and strdup converts it to a const char *. After the execution the we need to free it and destruct the Lua state.

Memory leaks

The code is only executed once and then nothing happens. You may think why we should care about some memory leaks. But it is easier to start small and eliminate some memory leaks in the beginning.

You will see relatively fast when you have a memory leak in the debug session view and Profile. Add this code in an endless loop (while (true)) after Bundle.main.path and run it.

If you add the Bundle.main.path call in the while loop you will get memory leaks.

run

But before you worry about memory leaks, your code should be running:

  • Select a Simulator
  • Product / Run

Now you should see in the Output a hello world.

Next

At the next blog post I will integrate a factorial calculation.