Why Zig + Qt Feels Like Doing the Impossible Right
34 points by rcalixte
34 points by rcalixte
In the interests of full disclosure, I did not participate in the creation of this post. The author spent his own time and used his own words without any external influence.
What about instead of calling it New and New2 (and I saw a New3) you name it NewWithParent or NewWithParams(struct new_params *params) and then you pass a struct inited with designated initializes (not sure if this is possible in Zig as I'm more familiar with C). I suppose another possibility is varargs like Motif had, but I know that is not type safe...
If you go through the history, there were previous attempts at using parameter names to expand method names. It turns out that it was more intuitive to onboard when there was a consistent approach to navigating the overloaded methods, especially because there are upstream methods containing "With" in the name. LSPs and the like are also better aids when there is a sort of natural order to the methods. There are also many types where the named overloads were still insufficient, exacerbating the inconsistency. QBrush is a good example where this happens a few times, not only in the constructors, but also the class methods. There are methods containing numbers in them that are overloaded too so there's always going to be some level of awareness required.
When it comes to the actual parameter format, it's important to remember that the front-end of the library is predominantly generated. That means that there needs to be a scalable and programmatic approach to binding and trade-offs will come with that. As of writing this, the total library has 4356 constructor methods and 3242 of them take parameters. For a smaller surface area, a more curated hand-binding approach could make sense. I believe that the best approach going forward is to try to project as many types as possible as native types, both in C and in Zig. A const char* or []const u8 parameter is ultimately a better developer experience while leaning into somewhat better type safety as well. While I haven't fully dismissed the use of varargs, I've tried to keep things as simple and maintainable as possible while also trying to keep the library implementation readable. At some point, I want to take a deeper pass with attempting to bind variadic parameters, functors, and a few other things that I think might be of value. Time will tell on whether those efforts yield value.
When it comes to type safety, ultimately, it's bindings to an OOP language and framework/ecosystem that leans heavily on OOP principles - from languages that do not support OOP principles natively. That means there will always be some notion of raw pointers that are unavoidable. As the examples demonstrate, with proper handling, it's not too much of an issue. Doubling down on documentation hopefully aids this.
I guess the anyopaque pointers is working around C++ class inheritance? Do you have something to prevent a future Qt version from causing your generator to swap, for example, qbrush.New7 and qbrush.New8, and exploding user code?
Not just inheritance but also encapsulation. The latter is much more difficult to express without the use of raw pointers in generated bindings at this scale. When it comes to ordering the constructors (and overloads in general), there is backend code that handles the upstream header parsing so that it will be as consistent as it is presented upstream. If you look at the rebase from Qt 6.4 to Qt 6.8 for C or Zig, many class constructors were fine but QVariant saw a larger breaking change due to upstream changes. Those actually still need to be addressed and will represent another breaking change when complete. While methods shouldn't "swap" per se, all of the breaking changes are called out in the CHANGELOG and with the examples currently numbering near 50 or so, there are often enough cases where the examples serve as additional documentation for breaking changes should they occur with a wide enough impact. It all boils down to how constructor modifications are made upstream. Fortunately, the upstream API is mature and stable enough that it has not been too problematic yet.
One of the core parts of Qt is using the MOC to create custom QObject subclasses with their own slots/signals/properties. I can't find information about that in this library, but surely zig comptime would be able to create very nice meta object definitions without a separate MOC? That's what would get me interested.
In theory, that could be implemented in a similar fashion to Verdigris but this would be a pretty significant and complex undertaking and probably best off as a separate auxiliary library if desired. Qt's MOC is tightly coupled with C++ macros, further complicating what could be a pure Zig implementation: not just comptime, but also the build system integration would have to be implemented in order to provide this functionality. If library support for functors were to materialize, it would hopefully offset much of the need for this.
Given the existing access to signals, slots, and runtime reflection methods, what do you feel you would be missing for your use case? Keep in mind, there are already custom tools analogous to what Qt provides for use with Qt Creator/Designer.