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 B
character).
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.