Closed HertzDevil closed 4 years ago
If the solution is to simply expose the superclass methods, this is what I came up with:
extern "C" int bg_Base_SUPER_calc_int_int(Base * _self_, int a, int b) {
return _self_->Base::calc(a, b);
}
extern "C" int bg_Base_calc_int_int(Base * _self_, int a, int b) { // keep
return _self_->calc(a, b);
}
module Test
lib Binding
fun bg_Base_SUPER_calc_int_int(_self_ : Base*, a : Int32, b : Int32) : Int32
fun bg_Base_calc_int_int(_self_ : Base*, a : Int32, b : Int32) : Int32 # keep
end
class Base
struct Superclass
def initialize(@myself : Base)
end
def calc(a : Int32, b : Int32) : Int32
Binding.bg_Base_SUPER_calc_int_int(@myself, a, b)
end
end
def superclass
Superclass.new(self)
end
def superclass(_type : Base.class)
Superclass.new(self)
end
def calc(a : Int32, b : Int32) : Int32 # keep
Binding.bg_Base_calc_int_int(self, a, b)
end
end
end
class OverrideThing < Test::Base
def calc(a, b)
superclass.calc(a, b) * (a - b)
# following also works in Test::Subclass
# superclass(Test::Base).calc(a, b) * (a - b)
end
end
That is, for every non-pure abstract class, on the Crystal
platform,
Superclass
, which is simply a wrapper of the calling instance;superclass
, which wraps the receiver instance (usually the caller itself);For every non-pure abstract method,
Cpp
platform, generate a clone that contains SUPER
in the function name and always invokes the method from the class itself, preventing dynamic lookup or invoking the CrystalProc
from the jump table;Binding
platform, generate a fun
clone that contains SUPER
;Crystal
platform, clone the method inside the Superclass
struct, using the SUPER
fun
instead of the original one.Then in the actual overriding method, rather than using super
, the superclass
method will provide direct access to those methods. It looks reasonably close to super
, supports overloaded methods, and should work with multiple inheritance on both C++ and Crystal. super
doesn't break inside initialize
, by the way.
The main drawback is that generating all this extra boilerplate for every single abstract method might be overkill in very large codebases like qt5.cr, so classes might have to opt in for this feature using the YAML config files.
Hello,
Oh dear, the virtual-override stuff was fun (for some good and some wrong) reasons to write back then :stuck_out_tongue:
Your solution sounds good to me. I'm wondering however if #superclass
should be marked as protected
as measure to avoid abuse. superclass
is also a lot like Javas super
giving out a "wrapper object" much like in your proposal.
Also, I see where you're coming from in terms of code bloat. I'd be interested in simply measuring how bad it is on Qt5, as I guess that it'll be the largest library we have for quite a while.
All in all your proposal is sound to me :+1: Cheers!
Got it to work in 5f9e545. On my local qt5.cr installation, this generated 82 Superclass
classes, 1999 SUPER
lib funs, and 971 C++ SUPER
wrappers when everything was enabled. The file size changes are as follows:
src/binding/binding_$(BINDING_PLATFORM).cr
: 3.2 MB -> 3.7 MB (+16.6%)ext/qt_binding_$(BINDING_PLATFORM).cpp
: 1.8 MB -> 1.9 MB (+8.8%)ext/qt_binding_$(BINDING_PLATFORM).o
: 5.8 MB -> 6.0 MB (+3.3%)ext/binding_$(BINDING_PLATFORM).a
: 6.4 MB -> 6.6 MB (+3.7%)Every class that derives from Qt::Object
must include #meta_object
, #qt_metacast
, #qt_metacall
, #event
, #event_filter
, #timer_event
, #child_event
, #custom_event
, #connect_notify
, and #disconnect_notify
. The first 3 are internals for the Q_OBJECT
macro and are presumably generated by moc
, so users are not supposed to override them manually, and it wouldn't hurt if wrappers for these methods are disabled.
The virtual classes that don't derive from Qt::Object
are PaintDevice
, LayoutItem
, SpacerItem
, WidgetItem
, GraphicsItem
and subclasses, GraphicsItemGroup
, and Surface
(which has no concrete abstract methods).
I don't think the impact is so large that anything needs to be disabled, but if that's the case, my current plan is that types should be able to opt out of the superclass wrapper scheme by specifying the following in the YAML config files:
types:
QObject:
generate_superclass: false # default true
QWidget:
superclass_ignore_methods: [ "metaObject", "qt_metacast", "qt_metacall" ] # default [ ]
Then the Qt
processor, which runs before VirtualOverride
, automatically adds those 3 methods to the superclass_ignore_methods
field of every class if necessary.
As mentioned in the PR, those Superclass
es are currently still classes instead of structs. Also, the #superclass
overloads that take a type argument are not added, because upon further thought this is now possible:
class MyWidget < Qt::Widget
def event(_evt : Qt::Event) : Bool
true # recognize every event, but do nothing
end
private def old_event(evt)
superclass.event(evt)
end
end
class SubWidget < MyWidget
def event(evt : Qt::Event) : Bool
old_event(evt) # calls Qt::Widget's version despite MyWidget overriding #event
end
end
Really good progress you have in #70, 👍 to that! My notes below:
On my local qt5.cr installation, this generated 82 Superclass classes, 1999 SUPER lib funs, and 971 C++ SUPER wrappers when everything was enabled. The file size changes are as follows:
src/binding/binding_$(BINDING_PLATFORM).cr: 3.2 MB -> 3.7 MB (+16.6%) ext/qtbinding$(BINDING_PLATFORM).cpp: 1.8 MB -> 1.9 MB (+8.8%) ext/qtbinding$(BINDINGPLATFORM).o: 5.8 MB -> 6.0 MB (+3.3%) ext/binding$(BINDING_PLATFORM).a: 6.4 MB -> 6.6 MB (+3.7%)
I think that's fine. The Crystal compiler should discard unused Crystal methods on its side, and the linker discard unused symbols from C++ land. So my only concern would be compile time implosion, is it much worse for you? Considering the opt-out possibility there's more to win than to lose I think.
types: QObject: generate_superclass: false # default true QWidget: superclass_ignore_methods: [ "metaObject", "qt_metacast", "qt_metacall" ] # default [ ]
These fields should be regexp's. They're not much more expensive but are easier to deal with in anticipation of future changes of a library unknown to us yet.
As mentioned in the PR, those Superclasses are currently still classes instead of structs. Also, the #superclass overloads that take a type argument are not added [...]
I see this as optional additional feature for now. It's required at some point to wrap stuff like this:
struct A { virtual void foo(); };
struct B { virtual void foo(); };
struct C : public A, virtual B { };
iirc, in terms of Qt, there was a single class making use of this, which is so rarely used I forgot what it's called. So, for Qt, it's not that interesting. No idea if some popular C++ lib out there does.
All in all this looks really good, thank you for working on this! 👍
I was doing work that required an overriding method to call the overridden method in the superclass (
Qt::Object::eventFilter
from qt5.cr to be exact), but to no avail. Suppose this class is inspec/integration/virtual_override_spec.cr
:This leads to a stack overflow:
The bodies of the relevant generated methods are:
If the method is simply overridden, calls should stop at (1) immediately; if an override isn't provided, the jump table entry for
calc
would be invalid, so C++'sBase::calc
would be invoked after (4). It looks likeBgInherit_Base::calc
always invokes the most derivedcalc
method, so it cannot distinguish betweensuper
and a regular call.