In the Using LuaRocks dependencies tutorial, you might have learned that you can use external libraries from LuaRocks in a Marta plugin. Of course, it applies to pure Lua libraries, but also to ones that contain native components written in C, C++, or any other compiled language. So, for example, you can get access to POSIX APIs from Lua using the luaposix package.
In fact, it is easy to embed native code in a Marta plugin, and you do not need LuaRocks for it. In this tutorial, we will create a simple plugin that calls a C function from Lua and shows an alert using the Marta API with the text we received from native code.
I will not use any IDEs or build tools, such as CMake, to keep things as simple as possible. However, if you feel confident, please use the tools you find convenient.
Getting started
First of all, we need a C compiler to build our plugin. This tutorial uses Clang. Check if it is already installed on your Mac by running this command in the terminal:
clang --version
If Clang is not found, you will be prompted to download and install the command line developer tools.
Plugin structure
As our plugin consists of several parts, we will create a complex (multi-file) plugin. For simplicity, we will keep all we need in cinterop
right inside the Plugins
folder.
Plugins/
cinterop/
init.lua
cinterop.c
libcinterop.so
build.bash
Lua part
Lua provides the require()
function for loading modules, be they additional Lua scripts or native libraries. For complex plugins, Marta sets package.cpath
automatically so you can load a library straight away:
local interop = require "libcinterop"
If executed, Lua will search for the libcinterop.so
library in a plugin folder.
In our library, we will have a single function helloWorld()
which returns a string. So let’s make an action that displays it in an alert, like in the very first tutorial. Put the following code to init.lua
:
marta.expose()
plugin {
id = "marta.example.cinterop",
name = "C Interoperability Test",
apiVersion = "2.2"
}
local interop = require "libcinterop"
action {
id = "test",
name = "Test C Interoperability",
apply = function()
martax.alert(interop.helloWorld())
end
}
C part
Normally, a C function returning a string literal looks like this:
const char* helloWorld() {
return "Hello from C!";
}
However, to be able to call the function from Lua, we need to use the Lua C API:
#include "lauxlib.h"
int helloWorld(lua_State *L) {
lua_pushstring(L, "Hello from C!");
return 1;
}
All functions accessible from Lua must accept a pointer to lua_State
and return an integer (a number of return values).
Lua is a stack-based virtual machine. When a function is called, its arguments are pushed onto the stack, and you can read them with functions such as lua_tolstring()
. When you return from a function, you push return values and return the number of them. Our function has zero parameters and returns a single value; we push it using the lua_pushstring()
function and return 1
.
You can find more information on the C API in the Lua reference. Also, I recommend the book “Programming in Lua”. The first edition of it is freely available on the internet.
However, even if we declare our function appropriately, Lua will not be able to load the library as we did not create its entry-point. Let’s add it:
static const struct luaL_Reg libcinterop [] = {
{"helloWorld", helloWorld},
{NULL, NULL}
};
int luaopen_libcinterop(lua_State *L) {
luaL_newlib(L, libcinterop);
return 1;
}
You can declare as many functions as you want, yet the last element shouls always be {NULL, NULL}
.
🐧 The entry point name (luaopen_libcinterop
→ libcinterop
) must match the module name specified in the require()
call.
That’s it! Put code from the listing below to cinterop.c
:
#include "lauxlib.h"
static int helloWorld(lua_State *L) {
lua_pushstring(L, "Hello from C!");
return 1;
}
static const struct luaL_Reg libcinterop [] = {
{"helloWorld", helloWorld},
{NULL, NULL}
};
int luaopen_libcinterop(lua_State *L) {
luaL_newlib(L, libcinterop);
return 1;
}
Compiling C code
The last thing we need to do is to compile the C source into a library.
We used the Lua C API, so we need to provide the compiler paths to the headers and the Lua library. Fortunately, all it can be found right inside the Marta application bundle:
- Headers:
Contents/Frameworks/LuaKit.framework/Versions/Current/Resources/include
liblua.dylib
:Contents/Frameworks/LuaKit.framework/Versions/Current/Frameworks
For simplicity, in this tutorial we will link against the Marta distribution installed to /Applications
.
LUAKIT_PATH=/Applications/Marta.app/Contents/Frameworks/LuaKit.framework/Versions/Current
clang \
-shared \
-o libcinterop.so \
-I$LUAKIT_PATH/Resources/include \
-L$LUAKIT_PATH/Frameworks \
-llua \
-mmacosx-version-min=10.12 \
-arch arm64 \
-arch x86_64 \
cinterop.c
Build the library by running the snippet above in the terminal. You might want to put it to a bash file so you can easily recompile cinterop.c
whenever you want.
Here we passed a few options worth explaining:
-shared
instructs the compiler to build a shared library;-mmacosx-version-min=10.12
builds a binary compatible with all macOS versions supported by Marta (for Marta 0.8.1, it is macOS Sierra);-arch arm64 -arch x86_64
build a universal binary that runs both on Intel-based Macs and Macs with an Apple Silicon chip.
If all is done right, you should have a libcinterop.so
in the plugin directory. Then, restart Marta and run the “Test C Interoperability” action. An alert should pop up.
If the action is absent, check the error messages in the Console.
Summary
The plugin we created during the tutorial was trivial, but it doesn’t have to be so. You can link your native code against various libraries, including Cocoa, and use other languages, such as Objective-C, Swift, Rust, Kotlin, or any other that provides C interoperability.
You can also write the whole plugin in a native language and use Lua solely as a glue between Marta and your functionality. Lua API calls can be incapsulated, so you will deal not with lua_State
, but with ActionContext
and ListModel
.