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

[XABT] Move marshal method generation to a "linker step". #10027

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jpobst
Copy link
Contributor

@jpobst jpobst commented Apr 11, 2025

This commit moves the process of rewriting marshal methods found in Java binding libraries to a new RewriteMarshalMethodsStep "linker step".

The marshal method generation step involves scanning through all assemblies that may contain Java bindings with Cecil, making changes, and writing the assemblies back out to disk. By combining this with our "linker steps" pipeline, we only need to open, modify, and write the assemblies to disk once instead of twice.

@jpobst jpobst force-pushed the dev/jpobst/move-marshal-methods branch from 5a287b2 to d8bcd19 Compare April 11, 2025 18:56
@jpobst
Copy link
Contributor Author

jpobst commented Apr 11, 2025

There seems to be an issue with this as we move from an "all at once" model to an incremental "per-assembly" model.

Previously we would scan all assemblies with the marshal method classifier to determine which methods needed to be changed. Then we would perform the needed modifications on all the assemblies.

In the new process, we would like to be able to convert single assemblies independently. For example, in an incremental build where only a user's custom binding project changed, we do not want to rerun the marshal method generation process on every assembly in the project.

However, this leads to a problem, as scanning an assembly requires being able to find the relevant "connector" method which may be in a dependent assembly. If this dependent assembly has already gone through the marshal method process then the connector method has already been removed then the marshal method process fails to convert the new method.

A concrete example:

  • Xamarin.AndroidX.Core contains the class ComponentActivity
  • ComponentActivity implements the ILifecycleOwner interface property Lifecycle from the assembly Xamarin.AndroidX.Lifecycle.Common
     
// Xamarin.AndroidX.Core - 1.12.0.2
[Register("androidx/core/app/ComponentActivity", DoNotGenerateAcw = true)]
public class AndroidX.Core.App.ComponentActivity.ComponentActivity : Activity, KeyEventDispatcher.IComponent, IJavaObject, IDisposable, IJavaPeerable, ILifecycleOwner
{
	public unsafe virtual Lifecycle Lifecycle
	{
		[Register("getLifecycle", "()Landroidx/lifecycle/Lifecycle;", "GetGetLifecycleHandler")]
		get {
			JniObjectReference val = _members.get_InstanceMethods().InvokeVirtualObjectMethod("getLifecycle.()Landroidx/lifecycle/Lifecycle;", (IJavaPeerable)(object)this, (JniArgumentValue*)null);
			return Object.GetObject<Lifecycle>(((JniObjectReference)(ref val)).get_Handle(), (JniHandleOwnership)1);
		}
	}

	private static IntPtr n_GetLifecycle(IntPtr jnienv, IntPtr native__this)
	{
		return JNIEnv.ToLocalJniHandle((IJavaObject)(object)Object.GetObject<ComponentActivity>(jnienv, native__this, (JniHandleOwnership)0).Lifecycle);
	}

	private static Delegate GetGetLifecycleHandler()
	{
		if ((object)cb_getLifecycle == null) {
			cb_getLifecycle = JNINativeWrapper.CreateDelegate((Delegate)new _JniMarshal_PP_L(n_GetLifecycle));
		}
		return cb_getLifecycle;
	}
}

// Xamarin.AndroidX.Lifecycle.Common - 2.6.2.2
[Register("androidx/lifecycle/LifecycleOwner", "", "AndroidX.Lifecycle.ILifecycleOwnerInvoker")]
public interface AndroidX.Lifecycle.ILifecycleOwner : IJavaObject, IDisposable, IJavaPeerable
{
	Lifecycle Lifecycle
	{
		[Register("getLifecycle", "()Landroidx/lifecycle/Lifecycle;", "GetGetLifecycleHandler:AndroidX.Lifecycle.ILifecycleOwnerInvoker, Xamarin.AndroidX.Lifecycle.Common")]
		get;
	}
}

[Register("androidx/lifecycle/LifecycleOwner", DoNotGenerateAcw = true)]
internal class AndroidX.Lifecycle.ILifecycleOwnerInvoker : Object, ILifecycleOwner, IJavaObject, IDisposable, IJavaPeerable
{
	public Lifecycle Lifecycle
	{
		get
		{
			if (id_getLifecycle == IntPtr.Zero)
			{
				id_getLifecycle = JNIEnv.GetMethodID(class_ref, "getLifecycle", "()Landroidx/lifecycle/Lifecycle;");
			}
			return Object.GetObject<Lifecycle>(JNIEnv.CallObjectMethod(((Object)this).get_Handle(), id_getLifecycle), (JniHandleOwnership)1);
		}
	}

	private static IntPtr n_GetLifecycle(IntPtr jnienv, IntPtr native__this)
	{
		return JNIEnv.ToLocalJniHandle((IJavaObject)(object)Object.GetObject<ILifecycleOwner>(jnienv, native__this, (JniHandleOwnership)0).Lifecycle);
	}

	private static Delegate GetGetLifecycleHandler()
	{
		if ((object)cb_getLifecycle == null)
		{
			cb_getLifecycle = JNINativeWrapper.CreateDelegate((Delegate)new _JniMarshal_PP_L(n_GetLifecycle));
		}
		return cb_getLifecycle;
	}
}

Now imagine that the marshal method rewriter runs on the Xamarin.AndroidX.Lifecycle.Common assembly before running on Xamarin.AndroidX.Core.

It removes GetGetLifecycleHandler () and replaces it with n_GetLifecycle_mm_wrapper (...):

// Xamarin.AndroidX.Lifecycle.Common - 2.6.2.2
[Register("androidx/lifecycle/LifecycleOwner", DoNotGenerateAcw = true)]
internal class AndroidX.Lifecycle.ILifecycleOwnerInvoker : Object, ILifecycleOwner, IJavaObject, IDisposable, IJavaPeerable
{
	[UnmanagedCallersOnly]
	private static IntPtr n_GetLifecycle_mm_wrapper(IntPtr jnienv, IntPtr native__this)
	{
		AndroidRuntimeInternal.WaitForBridgeProcessing();
		try
		{
			return n_GetLifecycle(jnienv, native__this);
		}
		catch (System.Exception e)
		{
			Android.Runtime.AndroidEnvironmentInternal.UnhandledException(e);
			return (IntPtr)0;
		}
	}
}

Then we run the marshal method rewriter on Xamarin.AndroidX.Core which fails on scanning ComponentActivity.Lifecycle with:

Connector method 'GetGetLifecycleHandler' not found in type 'AndroidX.Lifecycle.ILifecycleOwnerInvoker'

@jpobst jpobst force-pushed the dev/jpobst/move-marshal-methods branch from d8bcd19 to d26e3a2 Compare April 11, 2025 23:20
@jonpryor
Copy link
Member

However, this leads to a problem, as scanning an assembly requires being able to find the relevant "connector" method which may be in a dependent assembly.

An easier example would be "app assembly" and Mono.Android.dll, in which the "app assembly" has an Activity subclass that overrides OnCreate() (a'la the default dotnet new android template). If Mono.Android.dll is processed before the app assembly, then Android.App.Activity.GetOnCreateHandler() will be missing when the "app assembly" is processed, resulting in the same error message.

We will need to retain the Get…Handler() methods, and replace their body to be "empty", e.g. ldnull; throw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants