C FFI with Luart

By Samir Tine, published on January 2025

The C module

The C module in Luart provides C compatible data types and allows calling functions in DLL libraries, compiled in another programming language such as C/C++. It can be used to wrap these libraries in pure Lua.

This tutorial covers the key functionnalities of this foreign function interface to Lua.

Loading Dynamic Link Libraries (DLL)

The C module provides on object oriented approach to the foreign function interface. Loading a DLL is the first step before accessing its content (variables, functions,...).
The Library object is used to load a DLL in memory, by using the DLL path when creating a Library instance :

-- use the C FFI module local c = require "c" -- create a new Library instance to load the "kernel32.dll" local kernel32 = c.Library("kernel32.dll")

By default, the current folder and the system PATH are used to search the DLL. If no argument is provided, the standard C library is loaded.

Accessing DLL functions

Before using a DLL function, it must be defined first. Function definition is used to understand how to call the function, and what kind of parameters the function expects.
To define a function, just set a new Library instance field with the exact name of the DLL function. The value of this field must be a string. This string defines, the C type for the arguments and for the return value:

-- use the C FFI module local c = require "c" -- create a new Library instance to load the "kernel32.dll" local kernel32 = c.Library("kernel32.dll") -- define the kernel32 Beep() function kernel32.Beep = "(JJ)B"

In this case, the Beep() functions expects two 32bits unsigned integer (represented by the `J` character) and a bool return value (represented by the Bcharacter).

As you can see, each character between the parenthesis is associated with a C type for the corresponding argument (first character for the first argument and so on...). Then, after the parenthesis, the last character represents the return value type (and if omitted, the function will return nothing). This string is called a function signature. You can find other signature character in function definition.

Once the function have been defined with its signature, it can be called from Lua as any other Lua function :
kernel32.Beep(750, 300)

Luart will convert internaly the given Lua value to the corresponding C type, using the function signature previously defined. The same for the return value. Rather simple !

Using C data types

The C FFI module provides a set of C data types abstracted around the Value object :

-- use the C FFI module local c = require "c local int32 = c.int32(405) print(int32) -- Output : 405 local float = c.float(3.14) print(float) -- Ouput : 3.14 local str = c.string("Hello") print(str) -- Output : "Hello"

The Value object can be used to convert the internal C value to a string, to a number, and can be used for mathematical operations for numerical values.

Working with Structures

The Struct object allow grouping different C data types together, similar to C structs. To use a C struct from Lua, you must first create a C Struct definition to define it. The Struct definition is used to define each field and to calculate the Struct size in memory :

-- use the C FFI module local c = require "c -- Create a Struct definition object local POINT = c.Struct("ii", "x", "y")

Struct definition need to know each C type of its fields. That's why the Struct constructor takes a signature string as first parameter, that contains a succession of characters, each character defines the field type, in order of the provided field names. In this example, we have created a C struct object definition POINT that contains two C integer fields, named "x" for the first field and "y" for the second.You can even use Struct and Union as fields.

To create a new Struct from this definition, just call it. All fields will be initialized to zero by default, unless a table with the field values ​​is provided:

-- use the C FFI module local c = require "c -- Create a Struct definition object local POINT = c.Struct("ii", "x", "y") -- Creates a new Struct of type POINT local p1 = POINT() -- Creates a new POINT Struct with initialization values local p2 = POINT { x = 300, y = 200 } print(p1.x, p1.y) -- Outputs 0 0 print(p2.x, p2.y) -- Outputs 300 200

Working with Unions

The Union object allows to store different data types in the same memory location. It is similar to a struct, but with a key difference: all members of a union share the same memory space. To use a C union from Lua, you must first create a C Union definition to define it. This Union definition is used to define each field and to calculate the Union total size in memory :

-- use the C FFI module local c = require "c -- Create an Union definition object local CHAR = c.Union("Cc", "char", "byte")

Union definition need to know each C type of its fields : the Union constructor takes a signature string as first parameter, that contains a succession of characters, each character defines the field type, in order of the provided field names, as for Struct object. In this example, we have created a C struct object definition CHAR that contains a C unsigned char field, named "char", and a "byte" field of C type char.You can even use Struct and Union as fields.

To create a new Union from this definition, just call it. The Union memory will be initialized to zero by default :

-- use the C FFI module local c = require "c" -- Create an Union definition object local CHAR = c.Union("cC", "char", "byte") -- Creates a new Struct of type CHAR local ch = CHAR { byte = 65 } print(ch.char) -- Outputs 'A'

Working with C Pointers

C memory pointers can be manipulated using the Pointer object. Pointers are either pointing to a mermory address returned from a C function, or pointing to a Value, Struct, Union
Here is how you can create a C pointer in Lua :

-- use the C FFI module local c = require "c" -- Create a new C string, initialized with "Hello" local cstr = c.string("Hello") -- Create a Pointer to this C string local ptr = c.Pointer(cstr)

You may ask why the program exits without waiting and printing the message ?

The reason is simple : when called inside a Task, the sleep() function don't wait for the time to elapse, it puts the current Task to sleep and bring back execution flow to another Task or to the global execution flow. To illustrate this, we will add a last line of code :

function hello(name) -- wait for 2sec before printing the message sleep(2000) print("Hello "..name.." !") end task = sys.Task(hello) task("LuaRT") -- prints a message before exiting print("Program is exiting now !")

At the end of the program, the execution flow is stopped and the Task is cancelled. If you want to wait for a specific Task to terminate, you will need to call the Task.wait() method :

function hello(name) sleep(2000) print("Hello "..name.." !") end task = sys.Task(hello) task("LuaRT") -- Wait fot the task to terminate task:wait() print("Program is exiting now !")

Congratulation, you just have coded your first concurrent program with Luart !
As you can see, the Task.wait() function stops current execution flow until the Task is terminated, hence the message printed after 2 seconds. Now we will write several messages one after another, with a delay of one second between :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 -- Create and start only 3 Tasks, not more if n < 4 then sys.Task(hello)("from task #"..n) end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) task:wait()

In this example, once a Task is about to terminate, we create and start a new Task, and so on, with a maximum of 3 Tasks. But in this case, we are only waiting on the first task, that's why you will see only the first message. One thing you can do is to wait for each created tasks :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 -- Create and start only 3 Tasks, not more if n < 4 then local newtask = sys.Task(hello) newtask("from task #"..n) newtask:wait() end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) task:wait()

It works now as expected, but the code is bloated. Instead of waiting for each Task, you can use the waitall() function to wait for all Tasks to terminate :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 if n < 4 then sys.Task(hello)("from task #"..n) end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) -- Wait for all Tasks to terminate waitall()

As you can see the waitall() function is very convenient to block execution flow until all Tasks are terminated.
Here is a more simple approach to the previous example :

function hello(delay, msg) sleep(delay) print("Hello "..msg.." !") end for n=1,3 do sys.Task(hello)(i*1000, "from task #"..n) end waitall()

Well this first tutorial on concurrent programming with Luart comes to its end. You have learned the basics to create and start a new Task, to bring execution flow to other Tasks, and to wait on them.


The next part of the concurrent programming tutorial will cover the famous async/await functions.