Would it be better to invert this approach?
I'm hardly an expert on the C++ ABI, but if I recall correctly, in the absence of virtual base classes, each base class is laid out in the derived class identically to a data member of the same type. (There might be an edge case around empty classes with no vtable, but this seems like something you could work around.) If so, then perhaps we should import base classes as stored properties of the derived class, and then mirror their members onto the derived class so they can be called as though they were inherited:
// C++
struct A {
void test1();
};
struct B : A {
void test2();
};
A *useAndReturnItsPointer(A &a);
// Imported as
struct A {
func test1()
}
struct B {
// Stored properties and real members:
var asA: A
func test2()
// Mirrored from asA:
func test1()
}
func useAndReturnItsPointer(_: inout A) -> UnsafeMutablePointer<A>
// Ordinary use:
var b = B()
b.test2() // As normal
b.test1() // Call emitted as though you wrote `b.asA.test1()`
In this model, you would "upcast" by simply accessing asA, which seems convenient enough, and I think you could downcast with a helper function:
// Upcasting via property
var B = B()
useAAndReturnItsPointer(&b.asA)
// Downcasting via helper function
withUnsafeCxxDowncast(of: useAAndReturnItsPointer(b.asA), via: \B.asA) { recoveredB in
recoveredB.test2()
}
// Implementation in the standard library
@available(macOS 9999, *)
public func withUnsafeCxxDowncast<Base, Derived, Result>(of baseThis: UnsafeMutablePointer<Base>, via upcastProperty: WritableKeyPath<Derived, Base>, _ body: (inout Derived) throws -> Result) rethrows -> Result {
// We'd have to update `MemoryLayout.offset(of:)` to support applying `this`
// offsets from C++ vtables.
let derivedThisRaw = UnsafeRawMutablePointer(baseThis) - MemoryLayout<Derived>.offset(of: upcastProperty)!
// `assumingMemoryBound(to:)` is correct here because we are assuming
// that `baseThis` is an instance of `Base` inside `Derived`, i.e., the surrounding
// memory is already bound to `Derived`.
return try body(&derivedThisRaw.assumingMemoryBound(to: Derived.self).pointee)
}
I think this could be made to work reasonably even with multiple inheritance, and even in fairly tricky cases:
// C++
struct GrandparentA { void gpA(); };
struct GrandparentB { void gpB(); };
struct GrandparentC { void gpC(); };
struct ParentA: public GrandparentA, public GrandparentB, public GrandparentC { void pA(); };
struct ParentB: public GrandparentB, public GrandparentC { void pB(); };
struct A: public ParentA, public ParentB, public GrandparentC { void a(); };
// Imported as:
struct GrandparentA {
func gpA()
}
struct GrandparentB {
func gpB()
}
struct GrandparentC {
func gpC()
}
struct ParentA {
// Stored properties and real members:
var asGrandparentA: GrandparentA
var asGrandparentB: GrandparentB
var asGrandparentC: GrandparentC
func pA()
// Mirrored from asGrandparentA:
func gpA()
// Mirrored from asGrandparentB:
func gpB()
// Mirrored from asGrandparentC:
func gpC()
}
struct ParentB {
// Stored properties and real members:
var asGrandparentB: GrandparentB
var asGrandparentC: GrandparentC
func pB()
// Mirrored from asGrandparentB:
func gpB()
// Mirrored from asGrandparentC:
func gpC()
}
struct A {
// Stored properties and real members:
var asParentA: ParentA
var asParentB: ParentB
var asGrandparentC: GrandparentC
func a()
// Mirrored from asParentA:
var asGrandparentA: GrandparentA
func pA()
func gpA()
// Mirrored from asParentB:
func pB()
// *Not* mirrored because both ParentA and ParentB inherit from GrandparentB:
@available(*, unavailable, message: "use 'asParentA.asGrandparentB' or 'asParentB.asGrandparentB' instead")
var asGrandparentB: GrandparentB
@available(*, unavailable, message: "use 'asParentA.gpB()' or 'asParentB.gpB()' instead")
func gpB()
// Mirrored from asGrandparentC (even though it's also visible via
// asParentA and asParentB, the more direct inheritance "wins"):
func gpC()
}
However, virtual bases seem harder since an instance of e.g. ParentA embedded in A has a different size and layout from an instance of ParentA standing alone. Maybe a sufficiently clever copying operation could make it work, though—I don't know what you have planned for that.