Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Context: https://devblogs.microsoft.com/oldnewthing/20210830-00/?p=105617 Context: dotnet/java-interop#795 It occurs to @jonpryor that what we're dealing with is *kinda* like the world of COM and COM-related technologies; see [oldnewthing][0]. > C++/WinRT and C++/CX set up parallel universes like this: > > | C++/WinRT | ABI | C++/CX | > | ----------------------- | ------------------------- | ----------------- | > | winrt::Contoso::Widget | ABI::Contoso::Widget\* | Contoso::Widget^ | We have an "ABI": what JNI expects/requires. Then we have a "projection" ("binding") from the ABI, for consumption by end users. *Historically*, we "hand-waved" over the ABI; it was *implicit* to how things worked, but *not* something that customers interacted with directly. @jonpryor would like to change that, and this seems like a reasonable "two birds, one stone" opportunity. The support for generic types, as described in `readm.md`, is contingent upon a "layer of indirection": > - Java calls the static invoker > - The static invoker calls the instance invoker > - The instance invoker calls the desired method The "instance invoker" is the "layer of indirection". The original prototype "named" this "layer of indirection" `I[Java type name]Invoker`; e.g. given Java: // Java package example; public interface ArrayList<E> { void add(E obj); } we'd have "binding + infrastructure" of: // C# namespace Example { public partial interface IArrayList<E> { void Add(E obj); } public partial interface IArrayListInvoker { void InvokeAdd(Java.Lang.Object obj); } } My proposal is to take some ideas from dotnet/java-interop#795 and WinRT projections, "formalizing" the ABI: // Convention: `jniabi.` + *Java* package name namespace jniabi.example { // Convention: Identical to Java type name public static partial class ArrayList { // Convention: Java method name + "suffix" for overloads; uses "lowest level" types. public static void add(IJavaPeerable self, JniObjectReference obj); } // Convention: `_` prefix + Java name suffix; [Register ("example/ArrayList", DoNotGenerateAcw=true)] public partial interface _ArrayList : IJavaObject, IJavaPeerable { // Convention: Java method name + "suffix" for overloads; uses "lowest level" types. [Register ("add", "(Ljava/lang/Object;)V", …); void add(JniObjectReference obj); // jni marshal method glue… private static bool n_Add_Ljava_lang_Object_ (IntPtr jnienv, IntPtr native__this, IntPtr native_p0) { var __this = Java.Lang.Object.GetObject<_ArrayList>(native__this); __this!.add (new JniObjectReference (native_p0)); } } } The "un-mangled" `jniabi.example.ArrayList` type is for "manual binding" scenarios; see also xamarin-android/src/Mono.Android, which has a fair bit of "hand-written" code which was produced by taking `generator` output and copying it. `jniabi.example.ArrayList` is meant to remove the need for hand-copying code, but *only* provides low-level bindings. Actual type marshaling/etc. is "elsewhere". The "mangled" `jniabi.example._ArrayList` type is the layer of indirection, so that generic types *can* work. It's not "meant" for implementation by "normal" people, but *can* be implemented if they want/need to take control of marshaling. (For example, Photo data marshaling is like a 12MB `byte[]` that needs to be marshaled EVERY TIME, contributing to perf issues. Being able to interject and do "something else" could be *really* useful for some scenarios.) Note: wrt dotnet/java-interop#795, `_ArrayList` was instead written as `jnimarshalmethod.example.ArrayList`. I tried this initially, but wasn't "thrilled" with how it looked. Please discuss. We then separate out the "projection" ("binding"), in terms of the ABI: namespace Example { // Note: *NO* [Register]; always implements ABI public partial interface IArrayList<E> : IJavaObject, IJavaPeerable, global::jniabi.example._ArrayList { void Add (E obj); // projection // "glue" from ABI to projection; explicitly implemented void jniabi.example._ArrayList.add(JniObjectReference native_obj) { var obj = Java.Lang.Object.GetObject<T>(native_obj.Handle); Add (obj); } } } "Of course", we also want/need the raw types in the projection, if only for backward compatibility (we've historically *only* bound raw types), and this "slots in": namespace Example { // Note: *NO* [Register]; always implements ABI public partial interface IArrayList : IJavaObject, IJavaPeerable, global::jniabi.example._ArrayList { void Add (Java.Lang.Object? obj); // projection // "glue" from ABI to projection; explicitly implemented void jniabi.example._ArrayList.add(JniObjectReference native_obj) { var obj = Java.Lang.Object.GetObject<Java.Lang.Object>(native_obj.Handle); Add (obj); } } } The downside to having the raw type implement the ABI `_ArrayList` type is that it *also* incurs the indirection penalty. It *is* really flexible though! Pros to this approach vs. the "original prototype": * Reduces "namespace pollution" by moving the "indirection" types into a separate set of namespaces. I rather doubt that anyone will "accidentally" find the `jniabi.…` namespace. * Makes the ABI explicit, which in turn inserts flexibility. What if you don't *want* `java.util.Collection` to be marshaled as an `IEnumerable<T>`? With this approach, you can control it. * It's also really awesome looking to be within a generic class and use `Object.GetObject<T>()`, using the type parameter directly. * Use of Java-originated identifier names (hopefully?) reduces complexity. (I can hope, right?) Cons: * Use of Java-originated identifier names may complicate things. Sure, they're still accessible via the `generator` object model, but that doesn't mean everything is "fine". (If wishes were horses…) * "ABI" names *will* need to involve "hashes" for overloads, at some point, because all reference types "devolve" to `JniObjectReference`, so unless the number of parameters differs, method overloads that use "separate yet reference types" will collide, e.g. `java.lang.String.getBytes(String)` vs. `java.lang.String.getBytes(java.nio.charset.Charset)`. The `jniabi.java.lang.String` type will need to expose e.g. `getBytes_hash1()` and `getBytes_hash2()` methods, where the hash is e.g. the CRC for the JNI method signature. [0]: https://devblogs.microsoft.com/oldnewthing/20210830-00/?p=105617
- Loading branch information