Skip to content

Blueprint

David Ortinau edited this page Sep 3, 2024 · 7 revisions

Blueprint

.NET for Android (née Xamarin.Android) is a set of tooling to create Android apps in C#.

What are the various "pieces" of .NET for Android, and what are their relationships with each other?

Relations

App Build Process [WIP]

The .NET for Android app build process adds a number of custom steps to the standard .net build process.

Before Build

  • Resolve and Compile Android Resources (aapt2 compile)
  • Build a temp package for Resource designer generation (aapt2 link)

After Build

  • Generate Java Wrappers for .net code which derive from Java Classes (GenerateJavaStubs)
  • Compile all Java code (CompileJava)
  • Convert Java .class files to classes.dex (CompileToDalvik)
  • Resolve and Compile any updated Android Resources (aapt2 compile)
  • Build final app package (aapt2 link)
  • Add .NET for Android specific files to the final app package (BuildApk)
  • Sign and Align final app package.

Java Library Binding Process

The .NET for Android binding process, broadly speaking, involves four steps:

  1. Parse .class files -- present in .jar and .aar files -- into an XML description. The class-parse utility is responsible for generating this XML description.

  2. (Optional) Parse Java Source Code, typically provided via the @(JavaSourceJar) build action, to extract parameter names and Javadoc documentation. Java .class files often do not contain parameter names -- it requires using javac -parameters, which isn't a default -- thus parsing Java source code is often the most reliable mechanism to obtain parameter names. The java-source-utils utility is responsible for parsing Java source code.

  3. Consume the XML description, and generate a set of C# files which bind the types in (1). The .NET for Android generator utility is responsible for this. generator can also consume the output of java-source-utils, and can attempt to convert extracted Javadoc comments into C# XML Documentation comments.

  4. Compile the C# from (3) into the final assembly.

class-parse and generator are implicitly used as part of the .NET for Android @(EmbeddedJar) and @(InputJar) build action within Binding projects, and in .NET for Android via the @(AndroidLibrary) build action, when %(AndroidLibrary.Bind)=True.

See also:

Java Type Registration

Java Type Registration requires that On-Android Process Startup has completed, so that mono.android.Runtime is initialized and usable. This means that Java types which are instantiated before our Content Provider is executed cannot participate in "normal" Java Type Registration. This constraint is primarily limited to Application subclasses, support for which will be detailed "later".

During the App Build Process, Java Callable Wrappers are created. Among other things, the Java Callable Wrapper class constructor contains a Runtime.register() invocation, which lists all Java native methods which need to be registered:

/* partial */ class MainActivity {
	static {
		__md_methods = 
			"n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" +
			"";
		mono.android.Runtime.register ("HelloWorld.MainActivity, HelloWorld", MainActivity.class, __md_methods);
	}

	private native void n_onCreate (android.os.Bundle p0);
}

Runtime.register() is Java_mono_android_Runtime_register(), which eventually invokes JNIEnv. RegisterJniNatives() and AndroidTypeManager.RegisterNativeMembers(). AndroidTypeManager.RegisterNativeMembers() is in turn an unholy mess of string splitting and Reflection: the __md_methods value from the Java Callable Wrapper is split on \n, one line per method to register, and each line is in turn split on :, to extract:

  • The Java native method name to register.
  • The JNI signature of the method to register.
  • A "Locator" for where to get a Delegate which contains the marshal method to register.
  • (Optional) the type that the Locator method is declared on. (Only used if the method was declared on an interface.)

This process in turn relies on internal implementation details of generator output: the Locator method must exist either in the type (or base type) of the Managed type being registered -- HelloWorld.MainActivity, HelloWorld, for the above example -- or must exist in the explicit 4th parameter. The Locator method must also be:

  1. static
  2. Return System.Delegate
  3. Take no parameters.

For example:

namespace Android.App {
	public partial class Activity {

		static Delegate? cb_onCreate_Landroid_os_Bundle_;
#pragma warning disable 0169
		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
	}
}

Once all the Java native methods to register have been found, along with their corresponding Locator methods, the Locator methods are invoked (via Reflection), and the resulting value stored into a JniNativeMethodRegistration array, which is used to invoke JNIEnv::RegisterNatives().

Once this has been completed, Java code can invoke the Java native methods, which will cause the marshal method to be invoked (e.g. Activity.n_OnCreate_Landroid_os_Bundle_()), which will cause the Managed override method to be invoked (e.g. MainActivity.OnCreate()).

See also:

Primary source code locations:

On-Android Process Startup

TODO: Determine of the App Startup AndroidX Library should change how we do things.

TODO: Find some sources to elaborate on the "pre-.NET startup" bits.

When the user taps on an App icon on an Android device, the Activity associated with that App is looked up. If a process for that Activity doesn't already exist, a process will be created (somewhere) in Zygote.java, an Application singleton instance is created based on the contents of //application/@android:name attribute within AndroidManifest.xml, and eventually all relevant <provider/>s will be constructed.

This is where .NET for Android enters the picture: during the App Build Process, a <provider/> is always added to AndroidManifest.xml, one per detected process name:

<provider android:name="mono.MonoRuntimeProvider" android:exported="false" android:initOrder="1999999999" android:authorities="com.companyname.helloworld.mono.MonoRuntimeProvider.__mono_init__" />

This XML fragment causes MonoRuntimeProvider to be instantiated, and the Content Provider lifecycle to be started. We don't use <provider/> to actually add a Content Provider; we use it because this was the earliest point we could find during Android process startup through which we could execute code.

When MonoRuntimeProvider.attachInfo() is invoked by Android, .NET for Android has an opportunity to load Mono. This is done via MonoPackageManager.LoadApplication(), which is the primary Java-side startup code path.

MonoPackageManager.LoadApplication() loads various native libraries, including libmonodroid.so. Once libmonodroid.so has been loaded, mono.android.Runtime methods can be invoked, in particular Runtime.initInternal(). Through the glory of JNI native method resolution, Runtime.initInternal() is resolved as the C symbol Java_mono_android_Runtime_initInternal().

Runtime.initInternal() is responsible for preparing the process for managed code execution:

  • Initialize MonoVM
  • Find assemblies included with the App
  • etc., etc.

Once MonoVM has been initialized, Mono.Android.dll is loaded, then JNIEnv.Initialize() is invoked. This gives Mono.Android.dll a chance to perform initialization.

Once Runtime.initInternal() completes, MonoPackageManager.attachInfo() returns, and normal Android app startup resumes.

Primary source code locations:

Pieces

The pieces which make of .NET for Android include:

App Runtime

"Glue" code for running apps on Android device, process startup, etc.

Primary source code locations are:

class-parse

class-parse parses .class and .jar files, and produces an XML description of the Java APIs found.

Primary source code locations are:

generator

Create C# bindings from XML description.

Also can optionally attempt to import Javadoc documentation, as imported from java-source-utils, and convert into C# XML Documentation Comments.

Primary source code locations are:

Fast Deployment

Quickly update existing app installs on an Android device, for quick dev inner loops. Part of .NET for Android build tasks.

https://github.com/xamarin/monodroid#commercial-xamarinandroid-dependencies

Java Callable Wrapper Generator

Create Java “stub” code to permit Java-to-managed method invocations. Required for on-device app use. Part of Xamarin.AndroidBuild.Tasks.

Docs:

Primary source code locations are:

java-source-utils

Java source code parser, using com.github.javaparser.

Exists to improve Java Library bindings in .NET for Android. Bindings in .NET for Android frequently hit two common pitfalls:

  1. Lack of parameter names.
  2. Lack of documentation.

Java method parameter names are useful in-and-of-themselves. However, parameter names are also used when turning *Listener interfaces into events, wherein each method parameter becomes an *EventArgs property. See also: Events and Listeners in API Design.

The problem with Java method parameter names is that by default, Java .class files don't contain parameter names. Java .class files can contain parameter names, but only if javac -parameters was used to compile the Java source code, which frequently is not the case. If parameter names aren't present, then names such as p0 are used, which results in very ugly *EventArgs types.

Additionally, Java libraries frequently provide documentation, in the form of Javadoc documentation within Java source code.

java-source-utils parses Java source code, extracting parameter names and Javadoc documentation. The extracted information can then be used by the .NET for Android generator utility to provide parameter name information. generator can also attempt to convert the extracted Javadoc documentation into C# XML Documentation Comments.

See also:

java-source-utils is used by .NET for Android, via the @(JavaSourceJar) build item.

Primary source code locations are:

Mono.Android.dll

Core binding for the Android API.

Primary source code locations are:

Mono.Android.Export.dll

Runtime support for the [Export] custom attribute.

Primary source code locations are:

Xamarin.AndroidBuild.Tasks.dll

"Glue" for creating Android .apk and .aab files. Provides MSBuild tasks and targets and wrappers for various Android SDK toolchain apps. ("You are in a maze of twisted MSBuild, all alike.")

Primary source code locations are:

Clone this wiki locally