Table of Contents

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 😎

Retrieving the signal functions from the QT header files

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
        table.appendAll(cfg.clangArgs, opts.clangAdditionalArgs)
    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

Additional bindings for signals

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:
cfgtor:addEntityProcessor(import "bind.processors.gen_qt_signals")

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

Manually generating the signal helper functions

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"))

            sig:addArgument(Argument("func", tm:getType("luna::LuaFunction&")))
        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;
}

Providing custom code for our function bindings

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 🥳!

Using this new signal bindings mechanim

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

Fixing the parent functions overloading issue

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 mbar = self.win:menuBar()

    local menu = mbar:addMenu("&File") --[[@as Qt.QMenu]]
    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)

What to do next ?

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 ;-)