Objective-C Interoperability

In the previous tutorial, we created a simple plugin, part of which is written in C. This time, we will write the native part in Objective-C. Also, we will use Xcode instead of the command-line tools.

If you do not have Xcode installed, you can download it in the App Store.

Setting up the project

Creating the project

Create a new macOS library project with the following settings:

  • Product Name: objcinterop
  • Framework: Cocoa
  • Type: Dynamic
Setting up the project, step 1

Setting build settings

Now, we need to tune some of its build settings. Go to the library target and open the “Build Settings” tab. Set these properties:

  • Deployment
    • Skip Install: No
    • macOS Deployment Target: macOS 10.12
  • Linking
    • Dynamic Library Install Name: $(EXECUTABLE_PATH)
    • Runpath Search Paths:
      • $(inherited)
      • @executable_path/../Frameworks
      • @loader_path/../Frameworks
  • Packaging
    • Executable Extension: so
  • Search Paths
    • Header Search Paths:
      • /Applications/Marta.app/Contents/Frameworks/LuaKit.framework/Versions/Current/Resources/include (non-recursive)
    • Library Search Paths:
      • /Applications/Marta.app/Contents/Frameworks/LuaKit.framework/Versions/Current/Frameworks (non-recursive)
Setting up the project, step 2

Adding Lua dependency

The final step is to add the Lua library to the project dependencies.

  1. Go to the “General” tab.
  2. Click on the “+” button in the “Frameworks and Libraries” list.
  3. Choose “Add Other…” → “Add Files…”.
  4. Press ⌘⇧G and navigate to /Applications/Marta.app/Contents/Frameworks/LuaKit.framework/Versions/Current/Frameworks.
  5. Choose liblua.dylib, press “Open”.
  6. (Important) Change the “Embed” value to “Do not embed”.
Setting up the project, step 3

🐧 If you need to link against some other version of Marta, change paths to the Marta application bundle accordingly.

Great! We are now ready to code.

Objective-C part

XCode automatically created a header (objcinterop.h) and a source file (objcinterop.m). We will use them.

Let’s start with the header. In order to make Lua understand our library, we need to declare an entry point.

Remove all existing code in the header file and declare luaopen_libobjcinterop() instead:

#include "lauxlib.h"

int luaopen_libobjcinterop(lua_State *L);

Then we need to provide the implementation for the function we declared. Similar to the C Interoperability tutorial, we will expose to Lua a single function called helloWorld().

Replace code in objcinterop.m with the following listing:

#import "objcinterop.h"

static int helloWorld(lua_State *L) {
    return 0;
}

static const struct luaL_Reg mylib [] = {
    {"helloWorld", helloWorld},
    {NULL, NULL}
};

int luaopen_libobjcinterop(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;
}

Now you should be able to build the project. Yet, our function does nothing – it simply returns zero, telling Lua there are no return values. Let’s make use of Objective-C by calling some Cocoa APIs:

#import <Cocoa/Cocoa.h>

static int helloWorld(lua_State *L) {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setMessageText: @"Hello from Objective-C asasas!"];
    [alert runModal];
    return 0;
}

Here is the complete contents of objcinterop.m:

#import "objcinterop.h"
#import <Cocoa/Cocoa.h>

static int helloWorld(lua_State *L) {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setMessageText: @"Hello from Objective-C!"];
    [alert runModal];
    return 0;
}

static const struct luaL_Reg mylib [] = {
    {"helloWorld", helloWorld},
    {NULL, NULL}
};

int luaopen_libobjcinterop(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;
}

Building the library

To build the native library:

  1. Choose “Product” → “Archive” in the XCode application menu.
  2. As the binary is built, click on “Distribute Content” button on the right.
  3. Select “Build Products” and click “Next”.
  4. Choose some location and click “Export”.
  5. You can find the built library inside usr/local/lib.
Building the Objective-C library

Lua part

Create a multi-file plugin. Let’s name it objcinterop for consistency. Copy to there libobjcinterop.so we built in the previous section.

As we do not expect anything to be returned from helloWorld(), we just call the function inside apply():

marta.expose()

plugin {
    id = "marta.example.objcinterop",
    name = "Objective-C Interoperability Test",
    apiVersion = "2.1"
}

local interop = require "libobjcinterop"

action {
    id = "test",
    name = "Test Objective-C Interoperability",
    apply = function()
        interop.helloWorld()
    end
}

Add the Lua part to init.lua. Here is how the plugin layout should look like:

Plugins/
    objcinterop/
        init.lua
        libobjcinterop.so

Now you should be able to launch Marta and run the “Test Objective-C Interoperability” action.

Congratulations!