Writing binary modules for Luart

By Samir Tine, published on October 2022

The Lua programming language is intended to be extensible. Adding new functionalities is possible through modules called "binary modules". Because these modules are written in C, they are DLL files on Windows.

The Luart runtime is ABI-compatible with Lua, so binary modules for Luart can be developed the same way as for Lua. Luart thus supports binary modules compiled for Lua. There are, however, additional C API provided by the Luartruntime for the development of binary modules to extend the capabilities of Lua modules, including module properties and module finalizers.

In this tutorial, we'll learn how to write a binary module with this additional C API. This module will be called "calc" and will provide :

  • A calc.add() function to add two numbers, returning the result as a string.
  • A calc.base property to define the formating result of the previous function ("hex" for hexadecimal, "dec" for decimal).
  • An internal module finalizer that just prints "calc module has been finalized".

Module registration

In Lua, a module registration function is needed to register the module with the package subsystem. The function must be declared as extern and defined as exported inside a DLL. The registration function must be named with a prefix luaopen_function :

//--- include the luart.h for the LuaRT C API #include <luart.h> //----- "calc" module registration function extern int __declspec(dllexport) luaopen_calc(lua_State *L) { //--- lua_regmodulefinalize() registers the specified module with a finalizer function //--- and pushes the module on the stack lua_regmodulefinalize(L, calc); //--- returns one value (the just pushed calc module) return 1; }

The registration function uses a new C API function provided by Luart, to register a binary module with a finalizer function (called when the module is garbage-collected). To register a binary module without a finalizer function, you can use the lua_regmodule() function.

Both registration functions need two static arrays to bind the module functions (available in Lua) with a C function.
As in Lua, we use luaL_Reg arrays :

//--- luaL_Reg array for module properties, postfixed by "_properties" static const luaL_Reg calc_properties[] = { {"get_base", calc_getbase}, //--- module "base" property getter {"set_base", calc_setbase}, //--- module "base" property setter {NULL, NULL} }; //--- luaL_Reg array for module functions, postfixed by "lib" static const luaL_Reg calclib[] = { {"add", calc_add}, //--- module "add" function {NULL, NULL} };

As you can see, the first array is for declaring modules properties. The name of the array must follow the pattern [module name]_properties. Properties have the same name rule as for Lua objects properties, prefixed by get_ for getter function and set_ for setter function.

The second array is for declaring modules functions. The name of the array must follow the pattern [module name]lib.

To register the module finalizer function, you just have to define the C function this way :

//--- The finalizer function must be named with a "_finalize" postfix int calc_finalize(lua_State *L) { puts("calc module has been finalized"); //--- finalizer functions returns nothing return 0; }

Now that we have defined and registered the content of the calc module, let's dive into its property and function implementation.

Implementing a module property

A module property is just a C function that is called when the user get or set the property. In fact, each property access is implemented by a C function. If a setter C function for a property is not set, then the module property will be readonly.

The getter C function for the property calc.base is implemented below :

//-- number formats supported static const char **base_format = { "0x%.04x", "%d" }; //-- current format for the result of the calc.add() function static int base_index = 0; LUA_PROPERTY_GET(calc, base) { //--- pushes the string corresponding to the current base value and returns it lua_pushstring(L, base_index ? "dec" : "hex"); return 1; }

Here, the macro LUA_PROPERTY_GET translates to a Lua C function named calc_getbase. The function is pretty simple to implement as you can see, by using standard Lua C API. The result of the getter function must then be pushed and returned.

Warning

Remember that a getter C function can only return one value. (it's a known internal Lua __index metafunction limitation).

It's now time to implement the setter C function for the property calc.base :

LUA_PROPERTY_SET(calc, base) { //--- The value at the first stack index is the value to set the property with const char *format = luaL_checkstring(L, 1); //--- Compare the provided string and set the global base variable accordingly... if (strcmp(format, "hex") == 0) base_index = 0; else if (strcmp(format, "dec") == 0) base_index = 1; else //--- ...or throw an error if the format is not valid luaL_error(L, "invalid format"); //--- A setter function don't return any value return 0; }

The macro LUA_PROPERTY_SET translates to a Lua C function named calc_setbase. You can then retrieve the value to set the property with and do anything you want, as like in this example, set the current format for the output of the calc.add() function.

Note

A setter C function uses the value at index 1 on the Lua stack to retrieve the value to set the property with.

Implementing a module function

Now that we have learn how to register a binary module, and how to implement C getter/setter function for properties, the implementation of a module function is pretty easy :

LUA_METHOD(calc, add) { char buffer[12] = {0}; //--- You access function arguments like any other Lua C function (index 1 => first argument, ...) lua_Integer a,b; //--- Here we are checking and getting integers arguments a = luaL_checkinteger(L, 1); b = luaL_checkinteger(L, 2); //--- pushes a formated string for the result of a+b, using the current base_index format... snprintf(buffer, 12, base_format[base_index], a+b); lua_pushstring(L, buffer); //--- ...and returns it return 1; }

Here, the macro LUA_METHOD translates to a Lua C function named calc_add. Module functions are like standard Lua C functions as you can see.

You know now how to define and register a Luart binary module. You have learned how to define a module property, with getter and setter C functions, and a module function. But you may ask now how we compile it to a DLL ?

Compile the binary module

To compile the binary module, you should save all the previous code or download it here : calc.c

Then open a console prompt (GCC must have been correctly installed, and Luart sources must be somewhere on your hardrive).
Enter the following command :

gcc -shared -Ipath\to\LuaRT\src\include calc.c -Lpath\to\LuaRT\src -llua54 -o calc.dll

Don't forget to set the -I flag with the path to the Luart include directory, and the -L flag with the path to the liblua54.a file (generated during Luart compilation). If everything went right, you should see a new calc.dll the binary module.

Let's use it in the following Lua script (save it in the same folder as the calc.dll module) :

-- use the calc.dll binary module -- require for the calc module (will load calc.dll) local calc = require "calc" -- Get the calc.base module property value (calls the C function calc_getbase) print("calc.base = "..calc.base) -- Call the calc.add() module function (calls the C function calc_add) print("calc.add(254, 1) = "..calc.add(254, 1)) -- Set the calc.base module property (calls the C function calc_setbase) calc.base = "dec" -- Check that the calc.base property has been changed print("calc.base = "..calc.base) print("calc.add(254, 1) = "..calc.add(254, 1))

You can now run this script using the following command :

luart usecalc.lua

Just see as the module finalizer C function is called when the scripts ends.

And you're done ! Congratulation, you just have build your first binary module using Luart extensions to the Lua C API !