====== Optimizing QT6 bindings ====== {{tag>dev cpp qt6 nervland lua nervluna}} 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::push(L, dst, true); Luna::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::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: "< ===== 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::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::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 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 struct LunaPusher::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 ;-)