Skip to content

Instantly share code, notes, and snippets.

@jonathanpeppers
Last active November 9, 2021 16:49
Show Gist options
  • Save jonathanpeppers/054fcc955e540dcf9d8e71c0f7cc69e1 to your computer and use it in GitHub Desktop.
Save jonathanpeppers/054fcc955e540dcf9d8e71c0f7cc69e1 to your computer and use it in GitHub Desktop.
Android / Java / .NET interop TLDR

How does Java interop work in .NET 6?

Most of this also applies to Xamarin.Android, as we have nearly the same implementation.

Let's start with a simple Android API call:

Android.Util.Log.Debug ("MYTAG", "MyMessage");

If we inspect Log.Debug() from a linked build:

[Register("d", "(Ljava/lang/String;Ljava/lang/String;)I", "")]
public unsafe static int Debug(string P_0, string P_1)
{
	IntPtr native_tag = JNIEnv.NewString(P_0);
	IntPtr native_msg = JNIEnv.NewString(P_1);
	try
	{
		JniArgumentValue* __args = stackalloc JniArgumentValue[2];
		*__args = new JniArgumentValue(native_tag);
		__args[1] = new JniArgumentValue(native_msg);
		return _members.StaticMethods.InvokeInt32Method("d.(Ljava/lang/String;Ljava/lang/String;)I", __args);
	}
	finally
	{
		JNIEnv.DeleteLocalRef(native_tag);
		JNIEnv.DeleteLocalRef(native_msg);
	}
}

Notice that JNIEnv.NewString() is called and deleted for every System.String parameter.

Next, we have:

public unsafe int InvokeInt32Method(string P_0, JniArgumentValue* P_1)
{
	JniMethodInfo i = GetMethodInfo(P_0);
	return JniEnvironment.StaticMethods.CallStaticIntMethod(Members.JniPeerType.PeerReference, i, P_1);
}

Then:

public unsafe static int CallStaticIntMethod(JniObjectReference P_0, JniMethodInfo P_1, JniArgumentValue* P_2)
{
	if (!P_0.IsValid)
	{
		throw new ArgumentException("Handle must be valid.", "type");
	}
	if (P_1 == null)
	{
		throw new ArgumentNullException("method");
	}
	if (!P_1.IsValid)
	{
		throw new ArgumentException("Handle value is not valid.", "method");
	}
	IntPtr thrown;
	int result = NativeMethods.java_interop_jnienv_call_static_int_method_a(EnvironmentPointer, out thrown, P_0.Handle, P_1.ID, (IntPtr)P_2);
	Exception __e = GetExceptionForLastThrowable(thrown);
	if (__e != null)
	{
		ExceptionDispatchInfo.Capture(__e).Throw();
	}
	return result;
}

The p/invoke:

[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)]
internal static extern unsafe int java_interop_jnienv_call_static_int_method_a (IntPtr jnienv, out IntPtr thrown, jobject type, IntPtr method, IntPtr args);

This calls the C wrapper we have:

JI_API jint
java_interop_jnienv_call_static_int_method_a (JNIEnv *env, jthrowable *_thrown, jclass type, jstaticmethodID method, jvalue* args)
{
	*_thrown = 0;
	jint _r_ = (*env)->CallStaticIntMethodA (env, type, method, args);
	*_thrown = (*env)->ExceptionOccurred (env);
	return _r_;
}

Relation to .NET MAUI

.NET MAUI commonly has a "handler" implementation to set every property when the "native" side of controls are created.

And so on startup, dotnet/maui loops over each property and each Map*() method called:

Then for example TextView.Text is:

public unsafe ICharSequence? TextFormatted
{
	[Register("setText", "(Ljava/lang/CharSequence;)V", "")]
	set
	{
		IntPtr native_value = CharSequence.ToLocalJniHandle(value);
		try
		{
			JniArgumentValue* __args = stackalloc JniArgumentValue[1];
			*__args = new JniArgumentValue(native_value);
			_members.InstanceMethods.InvokeNonvirtualVoidMethod("setText.(Ljava/lang/CharSequence;)V", this, __args);
		}
		finally
		{
			JNIEnv.DeleteLocalRef(native_value);
			GC.KeepAlive(value);
		}
	}
}

public string? Text
{
	set
	{
		((Java.Lang.Object)(TextFormatted = ((value == null) ? null : new Java.Lang.String(value))))?.Dispose();
	}
}

Ideas

  1. We could make a Java.Lang.String pool to reuse JNIEnv.NewString() instances. This is only helpful if the same C# strings are passed in. This might be a narrow improvement.

  2. We could write a "message pipe" for running many methods in Java at once from a single C# call. This would only cross the JNI boundary once instead of many times when MAUI sets many properties from C#.

An idea of what the Java side would look like would be:

public class DotNetPipe {
	public final static int TEXTVIEW_TEXT = 1;
	public final static int TEXTVIEW_TYPEFACE = 2;
	public final static int TEXTVIEW_TEXTCOLOR = 3;
	public final static int TEXTVIEW_TEXTSIZE = 4;

	public static void Send (Object[] message) {
		int messageId;
		for (int i = 0; i < message.length;) {
			messageId = (int)message[i];
			if (messageId == TEXTVIEW_TEXT) {
				getTextView(message, i).setText((String)message[i + 2]);
				i += 3;
			} else if (messageId == TEXTVIEW_TYPEFACE) {
				getTextView(message, i).setTypeface((Typeface) message[i + 2]);
				i += 3;
			} else if (messageId == TEXTVIEW_TEXTCOLOR) {
				getTextView(message, i).setTextColor((int) message[i + 2]);
				i += 3;
			} else if (messageId== TEXTVIEW_TEXTSIZE) {
				getTextView(message, i).setTextSize((int) message[i + 2], (float) message[i + 3]);
				i += 4;
			} else {
				// Assume unknown messages of size 3
				i += 3;
			}
		}
	}

	private static TextView getTextView(Object[] message, int i)
	{
		return ((TextView)message[i + 1]);
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment