Skip to content

Optimize Interface Invokers #910

Closed
dotnet/android
#8339
@jonpryor

Description

@jonpryor

Context: http://github.com/xamarin/monodroid/commit/c0ad930298013425e4797f89b0c9243c22827152
Context: #858

From xamarin/monodroid@c0ad930298013425e4797f89b0c9243c22827152 (2011-Nov-28):

Remember xamarin/monodroid@62a4766, and how the issue was an intermixing of static and
instance data on the interface Invokers?

One of the related questions was why the Invoker.class_ref field was
an instance field in the first place. We use static class_ref fields
everywhere else, so being an instance field was certainly different,
but my efforts to figure out why via the commit messages and
git blame didn't pan out...

The reason Invoker.class_ref was an instance field is as follows:
consider the following set of Java interfaces:

	package android.view;

	public interface ViewManager {
		void addView(View view, ViewGroup.LayoutParams params);
		void updateViewLayout(View view, ViewGroup.LayoutParams params);
		void removeView(View view);
	}

	public interface WindowManager extends ViewManager {
		Display getDefaultDisplay();
		void removeViewImmediate(View view);
	}

The generated C# IWindowManagerInvoker type is (more or less):

	[global::Android.Runtime.Register ("android/view/WindowManager", DoNotGenerateAcw=true)]
	internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager {
		static IntPtr class_ref = JNIEnv.FindClass ("android/view/WindowManager");
		// ...
		static IntPtr id_addView;
		public void AddView (Android.Views.View view, Android.Views.ViewGroup.LayoutParams @params)
		{
			if (id_addView == IntPtr.Zero)
				id_addView = JNIEnv.GetMethodID (class_ref, "addView",
						"(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V");
			JNIEnv.CallVoidMethod (Handle, id_addView,
					new JValue (JNIEnv.ToJniHandle (view)), new JValue (JNIEnv.ToJniHandle (@params)));
		}
	}

Nice. Straightforward. So let's invoke IViewManager.AddView() via an
IWindowManager instance (see Hello.cs):

	var die = new TextView (this) {
		Text = "Added!",
	};
	WindowManager.AddView (die, new WindowManagerLayoutParams ());

What could possibly go wrong™?

D/dalvikvm( 6645): GetMethodID: method not found: Landroid/view/WindowManager;.addView:(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
I/MonoDroid( 6645): UNHANDLED EXCEPTION: Java.Lang.NoSuchMethodError: Exception of type 'Java.Lang.NoSuchMethodError' was thrown.
I/MonoDroid( 6645): at Android.Runtime.JNIEnv.GetMethodID (intptr,string,string) <0x0007c>
I/MonoDroid( 6645): at Android.Views.IWindowManagerInvoker.AddView (Android.Views.View,Android.Views.ViewGroup/LayoutParams) <0x00067>
I/MonoDroid( 6645): at Mono.Samples.Hello.HelloActivity.OnCreate (Android.OS.Bundle) <0x005ab>
I/MonoDroid( 6645): at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_ (intptr,intptr,intptr) <0x00057>
I/MonoDroid( 6645): at (wrapper dynamic-method) object.ecadbe0b-9124-445e-a498-f351075f6c89 (intptr,intptr,intptr) <0x00033>

Ouch.

What goes wrong is that the addView() method is declared in the
android.view.ViewManager interface, not the android.view.WindowManager
interface, but we're doing a GetMethodID() on WindowManager. This
sounds like it should work -- WindowManager does implement the
ViewManager interface, after all -- but GetMethodID() doesn't like it
and bombs accordingly.

This is why Invoker.class_ref was an instance field: because we need
to call GetMethodID() against the actual runtime type of the target
object, not against the (compile-time) interface type.

Consequently, in order to fix the bug fixed in xamarin/monodroid@62a4766 in addition to
the bug outlined above, we need to not only make Invoker.class_ref an
instance field (a'la pre- xamarin/monodroid@62a47668), but we also need to make all the
GetMethodID values instance members as well (to fix the issue that
xamarin/monodroid@62a4766 fixed).

The structure of that fix are still with us today; consider: https://github.com/xamarin/java.interop/blob/dcdcce13220d98a848ae3d3598b9b87b22a62c84/tests/generator-Tests/expected/java.lang.Enum/Java.Lang.IComparable.cs

  • IComparableInvoker.class_ref is an instance field.
  • IComparableInvoker.id_compareTo_Ljava_lang_Object_ is an instance field.
  • IComparableInvoker.CompareTo() looks up the jmethodID, storing into id_compareTo_Ljava_lang_Object_. As this is per-instance data, only subsequent IComparable.CompareTo() invocations on this instance will be cached; other instances will be separate.

Consequently, no interface *Invoker data is shared between instances; each instance must re-lookup jmethodID values.

Additionally, in the context of #858, none of the infrastructure currently used by interface *Invoker types will exist.

Thus, the proposal: optimize interface *Invoker types by using JniPeerMembers, one per implemented interface. For IWindowManager, this would be:

[global::Android.Runtime.Register ("android/view/WindowManager", DoNotGenerateAcw=true)]
internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager {

	// One JniPeerMembers value per implemented interface; as IWindowManager implements IViewManager, we need two values.
	static JniPeerMembers _members_ViewManager      = new XAPeerMembers ("android/view/ViewManager", typeof (IWindowManagerInvoker), isInterface: true);
	static JniPeerMembers _members_WindowManager    = new XAPeerMembers ("android/view/WindowManager", typeof (IWindowManagerInvoker), isInterface: true);

	public override JniPeerMembers JniPeerMembers {
		get => _members_WindowManager;
	}

	public override Type ThresholdType {
		get => _members.ManagedPeerType;
	}

	new IntPtr class_ref;

	public override IntPtr ThresholdClass {
		get => class_ref;
	}

	public IWindowManagerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (Validate (handle), transfer)
	{
		IntPtr local_ref = JNIEnv.GetObjectClass (((global::Java.Lang.Object) this).Handle);
		this.class_ref = JNIEnv.NewGlobalRef (local_ref);
		JNIEnv.DeleteLocalRef (local_ref);
	}

	public unsafe void AddView (Android.Views.View? view, Android.Views.ViewGroup.LayoutParams? @params)
	{
		const string __id = "addView.(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V";
		JniArgumentValue* __args = stackalloc JniArgumentValue [2];
		__args [0] = new JniArgumentValue ((view == null) ? IntPtr.Zero : ((global::Java.Lang.Object) view).Handle);
		__args [1] = new JniArgumentValue ((@params == null) ? IntPtr.Zero : ((global::Java.Lang.Object) @params).Handle);
		// As AddView() is from `IViewManager`, use `_members_ViewManager` to invoke it.
		_members_ViewManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, __args);
	}

	// …

	// Note: SKIP java_class_ref, GetObject(), Validate(); those aren't needed anymore.
}

In particular, all methods which implement the IViewManager interface would use _members_ViewManager, while all methods which implement the IWindowManager interface would use _members_WindowManager.

This should allow jmethodID values to be cached across instances, while also allowing appropriate JNI method lookup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementProposed change to current functionalitygeneratorIssues binding a Java library (generator, class-parse, etc.)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions