I was planning to do a detailed write-up for what I ended up doing once it actually functioned end-to-end (as I need to do that for work internally ). But I’d be happy to give an ad-hoc version right now.
Java, as you note, does not support multiple inheritance in concrete (instantiatable) types. It does, however, allow it in a form for interfaces. I say in a form, because unlike C++, Java will collapse multiple instances of a multiply inherited interface. But this model is close enough, I believe (and has shown to be workable for all of the things I’ve looked at in PANDA thus far).
So what I’ve done is express the type taxonomy, and nesting, in Java interface types.
Next a form is needed that can be instantiated. Similar to how things were done in the early days of interrogate integration I declare concrete types that implement points in the interface taxonomy that are ‘shadow’ types for the C++ and contain only a private (technically, protected, but that’s an implementation detail) holder for a ‘pointer’ to the real C++ entity. Java interface types cannot declare the JNI ‘native’ methods needed to bridge to the C++, so those live in the ‘proxy’ types. The ‘proxy’ types implement the abstract methods declared in the interface type(s), and vector to the appropriate JNI ‘native’ methods.
The interface types also declare static methods which model the constructors, these vector to static methods on the ‘proxy’ types that perform and wrap a proper construction. The proxy types also interact with end-of-live control (during Java GC) and arrange to call the correct end-of-life (delete, or unref_delete) in the C++.
There is one more implementation detail that is (unfortunately) important for the Java space. It is simplest for the C++ side of the JNI if a given method can know what type the object-like parameters are passed in as. Particular in the case of base types. From the Java side, a given ‘proxy’ type contains a pointer for that type. Java knows that the proxy type implements the base interfaces, but not how to adjust this pointer, or how to re-wrap it in an appropriate ‘proxy’. Further, since we are only doing this cast for a function call, we would rather Java’s ref-counting not get overly involved in the lifetime of the pointer in that parameter instance.
So to ‘cheat’ this, I introduced an intermediate instantiatable form between the interface and the ‘proxy’. I call this a ‘param’ form. Ultimately all of the non-construct, non-destruct methods are implemented there. The ‘proxy’ form extends this with construct/destruct. When converting to a form for passing as a parameter, the proxy presents a re-wrapped param form. This fixes all of the otherwise painful lifetime stuff. Though this shim could be thinner for PANDA types that also ref-counted, I wanted a single working model to get things working. Optimization can happen after.
Neither the ‘proxy’ or ‘param’ forms should ever be referenced in user code. Only the interfaces. All the rest is implementation detail.