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

Separate out ABI from method bindings #795

Open
jonpryor opened this issue Feb 8, 2021 · 2 comments
Open

Separate out ABI from method bindings #795

jonpryor opened this issue Feb 8, 2021 · 2 comments
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)

Comments

@jonpryor
Copy link
Member

jonpryor commented Feb 8, 2021

Context: https://github.com/xamarin/xamarin-android/blob/6c6766de4db50fb43c42fccf442387a95f39086c/src/Mono.Android/Android.Widget/AbsListView.cs#L55-L64
Context: dotnet/android-libraries@5ce8b5f

Background:

Sometimes it is necessary to use partial classes to "coerce" generator output to be valid C# code. A not-entirely-uncommon use case around method overrides (related: Issue #681, fixed in 5a0e37e).

Consider dotnet/android-libraries@5ce8b5f: in it, NotificationCompat.Action.getRemoteInputs() was supposed to override a base class method, but -- for whatever reason -- generator couldn't properly determine that override was needed. As this was before 5a0e37e, the "workaround" was to rename the managedName of the Java method to _GetRemoteInputs(), then add a partial class declaration which could have the proper override logic.

While this works, it isn't obvious (hence #681). Additionally, such a solution should require two Metadata fixups: one to rename the method, and another to change the visibility of the method so that it isn't public:

	<attr path="/api/package[@name='android.support.v4.app']/class[@name='NotificationCompat.Action']/method[@name='getRemoteInputs' and count(parameter)=0]" name="managedName">_GetRemoteInputs</attr>
	<attr path="/api/package[@name='android.support.v4.app']/class[@name='NotificationCompat.Action']/method[@name='getRemoteInputs' and count(parameter)=0]" name="visibility">internal</attr>

(This wasn't done in the dotnet/android-libraries@5ce8b5f, and I don't understand why NotificationCompat.Action._GetRemoteInputs() isn't public…)

Another example is within Mono.Android.dll (and elsewhere), where it is too common to have generator emit output, copy that output into a new file, fixup that output, and then use Metadata to remove the original "bad" output. See e.g.

There should be no need for such hand-written JNI code, yet we have lots of it.

Proposal

I propose that we "refactor and cleanup" our bindings to separate out the code responsible for JNI invocations from the code responsible for "C# bindings".

Consider Android.App.Activity, where JNI code is interspersed with the C# API:

namespace Android.App {
	[global::Android.Runtime.Register ("android/app/Activity", DoNotGenerateAcw=true)]
	public partial class Activity {

	internal static readonly JniPeerMembers _members = new XAPeerMembers ("android/app/Activity", typeof (Activity));

		static Delegate? cb_createPendingResult_ILandroid_content_Intent_I;
#pragma warning disable 0169
		static Delegate GetCreatePendingResult_ILandroid_content_Intent_IHandler ()
		{
			if (cb_createPendingResult_ILandroid_content_Intent_I == null)
				cb_createPendingResult_ILandroid_content_Intent_I = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPILI_L) n_CreatePendingResult_ILandroid_content_Intent_I);
			return cb_createPendingResult_ILandroid_content_Intent_I;
		}

		static IntPtr n_CreatePendingResult_ILandroid_content_Intent_I (IntPtr jnienv, IntPtr native__this, int requestCode, IntPtr native_data, int native_flags)
		{
			var __this = global::Java.Lang.Object.GetObject<Android.App.Activity> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
			var data = global::Java.Lang.Object.GetObject<Android.Content.Intent> (native_data, JniHandleOwnership.DoNotTransfer);
			var flags = (Android.App.PendingIntentFlags) native_flags;
			IntPtr __ret = JNIEnv.ToLocalJniHandle (__this.CreatePendingResult (requestCode, data!, flags!));
			return __ret;
		}
#pragma warning restore 0169

		// Metadata.xml XPath method reference: path="/api/package[@name='android.app']/class[@name='Activity']/method[@name='createPendingResult' and count(parameter)=3 and parameter[1][@type='int'] and parameter[2][@type='android.content.Intent'] and parameter[3][@type='int']]"
		[Register ("createPendingResult", "(ILandroid/content/Intent;I)Landroid/app/PendingIntent;", "GetCreatePendingResult_ILandroid_content_Intent_IHandler")]
		public virtual unsafe Android.App.PendingIntent? CreatePendingResult (int requestCode, Android.Content.Intent data, [global::Android.Runtime.GeneratedEnum] Android.App.PendingIntentFlags flags)
		{
			const string __id = "createPendingResult.(ILandroid/content/Intent;I)Landroid/app/PendingIntent;";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [3];
				__args [0] = new JniArgumentValue (requestCode);
				__args [1] = new JniArgumentValue ((data == null) ? IntPtr.Zero : ((global::Java.Lang.Object) data).Handle);
				__args [2] = new JniArgumentValue ((int) flags);
				var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, __args);
				return global::Java.Lang.Object.GetObject<Android.App.PendingIntent> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
			} finally {
				global::System.GC.KeepAlive (data);
			}
		}
	}
}

I propose three changes:

  1. Emit the "marshal methods" into a separate jnimarshalmethods.[Java-package-name].[Java-type] class.
  2. Emit the "JNI" into a separate jniabi.[Java-package-name].[Java-type] class.
  3. Implement C# "API descriptions" in terms of (1) and (2).

For example:

namespace Android.App {
	[global::Android.Runtime.Register ("android/app/Activity", DoNotGenerateAcw=true)]
	public partial class Activity {

		public override JniPeerMembers JniPeerMembers => jniabi.android.app.Activity._members;

		[Register ("createPendingResult", "(ILandroid/content/Intent;I)Landroid/app/PendingIntent;", "GetCreatePendingResult_ILandroid_content_Intent_IHandler")]
		public virtual unsafe Android.App.PendingIntent? CreatePendingResult (int requestCode, Android.Content.Intent data, [global::Android.Runtime.GeneratedEnum] Android.App.PendingIntentFlags flags)
		{
			var __rm = jniabi.android.app.Activity.createPendingResult (this, requestCode, data, (int) flags);
			return global::Java.Lang.Object.GetObject<Android.App.PendingIntent> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
		}

		// Ideally we wouldn't need this; would require updating `[Register]` to use the "interface-style" registration pattern w/ separate type.
		// May actually be *desirable* to "better bind" Java generics; see also:
		// https://discord.com/channels/732297728826277939/732297837953679412/906288298853556224
		static Delegate GetCreatePendingResult_ILandroid_content_Intent_IHandler () =>
			jnimarshalmethods.android.app.Activity.GetOnCreate_Landroid_os_Bundle_Handler ();
	}
}

namespace jniabi.android.app {
	partial static class Activity {
		internal static readonly JniPeerMembers _members = new XAPeerMembers ("android/app/Activity", typeof (Activity));

		public static JniObjectReference createPendingResult (IJavaPeerable self, int requestCode, IJavaPeerable data, int flags)
		{
			try {
				return createPendingResult (self, requestCode, data?.PeerReference ?? new JniPeerReference (), flags);
			}
			finally {
				GC.KeepAlive (self);
				GC.KeepAlive (data);
			}
		}

		public static JniObjectReference createPendingResult (IJavaPeerable self, int requestCode, JniObjectReference data, int flags)
		{
			JniArgumentValue* __args = stackalloc JniArgumentValue [3];
			__args [0] = new JniArgumentValue (requestCode);
			__args [1] = new JniArgumentValue ((data == null) ? IntPtr.Zero : ((global::Java.Lang.Object) data).Handle);
			__args [2] = new JniArgumentValue ((int) flags);
			var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, __args);
		}

	}
}

namespace jnimarshalmethods.android.app {

	partial static class Activity {

		static Delegate? cb_createPendingResult_ILandroid_content_Intent_I;
#pragma warning disable 0169
		static Delegate GetCreatePendingResult_ILandroid_content_Intent_IHandler ()
		{
			if (cb_createPendingResult_ILandroid_content_Intent_I == null)
				cb_createPendingResult_ILandroid_content_Intent_I = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPILI_L) n_CreatePendingResult_ILandroid_content_Intent_I);
			return cb_createPendingResult_ILandroid_content_Intent_I;
		}

		static IntPtr n_CreatePendingResult_ILandroid_content_Intent_I (IntPtr jnienv, IntPtr native__this, int requestCode, IntPtr native_data, int native_flags)
		{
			var __this = global::Java.Lang.Object.GetObject<Android.App.Activity> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
			var data = global::Java.Lang.Object.GetObject<Android.Content.Intent> (native_data, JniHandleOwnership.DoNotTransfer);
			var flags = (Android.App.PendingIntentFlags) native_flags;
			IntPtr __ret = JNIEnv.ToLocalJniHandle (__this.CreatePendingResult (requestCode, data!, flags!));
			return __ret;
		}
#pragma warning restore 0169
	}
}

Key to the idea here is that jniabi.… will contain all methods in the api.xml for the declaring type. That way a partial class can always access the "ABI" methods in partial classes/etc.

This may result in larger binding assemblies (all declared methods get "JNI glue code", not just those appearing within the bindings). This assumes that the linker will be able to remove such unused methods, which it should be able to.


Related design points on the jniabi type:

  • It should ideally be identical to a future "JavaInterop1" ABI, so no use of Android-specific types like IJavaObject.
@jonpryor jonpryor added enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.) labels Feb 8, 2021
@jonpryor jonpryor added this to the Under consideration milestone Feb 8, 2021
@AmrAlSayed0
Copy link

This would be even better
Activity.cs

public partial class Activity {

	// Metadata.xml XPath method reference: path="/api/package[@name='android.app']/class[@name='Activity']/method[@name='onCreate' and count(parameter)=1 and parameter[1][@type='android.os.Bundle']]"
	partial protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState);

}

Activity._Binding.cs

[global::Android.Runtime.Register ("android/app/Activity", DoNotGenerateAcw=true)]
public partial class Activity {

	static Delegate GetOnCreate_Landroid_os_Bundle_Handler () => _JniJavaCaller.GetOnCreate_Landroid_os_Bundle_Handler ();

	// Metadata.xml XPath method reference: path="/api/package[@name='android.app']/class[@name='Activity']/method[@name='onCreate' and count(parameter)=1 and parameter[1][@type='android.os.Bundle']]"
	[Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
	partial protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState)
	{
		try {
			_JniMethods.onCreate (this, savedInstanceState?.Handle ?? IntPtr.Zero);
		} finally {
			global::System.GC.KeepAlive (savedInstanceState);
		}
	}

	internal static class _JniJavaCaller {
		static Delegate? cb_onCreate_Landroid_os_Bundle_;
#pragma warning disable 0169
		public static Delegate GetOnCreate_Landroid_os_Bundle_Handler ()
		{
			if (cb_onCreate_Landroid_os_Bundle_ == null)
				cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_);
			return cb_onCreate_Landroid_os_Bundle_;
		}

		static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
		{
			var __this = global::Java.Lang.Object.GetObject<Android.App.Activity> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
			var savedInstanceState = global::Java.Lang.Object.GetObject<Android.OS.Bundle> (native_savedInstanceState, JniHandleOwnership.DoNotTransfer);
			__this.OnCreate (savedInstanceState);
		}
#pragma warning restore 0169
	}

	internal static class _JniMethods {
		internal static readonly JniPeerMembers _members = new XAPeerMembers ("android/app/Activity", typeof (Activity));

		public static void onCreate (IJavaPeerable self, IntPtr savedInstanceState)
		{
			const string __id = "onCreate.(Landroid/os/Bundle;)V";
			JniArgumentValue* __args = stackalloc JniArgumentValue [1];
			__args [0] = new JniArgumentValue (savedInstanceState);
			_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
		}
	}
}

This would help external developers with fixing the bindings in the Metadata file. Most of the time developers don't care about the implementation, they just care about the interface. If the interface is separated out in it's own file, finding the issue by just scrolling will be easier, especially issue that aren't build errors (e.g JLO instead of generic constraint return).

But maybe this could be a separate issue.

@jpobst
Copy link
Contributor

jpobst commented May 5, 2021

One issue with this proposal is that our "internal" binding infrastructure is a kind of "public" API due to our use of partial types. Making changes to it can break people's Additions code.

For example, I tried to make the trivial change to turn this generated code:

int foo;
public int Foo { get { return foo; } }

Into an auto-property:

public int Foo { get; }

However this broke Additions code that was accessing the field foo. 😢

While I would love all of our generated code to be cleaner, I don't know if we can do much without creating a new $(AndroidCodegenTarget) and maintaining the new and old one ~forever. Unless there's another way to handle this, I don't think it's worth making this change.

jonpryor added a commit to jonpryor/GenericBindingPrototype that referenced this issue Mar 10, 2022
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
jonpryor added a commit to jonpryor/GenericBindingPrototype that referenced this issue Mar 14, 2022
Context: https://devblogs.microsoft.com/oldnewthing/20210830-00/?p=105617
Context: dotnet/java-interop#795
Context: dotnet/java-interop#910

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: `Jniabi` prefix + Java type name
	    [EditorBrowsable (EditorBrowsableState.Never)]
	    public static partial class JniabiArrayList {
	        // Convention: Java method name + "suffix" for overloads; uses "lowest level" types.
	        public static void add(IJavaPeerable self, JniObjectReference obj);
	    }

	    // Convention: `_Jniabi` prefix + Java name suffix;
	    [EditorBrowsable (EditorBrowsableState.Never)]
	    [Register ("example/ArrayList", DoNotGenerateAcw=true)]
	    public partial interface _JniabiArrayList : 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));
	        }
	    }
	}

[`[EditorBrowsable(EditorBrowsableState.Never)]`][1] is used to hide
these types from normal IDE code completion scenarios.  They're
intended for internal use.

The `jniabi.example.JniabiArrayList` static class 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.JniabiArrayList`
is meant to remove the need for hand-copying code, but *only* provides
low-level bindings.  Actual type marshaling/etc. is "elsewhere".
It is also useful as part of dotnet/java-interop#910, to allow
"interface Invokers" to begin caching `jmethodID` values.

The `jniabi.example._JniabiArrayList` interface 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, `_JniabiArrayList` 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._JniabiArrayList.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._JniabiArrayList.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
`_JniabiArrayList` 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
[1]: https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.editorbrowsableattribute?view=net-6.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)
Projects
None yet
Development

No branches or pull requests

3 participants