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:
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.
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:To that end I suggest wrapping
dynamic_cast
for every possible downcast from one wrapped polymorphic type to another. For example, the cast fromA
toB
would look like: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 inT2
andT3
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: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, usingstatic_cast
instead ofdynamic_cast
.