Simple Lua console in Qt

Lua is a simple, fast and embeddable scripting language. Capability to run Lua scripts can be relatively easily added into C and C++ applications. Due to it’s ease of use and performance it’s being used in computer games (Crysis, Garry’s mod, …), general software (Wireshark, Neovim, …) and also embedded devices (Logic Machine). Let’s take a look how to create a very simple Lua console GUI with Qt.

Creating a new project

Use QtCreator to create a new QMainWindow based Qt project. In this example I’m using Kubuntu 23.10 and compiling with Qt 6.6.1 for desktop.

Note: this project was created as a proof-of-concept of embedding Lua scripting engine into a Qt Widgets based program using QMake build system. If you are creating new applications with Qt, take a look at QML/QtQuick and CMake.

Choosing a Lua library

To run Lua scripts you’ll need an interpreter. There are several of them to choose from. I’ll use the default one that’s in the Ubuntu repository – Lua 5.4.

sudo apt install lua5.4 liblua5.4-dev

Theoretically that’s everything you need but using Lua from C++ this way is a pain. You would have to use the cumbersome Lua C API. To make our lives easier, let’s use a binding library. Again, there are several options available. One of the most popular ones is Sol2. Let’s try it out.

Clone Sol2 source as a Git submodule to project’s repository. It’s generally a good idea to create a “lib” directory in your project directory for storing libraries. Open this directory and clone the repo:

git submodule add https://github.com/ThePhD/sol2.git

A new directory “sol2” with source code should be created now. Let’s link it into the Qt project by extending the project’s .pro (QMake) file. This step is very distribution and version-specific. You might have to change it a little bit if you run different distribution or use a different version of Lua than me. Or better use a different approach if you are trying to build an actual application.

First, let’s add Lua headers to include path:

INCLUDEPATH += \
    lib/sol2/include \
    /usr/include/lua5.4

The first line defines path to Sol2 includes and the second to system Lua installation.

Second, tell the linker to link with Lua library dynamically:

LIBS += -llua5.4

That’s it. We should be able to build the project now and start using Lua. Try to compile the project to check everything is ok.

Now let’s do the Hello world thing by following the Sol2 tutorial. Extend your main.c with:

#define SOL_ALL_SAFETIES_ON 1
#include "sol/sol.hpp"

And run the Lua script from your main function:

sol::state lua;
lua.open_libraries(sol::lib::base);

lua.script("print('bark bark bark!')");

If you run the program now, your console output should bark.

Building the GUI

The simplest scripting console should have an input window, output window and a “run” button. Open the MainWindow .ui file in QtDesigner and create these.

Try to run the program to see if the modified window appears.

Running a user-defined script means waiting for the “Run” button click, reading the user input, running it and displaying the result in the output window. Let’s modify the MainWindow class now. In this example it’s called “Console”. Create a new slot:

public slots:
    void run() const

In the constructor, connect the slot to button’s clicked() signal:

connect(ui->runButton, &QPushButton::clicked, this, &Console::run);

In the slot body we should initialize Lua. Initializing it every time might not be the most efficient solution but it will be initialized in a clean state every time.

sol::state lua;
lua.open_libraries(sol::lib::base);

I’ve created the input box as a QTextEdit, the basic multiline text editor widget. Since it supports text formatting, to read the plain text written in it, call the QTextEdit::toPlainText().

We can theoretically call and run the code now calling the lua::script() function in the slot:

lua.script(ui->inputBox->toPlainText().toStdString());

However this has several problems

  1. The script output is printed to console and is not displayed anywhere in GUI
  2. Any errors in script cause the whole application to crash
  3. Error output is also printed only to console

The first problem can be solved by redefining the Lua print function. It is theoretically possible to redirect the output deeper inside Lua, but for a simple example this is the easier option. For printing the simple hello world string as shown above we could just define a function that takes one string as a parameter. But since the real Lua “print” function takes N arguments of different types, we have to accept variadic arguments, iterate over them in a loop and stringify each using the lua “tostring” function.

QStringList outputStrings;

lua.set_function("print", [&lua, &outputStrings](sol::variadic_args args) {
    for (auto arg: args) {
        auto ba = QByteArray::fromStdString(lua["tostring"](arg.get<sol::object>()).get<std::string>());
        outputStrings << ba;
    }
});

Catching script run errors during execution can be done by running the “safe_*” version of execution functions, like lua.safe_script(). These functions return an error to be checked instead of just crashing the application. The optional second argument of lua.safe_script() function takes a lambda function to be run on error:

QStringList errorStrings;

auto result = lua.safe_script(script.toStdString(), [&errorStrings](lua_State*, sol::protected_function_result pfr) {
    // error occured during script execution - print it
    sol::error err = pfr;
    errorStrings << tr("Script error");
    errorStrings << err.what();

    // return the protected_function_result
    return pfr;
});

If we only want to display the text description of an error, we can ignore “result” and “pfr” variables further.

Now, if we run the code and there is an error, it gets stored in errorStrings variable. The normal text output is stored in outputStrings variable. The simplest way to communicate errors to the user is to just display the error string. Let’s just display it if there is any:

if (errorStrings.isEmpty()) {
    ui->outputBox->setPlainText(outputStrings.join("\n"));
} else {
    ui->outputBox->setPlainText(errorStrings.join("\n"));
}

Lines of output are joined by newline character “\n”. This could possibly lead to output text being formatted differently than when using the real Lua print() function but is good enough for a simple demo.

That’s it. You can run Lua scripts in your new Lua Console!

You can also view the complete source code used in this example.

Leave a Reply

Your email address will not be published. Required fields are marked *