Papierkorb / bindgen

Binding and wrapper generator for C/C++ libraries
GNU General Public License v3.0
179 stars 18 forks source link

Support downcasting between Crystal wrapper types #87

Open HertzDevil opened 4 years ago

HertzDevil commented 4 years ago

Previously we discussed the possibility of recovering Crystal wrapper instances/types from their @unwrap pointers, but it turns out that only solves part of the marshalling problem; if there wasn't a wrapper instance in the first place, we are left with a C pointer in Crystal with no access to C++'s RTTI. This issue attempts to address this other part. Consider:

struct A {
  virtual ~A() { }
  A *create();
};

struct B : A { };
struct C : A { };

A *A::create() {
  return new B;
}
module Test
  class A
    def create : A
      A.new(unwrap: Binding.bg_A_create_(self))
    end
  end

  class B < A end
  class C < A end
end

x = Test::A.new.create
x.as?(Test::B) # returns `nil`, runtime type of `x` is `Test::A` (or even `Test::AImpl`)
x.unsafe_as(Test::C) # bad idea

To that end I suggest wrapping dynamic_cast for every possible downcast from one wrapped polymorphic type to another. For example, the cast from A to B would look like:

extern "C" B * bg_A__CAST_B_(A * _self_) {
  return dynamic_cast<B *>(_self_);
}
module Test
  lib Binding
    alias A = Void
    alias B = Void
    fun bg_A__CAST_B_(_self_ : A*) : B*
  end

  class A
    def to?(_type_ : B.class) : B?
      ptr = Binding.bg_A__CAST_B_(self)
      B.new(unwrap: ptr) unless ptr.null?
    end
  end

  class B < A
    # cast is not needed from here, because `Test::B` and
    # its subclasses always wrap a `B` instance from C++
    # the return type is also no longer nilable
    def to?(_type_ : B.class) : B
      self
    end
  end
end

x.to?(Test::B) # => #<Test::B:...>
x.to?(Test::C) # => nil

Taking inspiration from block overloads, these cast methods take the target wrapper class itself as an argument. The C++ wrappers always perform casts using pointers; bad casts can be reported on the Crystal side with #not_nil!.

For a linear hierarchy of n classes (Tn < Tn-1 < ... < T2 < T1), a naive approach would generate a total of O(n²) #to? methods. This can be reduced to O(n) by noting that pointers to base types are also pointers to derived types, so that e.g. T1#to?(T4.class) can be reused in T2 and T3 and therefore does not have to be redefined in those subclasses. (The case with multiple inheritance will be more complicated.)

No special handling is needed for abstract wrappers; the Impl classes will pick up the #to? methods from the abstract classes they inherit from. Code like this will finally be possible:

def all_groups(scene : Qt::GraphicsScene)
  scene.items.compact_map &.to?(Qt::GraphicsItemGroup)
end

As part of the changes I'd suggest that the existing upcast methods for classes with multiple bases (the #as_X methods generated by the Inheritance processor) also take the #to? form. Note that they already share a similar C++ body, using static_cast instead of dynamic_cast.