Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support downcasting between Crystal wrapper types #87

Open
HertzDevil opened this issue Sep 5, 2020 · 0 comments
Open

Support downcasting between Crystal wrapper types #87

HertzDevil opened this issue Sep 5, 2020 · 0 comments

Comments

@HertzDevil
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant