blog:2023:0309_nervland_optimizing_qt6_bindings

# Optimizing QT6 bindings

So, continuing today from the end of our last session, where I introduced some initial minimal QT6 bindings to be able to display a very simple window with a menu bar, and handling a signal connection in lua. This time, we will try to improve a bit on this initial implementation to try to collect all the QT signals automatically when generating the bindings 👍! Let's see if the idea I have will work 😎

First step I would like to take is directly inspired from this article: https://woboq.com/blog/moc-with-clang.html

⇒ The idea would be to let the clang-c library find annotations where we have the signals (and slots) defined in each class. And for that the idea is to provide a custom definition for the Q_SIGNAL/Q_SLOT macros as follow for instance:

#define Q_SIGNAL  __attribute__((annotate("qt_signal")))
#define Q_SLOT    __attribute__((annotate("qt_slot")))

Let's see if we can already simply detected those annotations in the nervBind parsing…

First I added support to specify additional arguments for clang during the parsing:

    if opts.clangAdditionalArgs then
end

Next I specify a new definition on the command line… which is actually stupid lol… I already have the “NERVBIND” flag so I can add my definitions right in the binding root file (ie. “bind_context.h”), so let's do that instead:

#ifdef __NERBVBIND__
#define Q_SIGNALS                                                              \
public                                                                       \
__attribute__((annotate("qt_signal")))
#endif

OK, so now when parsing a class, let's see if we can detect that annotation: hmmm, nope, not really working. But then I figured out that I need to place my macro in shared_context.h which is included first (well after the empty parse_context.h file), and I simply have to define the “QT_ANNOTATE_ACCESS_SPECIFIER” macro as follow:

#ifdef __NERVBIND__
#define QT_ANNOTATE_ACCESS_SPECIFIER(name) __attribute__((annotate(#name)))
#endif

With that change the AST I can generate parsing the QT headers will look like this:

  - CXXAccessSpecifier "" {}
- CXXMethod "event" {attribute(dllimport) attribute(override) protected bool QAction::event(QEvent* )}: FunctionProto (bool (QEvent*))
- CXXConstructor "QAction" {attribute(dllimport) protected void QAction::QAction(QActionPrivate& dd, QObject* parent)}: FunctionProto (void (QActionPrivate&,QObject*))
- CXXAccessSpecifier "" {}
- attribute(annotate) "qt_slot" {attribute(annotate)}
- CXXMethod "trigger" {attribute(dllimport) attribute(annotate) public void QAction::trigger()}: FunctionProto (void ())
- CXXMethod "hover" {attribute(dllimport) attribute(annotate) public void QAction::hover()}: FunctionProto (void ())
- CXXMethod "setChecked" {attribute(dllimport) attribute(annotate) public void QAction::setChecked(bool )}: FunctionProto (void (bool))
- CXXMethod "toggle" {attribute(dllimport) attribute(annotate) public void QAction::toggle()}: FunctionProto (void ())
- CXXMethod "setEnabled" {attribute(dllimport) attribute(annotate) public void QAction::setEnabled(bool )}: FunctionProto (void (bool))
- CXXMethod "resetEnabled" {attribute(dllimport) attribute(annotate) public void QAction::resetEnabled()}: FunctionProto (void ())
- CXXMethod "setDisabled" {attribute(dllimport) attribute(annotate) public void QAction::setDisabled(bool b)}: FunctionProto (void (bool))
- CXXMethod "setVisible" {attribute(dllimport) attribute(annotate) public void QAction::setVisible(bool )}: FunctionProto (void (bool))
- CXXAccessSpecifier "" {}
- attribute(annotate) "qt_signal" {attribute(annotate)}
- CXXMethod "changed" {attribute(dllimport) attribute(annotate) public void QAction::changed()}: FunctionProto (void ())
- CXXMethod "enabledChanged" {attribute(dllimport) attribute(annotate) public void QAction::enabledChanged(bool enabled)}: FunctionProto (void (bool))
- CXXMethod "checkableChanged" {attribute(dllimport) attribute(annotate) public void QAction::checkableChanged(bool checkable)}: FunctionProto (void (bool))
- CXXMethod "visibleChanged" {attribute(dllimport) attribute(annotate) public void QAction::visibleChanged()}: FunctionProto (void ())
- CXXMethod "triggered" {attribute(dllimport) attribute(annotate) public void QAction::triggered(bool checked)}: FunctionProto (void (bool))
- CXXMethod "hovered" {attribute(dllimport) attribute(annotate) public void QAction::hovered()}: FunctionProto (void ())
- CXXMethod "toggled" {attribute(dllimport) attribute(annotate) public void QAction::toggled(bool )}: FunctionProto (void (bool))
- CXXAccessSpecifier "" {}
- CXXConstructor "QAction" {private void QAction::QAction(const QAction& )}: FunctionProto (void (const QAction&))
- CXXMethod "operator=" {private QAction& QAction::operator=(const QAction& )}: FunctionProto (QAction&(const QAction&))

⇒ How amazing is this ?! 😎 The only thing I really need to do here is to search of a child attribute each time I reach a CXXAccessSpecifier cursor, and with that toggle between “standard”, “signal” or “slot” mode for the following CXXMethods until that mode is changed again. That should do the trick 🤔.

To test this theory I added the following code in my parseClass() function, and this seems to work as expected:

        if ckind == clang.CursorKind.CXXAccessSpecifier then
-- scope:setCurrentVisibility(cur:getCXXAccessSpecifier())
-- QT extension: we check here if we have an annotation attribute:
local child = cur:getFirstChild()
if child and child:getKindSpelling() == "attribute(annotate)" then
currentFuncType = child:getSpelling()
logDEBUG("Entering function type ", currentFuncType)
elseif currentFuncType then
logDEBUG("Leaving function type ", currentFuncType)
currentFuncType = nil
end

Now, I will also store that “category” information in the created functions inside a given class:

---Assign a categoy to this function
---@param cat string|nil The category
function Class:setCategory(cat)
self._category = cat
end

---Retrieve the current category from this function
---@return string|nil category this function category or nil
function Class:getCategory()
return self._category
end

Now we still have a bunch of CXXMethods in each class, but some of them will have the category “qt_signal”, and others “qt_slot”, (and otherwise a nil value by default). What should I do with these ?

For a signal function triggered(bool val) I could maybe generate another function in the bindings, called onTriggered(luna::LuaFunction& func)

We could then provide a custom implementation for that new function that would write something like that:


static auto _bind_onTriggered_sig1(lua_State* L) -> int {
QAction* self = Luna< QAction >::get(L,1);
LUNA_ASSERT(self!=nullptr);

luna::LuaFunction func(L, 2);

auto* dst = new QLuaConnector(func);

auto con_src = QObject::connect(self, QAction::triggered, dst, QLuaConnector::handle);
auto* con = new QMetaObject::Connection(con_src);

// Store the QLuaConnector object in a lua table here with con as key
// Luna<QObject>::push(L, dst, true);

Luna<QMetaObject::Connection>::push(L, con, true);
return 1;
}

Or even better, since we can use a lambda or a functor for the connection here, I could now write something like that (completely avoiding the need for a QLuaConnector object then):

static auto _bind_onTriggered_sig1(lua_State* L) -> int {
QAction* self = Luna< QAction >::get(L,1);
LUNA_ASSERT(self!=nullptr);

luna::LuaFunction func(L, 2);

func.makeRef();

auto con_src = QObject::connect(self, QAction::triggered, self, func);
auto* con = new QMetaObject::Connection(con_src);

Luna<QMetaObject::Connection>::push(L, con, true);
return 1;
}

⇒ That last option would be very cool as it will be a lot easier to use/maintain and less memory will be used in the process. So let's try to implement that. Yet, one detail I'm thinking about now is that the “function category” value I added above should really be on a given signature only each time: we might have multiple signatures for a function name and only one of them declared as a signal for instance, so let's start with fixing that:

            -- Add the category to the signature:
if currentFuncCat then
local ftype = cur:getType()
local signame = ftype:getSpelling()

local sig = obj:getSignatureByName(signame)
CHECK(sig ~= nil, "Invalid signature object")
---@cast sig -nil
sig:setCategory(currentFuncCat)
end

Next, let's add this makeRef() method in the LuaFunction that we will need above, here is the updated code for that:

    LuaFunction(lua_State* L, int idx, bool asRef = false)
: state(L), index(idx) {
LUNA_ASSERT(lua_isfunction(state, index));
if (asRef) {
makeRef();
}
};

~LuaFunction() {
if (isRef && nv::LuaManager::instance().is_state_opened(state)) {
// Unregister the ref:
luaL_unref(state, LUA_REGISTRYINDEX, index);
}
};

void makeRef() {
// Turn this lua function into a ref if it's not a ref already:
if (isRef) {
return;
}

isRef = true;
// Keep a ref on this function:
lua_pushvalue(state, index);

// Store the reference on the function and keep its ref index:
// NOLINTNEXTLINE
index = luaL_ref(state, LUA_REGISTRYINDEX);

lua_State* L = state;
LUNA_ASSERT(index != LUA_NOREF);
}

Next we need to collect all the signatures marked as “qt_signal” in a post-processing step: And this I think could be enabled from the main config file for the QT bindings 🤔 ?

Okay, so for now I decided to write a simple “Entity processor” as follow:

--- Processor function that will generate QT signals where applicable:
---@param ent reflection.Entity Current entity
---@return boolean continue flag
local processor = function(ent)
if ent:isFunction() then
local func = ent --[[@as reflection.Function]]
if not func:isClassMethod() then
return false
end

-- This is a class method, so we check if one of the signature is a signal:
local sigs = func:getSignaturesByCategory("qt_signal")
if #sigs == 0 then
-- No signal for this method
return false
end

logDEBUG("==> Detected " .. #sigs .. " signal(s) for function " .. func:getFullName())

-- TODO: Should process the function here.
return false
end
return ent:isNamespace()
end

return processor


And then I simply add this processor at the end of the parsing of the QT input files from the config:

-- Signal generator:

return cfgtor:createConfig("QT", opts)

And this will produce the following outputs when executed (which is exactly what I wanted):

2023-03-08 08:13:48.607968 [DEBUG] clang done parsing file: D:\Projects\NervLand\sources/lua_bindings/QT\luna_bindings.h
2023-03-08 08:13:48.607972 [DEBUG] Resolving target types...
2023-03-08 08:13:48.608175 [DEBUG] Done resolving 0 target types.
2023-03-08 08:13:48.608185 [DEBUG] TypeManager: registered 0 types.
2023-03-08 08:13:48.610431 [DEBUG] ==> Detected 1 signal(s) for function QObject::destroyed
2023-03-08 08:13:48.610448 [DEBUG] ==> Detected 1 signal(s) for function QObject::objectNameChanged
2023-03-08 08:13:48.611214 [DEBUG] ==> Detected 1 signal(s) for function QWidget::windowTitleChanged
2023-03-08 08:13:48.611225 [DEBUG] ==> Detected 1 signal(s) for function QWidget::windowIconChanged
2023-03-08 08:13:48.611228 [DEBUG] ==> Detected 1 signal(s) for function QWidget::windowIconTextChanged
2023-03-08 08:13:48.611232 [DEBUG] ==> Detected 1 signal(s) for function QWidget::customContextMenuRequested
2023-03-08 08:13:48.611347 [DEBUG] ==> Detected 1 signal(s) for function QCoreApplication::aboutToQuit
2023-03-08 08:13:48.611353 [DEBUG] ==> Detected 1 signal(s) for function QCoreApplication::organizationNameChanged
2023-03-08 08:13:48.611356 [DEBUG] ==> Detected 1 signal(s) for function QCoreApplication::organizationDomainChanged

As mentioned above, next we need to manually generate that signal helper function (ie. “onTriggered(bool)” for instance). The updated code for the processor will thus be:

--- Processor function that will generate QT signals where applicable:
---@param ent reflection.Entity Current entity
---@return boolean continue flag
local processor = function(ent)
if ent:isFunction() then
local func = ent --[[@as reflection.Function]]
if not func:isClassMethod() then
return false
end

-- This is a class method, so we check if one of the signature is a signal:
local sigs = func:getSignaturesByCategory("qt_signal")
if #sigs == 0 then
-- No signal for this method
return false
end

logDEBUG("==> Detected " .. #sigs .. " signal(s) for function " .. func:getFullName())

-- Create the new function the class:
local cl = func:getParent() --[[@as reflection.Class]]

local sig_func_name = "on_" .. func:getName()
-- That function name should not be available already:
CHECK(cl:hasFunction(sig_func_name) == false, "Signal function name ", sig_func_name, " already exists in ",
cl:getFullName())

local signal_func = cl:getOrCreateFunction(sig_func_name)
local tm = luna.tm

-- Create the signatures:
for _, refsig in ipairs(sigs) do
local sig = signal_func:createSignature(refsig:getName())

sig:setVisibility(clang.Vis.PUBLIC)
sig:setDllImported(true)
sig:setDefined(true)

sig:setReturnType(tm:getType("void"))

end

return false
end
return ent:isNamespace()
end

For now I decided to got with function names as “on_triggered”, to make it clear that this is now really an official function from QT… not sure I will keep it this way though

This processor will indeed the generate the bindings functions properly, for instance:

// Bind for on_enabledChanged (1) with signature: void (bool)
static auto _bind_on_enabledChanged_sig1(lua_State* L) -> int {
QAction* self = Luna< QAction >::get(L,1);
LUNA_ASSERT(self!=nullptr);

luna::LuaFunction func(L, 2);

self->on_enabledChanged(func);

return 0;
}

// Overall bind for on_enabledChanged
static auto _bind_on_enabledChanged(lua_State* L) -> int {
if(_check_on_enabledChanged_sig1(L)) return _bind_on_enabledChanged_sig1(L);

LUNA_ERROR_MSG("Current lua stack: "<<luna::dumpStack(L));
// NOLINTNEXTLINE
luaL_error(L, "Binding error for function on_enabledChanged, cannot match any of the 1 signature(s):\n  sig1: void (bool)");
return 0;
}

The problem with the code generated above is that the function call self→on_enabledChanged(func); doesn't actually make any sense since that function doesn't really exists 😄.

⇒ What I need here is a new mechanism to inject some custom calling code for those functions, but that should not be too hard to provide in fact, let's see…

So for the next update cycle I used this custom code generation:

            -- We should generate some custom code to be injected for this function signature:
local code = [[func.makeRef();

auto con_src = QObject::connect(self, &${func_name}, self, func); auto* con = new QMetaObject::Connection(con_src); Luna<QMetaObject::Connection>::push(L, con, true); return 1;]] -- code = code:replaceAll("${sig_func_name}", sig_func_name)
code = code:replaceAll("${func_name}", func:getFullName()) code = code:replaceAll("${sig_idx}", idx)

sig:setCustomCode(code)

Which gives us a nice looking code in the bindings:

static auto _bind_on_toggled_sig1(lua_State* L) -> int {
QAction* self = Luna< QAction >::get(L,1);
LUNA_ASSERT(self!=nullptr);

luna::LuaFunction func(L, 2);

func.makeRef();

auto con_src = QObject::connect(self, &QAction::toggled, self, func);
auto* con = new QMetaObject::Connection(con_src);

Luna<QMetaObject::Connection>::push(L, con, true);
return 1;
}

But unfortunately, this will not compile yet since I deleted the copy constructor for LuaFunction 😅, but maybe I can enable that in the end… Hmmm, not quite: would really not be a good idea to have multiple copy of a reference lua function 😵‍💫 so let's try a lambda instead passing the function as reference… arrfff, naaa, that's too much plain, let's try to think a bit harder on the LuaFunction copy constructor instead… [thinking hard…] Eureka! And here we go:

    LuaFunction(const LuaFunction& rhs)
: state(rhs.state), index(rhs.index), isRef(rhs.isRef) {
if (rhs.isRef) {
// We need special care here in case the source function is already
// a reference: We push the referenced value on the stack:
lua_rawgeti(state, LUA_REGISTRYINDEX, index);

// And next we make another ref on it:
index = luaL_ref(state, LUA_REGISTRYINDEX);

lua_State* L = state;
LUNA_ASSERT(index != LUA_NOREF);
}
};

⇒ Now let's see if we can compile all this… Oooppss… nope: seems we'll try to push a QPalette at some point on the stack, which we don't support yet ⇒ So yeah, I should probably first check that my signal function is bindable 😅

And I also had to introduce a simple operator() function in LuaFunction taking no argument at all (and thus discarding pushArgs()):

    template <typename... Args> void operator()(const Args&... args) {
pushSelf();
pushArgs(args...);
execute();
}

// Simple version with no arguments:
void operator()() {
pushSelf();
execute();
}

Hmmm, but sig:isBindable() doesn't seem to help filtering out the unsupported classes yet how could that be 🤔? Ohh, of course: that's because I should really execute that entity processor after ignoring the classes/functions/etc:

    cfg.processEntities = function(root, tm)
-- local cl = root:getOrCreateClass("void")
-- cl:setDefined(false)
root:foreachClass(ignoreClasses)

root:foreachEntity(ignoreFuncs)

root:foreachEntity(ignoreEnums)

for _, proc in ipairs(self.entityProcessors) do
root:foreachEntity(proc)
end
end

Arrggg… Still not compiling then: we still have an error when trying to push a QLayoutDirection value on the stack: I suspect this is because this is an enum class ? HHmmm, well, no, not even: it's really just a simple enum, but I need to handle pushing such things then…

⇒ I eventually found a way to handle this, introducing an additional custom LunaPusher specialization (with an additional template parameter in the parent class template):

template <typename T>
struct LunaPusher<T, typename std::enable_if<std::is_enum<T>::value>::type> {
static void pushValue(lua_State* L, const T& arg) {
lua_pushinteger(L, (int)arg);
}
};

And now this is finally compiling! Hhoouurraa 🥳!

Now time to give this a try using this updated code:

    local menu = mbar:addMenu("&File") --[[@as Qt.QWidget]]
local act = Qt.QAction(gutils.createIcon("folder-open-document"), "Open file...", self.win)
act:on_triggered(function(val) logDEBUG("Yeeppee! It works! value is: ", val) end)

⇒ And this will indeed works just fine! Yeeppeee 🥳!

So now, we have our QT signals available in lua automatically, and we don't even need the QLuaConnector object class anymore (but I'll just keep it around as reference for a while, in case I need to write some QT class in C++ directly later).

Yet, there is still one annoying issue here that I would like to fix, and that's the issue we currently have with the overloading of a parent class function in a child class, like this is the case for instance with QWidget::addAction vs QMenu::addAction

Hmmmm… well, this is indeed a problem that would deserve to be solved eventually, but in fact here, it seems the QMenu::addAction methods are deprecated ⇒ So I could just as well ignore them in the bindings config!

And with that minimal change, I can now use the correct/desired syntax in the lua file:

    -- Add menu bar:

local act = Qt.QAction(gutils.createIcon("folder-open-document"), "Open file...", self.win)
act:on_triggered(function(val) logDEBUG("Yeeppee! It works! value is: ", val) end)
menu:addAction(act)

From this point, I'm actually not quite sure what I should really do next: I have many ideas in mind that I would like to investigate, and only so much time to do it

I would like to really consider how to integrate Vulkan inside my QT framework in an optimal way, because in the end, I still want to do most of my “sub-projects” in a 3D environment, but I feel this is going to be another trick step to take so I should keep that for another time.

I would also like to start implementing a stable-diffusion project in QQT but this is again going to be really tricky…

I also have my NervStudio project which I could re-implement here in lua, but again… (guess what?) That will be tricky and will require a working vulkan environment 😅

So I think I should just stop here for the moment and think more about what I should really do as a next step before dying in any further

• blog/2023/0309_nervland_optimizing_qt6_bindings.txt