qt6

local qt = require 'qt6'

This module exposes essential features from Qt to access Qt Quick.

Qt will run on its own GUI thread. The Lua module will always automatically suspend the caller fiber until the operation is fully committed, so this implementation detail is mostly hidden. However a few big caveats still exist.

The GUI thread runs independent from Emilua threads. When the Lua program is running, the GUI thread is not blocked. That’s rather different from the traditional scenario of GUI-based applications. Suppose you’re designing a form and you want to process entered data when a button is clicked.

qml_context.property.button['clicked()'] = function()
    local props = qml_context.property
    process(props.field1, props.field2, props.field3, ...)
end

The callback doesn’t run in the GUI thread. Therefore the fiber will suspend every time you try to access a property from the GUI thread. The implementation will send a message to the GUI thread and it’ll awake the fiber when it reads the reply back. Meanwhile the GUI thread is still processing user events sent by the window subsystem from the running OS. So it’s possible that the form data read won’t reflect the state it had at the time the callback was triggered. That’s not how GUI toolkits traditionally work. The traditional way is to run the callback on the same thread, and while the callback is running the state is frozen as the GUI is precluded from processing further user events queued on the window subsystem’s socket.

Suppose you’re creating a chat application. The user will type a message and quickly press Enter. He may immediately start to type a second message as soon as Enter is pressed. When your application tries to read the contents of the text input, it may already contains text from extra keys the user entered after pressing Enter. The solution to this race is to write QML code to process the data locally in the GUI thread and send a signal with the final result to the application.

ApplicationWindow {
    signal newMessage(message: string)

    Connections {
        target: button
        function onClicked() {
            var txt = input.text
            input.clear()
            newMessage(txt)
        }
    }
}

And then, on the Lua program, you subscribe to the newMessage() signal:

qml_context.object['newMessage(QString)'] = function(text)
    process(text)
end

A second caveat is that you can’t actually suspend the fiber in any signal’s callback. The callback runs in an unspecified fiber which is just another way to say that suspension can (and will) be forbidden in that fiber. A simple way to solve this problem is to write callbacks that spawn new fibers. However the most appropriate solution will be dependant on what you’re trying to achieve.

qml_context.object['newMessage(QString)'] = function(text)
    spawn(function() process(text) end):detach()
end

And last, if you need to trigger a transaction from the Lua program to modify the GUI thread (e.g. change multiple properties at once in an atomic manner), the solution is to encode the transaction as QML code and call this function from the Lua program with your data.

ApplicationWindow {
    function my_transaction(a: string, b: int) {
        // ...

        return result
    }
}

And then, from the Lua program:

local result = qml_context.object(
    'my_transaction(QString,int)',
    'foobar', 1234)

These tools should cover any needs you might have to develop GUIs using Qt Quick. The bridge layer in the Lua program to communicate with the GUI is focused solely on data passing. The GUI can be fully defined using QML. Use Lua to define the program logic and QML to define the presentation GUI. All interfaces are non-blocking and the program will stay very responsive.

Implementation details

Qt permanently modifies the process state once loaded. To avoid any problems, Qt libraries will never be unloaded (i.e. we intentionally provoke an one-time leak).

Qt has a lot of restrictions with respect to the GUI thread and its integration with Qt Platform Abstraction (QPA). This Emilua plugin will only load the Qt libraries from the thread reserved to the GUI thread as to avoid any problems with QPA.

Qt “remembers” the main thread (which we lie about). Destructors for global variables that are only triggered once the process exits (i.e. when the real main thread exits) may reference the wrong thread, and you’ll see the following warning at application exit:

QObject::~QObject: Timers cannot be stopped from another thread

It’s not possible to get rid of this warning. This warning is harmless.

The GUI thread will be shared to the whole process. Any Lua VM trying to communicate with Qt will perform message passing against the same Qt event loop.

Reference counting will be used to avoid dangling references. If you want to dodge the GC (e.g. the destruction of the main application window), just save the object to a global. If you want to make sure all handles are destroyed, call system.exit() to kill the calling VM (and all state it was holding). It may be useful for some programs to reserve a Lua VM to only perform interactions with the GUI while another Lua VM deals with the program logic.

If an object is part of the QML tree, do not call Qt’s deleteLater() directly from Lua as that will provoke a double-free (ownership is already transparently taken care of by automatic reference counting). If you manage to somehow pluck the object out of the QML tree (e.g. Qt’s setParent()), then it’s safe to explicitly call deleteLater().

Given Qt runs in a different thread, your program won’t stall on things such as blocking file operations when the GUI is trying to access new resources (e.g. a menu icon on a slow disk).

Functions

load_qml(qml: string|byte_span) → qml_context

qml
string

An URL for the QML resource.

byte_span

The QML’s contents.

register_resource(rcc: filesystem.path|byte_span, map_root: string) → boolean

rcc
filesystem.path

The RCC file name.

byte_span

The RCC data.

Registers the resource at the location in the resource tree specified by map_root, and returns true if the file is successfully opened; otherwise returns false.

unregister_resource(rcc: filesystem.path|byte_span, map_root: string) → boolean

rcc
filesystem.path

The RCC file name.

byte_span

The RCC data.

Unregisters the resource at the location in the resource tree specified by map_root, and returns true if the resource is successfully unloaded and no references exist for the resource; otherwise returns false.