qt5.cr icon indicating copy to clipboard operation
qt5.cr copied to clipboard

Marshalling polymorphic Crystal wrappers in QObject

Open HertzDevil opened this issue 3 years ago • 3 comments

Once a Crystal wrapper type reaches C++, it loses its Crystal type information because only @unwrap remains, which is the pointer to the original C++ instance; something must keep track of the Crystal instance in a C++ instance. QObject, the base class of most virtual classes, doesn't offer any void * instance variable, but it has dynamic properties. So my idea is:

  • Rename CrystalVariant to a different name like CrystalAny, since it can store any Crystal value, but isn't really a drop-in replacement for QVariant;
  • Wrap QVariant normally, removing the entire type configuration in config/types.yml;
  • Expose qvariant_to_crystal and crystal_to_qvariant to Crystal manually;
  • Somewhere during initialization of every instance derived from Qt::Object, convert the instance into a Qt::Variant storing a CrystalAny;
  • Store that variant into a dynamic property called _bindgen_value for example (since Crystal wrappers never call super in their #initialize methods, this will be stored exactly once), most likely through a code body hook;
  • Define Qt::Object#to_crystal(T.class) in src/qt5/object.cr, which reads the dynamic property and converts it back to a Crystal value of type T, and similarly for the non-throwing #to_crystal?. Suppose that MyWidget < Qt::Widget < Qt::Object. Then calling #to_crystal?(MyWidget) would be something like:
def to_crystal?(_t : MyWidget.class) : MyWidget?
  # `any` shall refer to the receiver itself (same `@unwrap` if valid)
  any = qvariant_to_crystal(self.property("_bindgen_value"))

  # `CrystalAny` itself does not store polymorphic values; attempt every class down the hierarchy
  # if given type argument is a union type, repeat the part for each alternative
  case any.type_id
  # early return
  when Nil.crystal_instance_type_id         then nil

  # the following part shall be macro-ized
  when MyWidget.crystal_instance_type_id    then any.to(MyWidget)
  when Qt::Widget.crystal_instance_type_id  then any.to(Qt::Widget).as?(MyWidget)
  when Qt::Object.crystal_instance_type_id  then any.to(Qt::Object).as?(MyWidget)
  when ::Reference.crystal_instance_type_id then any.to(::Reference).as?(MyWidget)
  when ::Object.crystal_instance_type_id    then any.to(::Object).as?(MyWidget)

  # nothing matches
  else nil
  end
end

#to_crystal works even for descendents of abstract wrapper types. If MyGraphicsItem, Qt::GraphicsItemImpl < Qt::GraphicsItem:

scene : Qt::GraphicsScene
scene.items.each do |item|
  # `item` is a `Qt::GraphicsItemImpl`
  # if `item` is in fact a `MyGraphicsItem`, the first `any.to` will succeed;
  # otherwise, next check will be against `Qt::GraphicsItem`, which can be
  # downcast to `MyGraphicsItem`, so no syntax error occurs
  if my_item = item.to_crystal?(MyGraphicsItem)
    # item.to_unsafe == my_item.to_unsafe
  end
end

This will be helpful whenever a Qt function returns a Qt object where subclassing is expected. It also makes Qt::Variant usable (to be fair I see no problems with Qt's original interface, but more importantly the type shouldn't give the impression that Crystal types are already integrated within the Qt meta-object system).

Any thoughts on this?

HertzDevil avatar Aug 15 '20 14:08 HertzDevil

I'm not sure if we should go with a QObject-only approach. I currently see two possibilities that'd work for everything:

  1. Sub-class every class that's wrapped and add a variable that'd carry the Crystal object pointer. libGC can handle cycles, so on that front we're set. On the downside, it'd make the generated code fat. Also, it wouldn't work for classes marked as final - Which isn't used in Qt afaik, but in general, I think it'll become more popular with time. I'm not familiar with the modern Crystal memory layout, so I presume we can't frankenstein a Crystal object and the C++ object into something C++ and Crystal can work at once with (This'd make self == @unwrap).
  2. Have a global thread-safe map to go from raw pointer to Crystal object. Easy and would work for everything, however it's hard to detect that an object was destroyed. libGC would allow some kind of hacks into this I gues to get notified when something is removed, however we can't expect to be everything that's passed to Crystal to be libGC allocated. This would however also work for C-only libraries.

Papierkorb avatar Aug 15 '20 16:08 Papierkorb

SIP is to PyQt5 what Bindgen is to qt5.cr, and it seems they took the second approach: https://github.com/eduardosm/sip-4.19.7/blob/master/siplib/objmap.c

Their wrapper structs look rather fat too, despite Python being dynamically typed (or perhaps that is the reason). I imagine the #to_crystal code would remain largely the same, the major difference being where the CrystalAny is stored.

Regarding Boehm GC: defining #finalize should be the same as using GC_register_finalizer_ignore_self or passing a C++ finalizer function next to UseGC.

HertzDevil avatar Aug 15 '20 16:08 HertzDevil

Regarding Boehm GC: defining #finalize should be the same as using GC_register_finalizer_ignore_self or passing a C++ finalizer function next to UseGC.

Yup, that'd be fine. However, I'm not sure if this works even for structures that didn't go through Boehms new/malloc().

Papierkorb avatar Aug 16 '20 10:08 Papierkorb