Skip to content

[XABT] Move JLO scanning needed for typemap generation to a linker step. #10015

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

Merged
merged 4 commits into from
Apr 30, 2025

Conversation

jpobst
Copy link
Contributor

@jpobst jpobst commented Apr 8, 2025

Context: #9893
Context: #9930

This commit moves the process of scanning for JLOs needed for the typemap generation task to a new FindTypeMapObjectsStep "linker step".

The types are serialized to new <assembly>.typemap.xml files that sit beside the source assembly. A new file was used rather than adding more sections to the .jlo.xml file because this file contains a lot more types and will likely change more often on incremental builds, and we don't want to trigger ie: creating new Java stubs when it isn't needed.

This file is then read in the existing <GenerateTypeMappings> task to generate the final typemap file.

The existing method of JLO scanning for typemaps is still used when using LLVM marshal methods, as the marshal method rewriter runs after the linker steps, and when it changes the assemblies the MVID and type tokens no longer match the values saved in the .typemap.xml files.

Like #9893, this temporarily leaves the old typemap generation code in place, guarded behind the $(_AndroidJLOCheckedBuild) flag. This flag generates the typemap both the new and old way, and errors the build if there are differences.

Note that #10024 updated our "pipeline" to save assemblies after all steps have been run. However, looking for JLOs needed for typemap generation has to be run after assemblies are saved. This is because Release typemaps rely on assembly MVID and type tokens which change when the assemblies are modified.

Update the pipeline's "modified assembly saving" logic to itself be a pipeline step, so we can insert it before the new FindTypeMapObjectsStep step.

@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch from e187164 to 4a00982 Compare April 8, 2025 20:57
@jpobst
Copy link
Contributor Author

jpobst commented Apr 8, 2025

A potential issue here is with cross-assembly duplicate detection.

Today, we are scanning all JLO derived types from all assemblies in a single loop, so duplicate detection logic like the following finds duplicates even if they are in different assemblies:

TypeMapDebugEntry oldEntry = duplicates [0];
if ((td.IsAbstract || td.IsInterface) &&
!oldEntry.TypeDefinition.IsAbstract &&
!oldEntry.TypeDefinition.IsInterface &&
td.IsAssignableFrom (oldEntry.TypeDefinition, cache)) {
// We found the `Invoker` type *before* the declared type
// Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate.
duplicates.Insert (0, entry);
oldEntry.SkipInJavaToManaged = false;
} else {
// ¯\_(ツ)_/¯
duplicates.Add (entry);

Because the new process only scans a single assembly at a time with Cecil, it will find duplicates in the same assembly, but not duplicates that may exist in other assemblies. An example is Java.Lang.Object:

Existing process:
- JavaName: "java/lang/Object" 
- ManagedName: "Java.Interop.JavaObject, Java.Interop" 
- SkipInJavaToManaged: "False" 
- DuplicateForJavaToManaged
  - JavaName: "java/lang/Object" 
  - ManagedName: "Java.Lang.Object, Mono.Android" 

New process:
- JavaName: "java/lang/Object" 
- ManagedName: "Java.Interop.JavaObject, Java.Interop" 
- SkipInJavaToManaged: "False" 
- DuplicateForJavaToManaged: null

It looks like we have 2 ordering semantics we have to preserve in replicating this logic:

  • A declared type should take precedence over an "invoker" type.
  • Types in Mono.Android should take precedence over other assemblies.

Aside from these 2 cases, which type is considered the "primary" and which type(s) are considered the "duplicates" seems to be luck of the draw.

@jonpryor
Copy link
Member

jonpryor commented Apr 8, 2025

@jpobst wrote:

type is considered the "primary" and which type(s) are considered the "duplicates" seems to be luck of the draw.

This is correct and incomplete. If you're only going by Java.Lang.Object.GetObject(handle), then you are correct.

However, most codepaths are not hitting Object.GetObject(IntPtr). Most codepaths are hitting Object.GetObject<T>(…) or Object.GetObject(…, Type targetType), which constrains which type that is used.

Thus, even if there are duplicate types (because bindings for a .jar are present in multiple assemblies), it generally won't matter because the APIs you're using will dictate the actual types used.

@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch 3 times, most recently from 9b84897 to faaad5a Compare April 15, 2025 22:58
@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch 5 times, most recently from 0d66bf2 to f0dea44 Compare April 17, 2025 22:40
@dotnet dotnet deleted a comment from azure-pipelines bot Apr 18, 2025
@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch 3 times, most recently from 8650e83 to f8100ea Compare April 18, 2025 22:28
@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch from f8100ea to eae6b50 Compare April 18, 2025 23:00
@jpobst jpobst marked this pull request as ready for review April 19, 2025 01:45
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the new trimmer step not setup for Release mode? I thought it would be listed here:

The PR description mentions it won't work in combination with $(AndroidEnableMarshalMethods)=true, but would it work for a Release build with marshal methods off?

@jpobst
Copy link
Contributor Author

jpobst commented Apr 21, 2025

Is the new trimmer step not setup for Release mode?

Yes, it runs as part of the <AssemblyModifierPipeline> task, which is run by both the LinkAssembliesNoShrink and _AfterILLinkAdditionalSteps targets.

It works for Release mode when $(AndroidEnableMarshalMethods)=false.


public TaskLoggingHelper Log { get; set; }

public FindTypeMapObjectsStep (TaskLoggingHelper log) => Log = log;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a "ha ha only serious" / "future coding guidelines" question/discussion: should we start using C# primary constructors?

public partial class FindTypeMapObjectStep (TaskLoggingHelper Log) : BaseStep, IAssemblyModifierPipelineStep {
    // …
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love primary constructors, but if that's what we decide we want to standardize as a team it won't kill me. 😁

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of primary constructors either... :)

};

if (Debug) {
var (javaToManaged, managedToJava) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context, out var foundJniNativeRegistration);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a tuple return type, why not have out var foundJniNativeRegistration be a separate tuple value?

xml.ManagedToJavaDebugEntries.AddRange (managedToJava);
xml.FoundJniNativeRegistration = foundJniNativeRegistration;

if (!xml.HasDebugEntries) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this block and the "equivalent" block in the else branch should be moved to the end…

return;
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…to here, as:

if (!xml.HasDebugEntries && xml.ModuleReleaseData == null) {
    Log.LogDebugMessage ("No Java types found…");
    TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log)
    return;
}


public bool WasScanned { get; private set; }

public void Export (string filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be responsible for checking for !this.HasDebugEntries && this.ModuleReleaseData == null and writing an empty file if there are no entries to write? This could simplify the call-sites.

Additionally, if we add a Log property to this class, the No Java types found/Wrote … message could also be moved here:

public void Export (string filename, string assemblyName)
{
    if (!HasDebugEntries && ModuleReleaseData == null) {
        Log.LogDebugMessage ($"No Java types found in '{assemblyName}'");
        TypeMapObjectsXmlFile.WriteEmptyFile (filename, Log)
        return;
    }
    Log.LogDebugMessage ($"Wrote '{filename}', {JavaToManagedDebugEntries.Count} JavaToManagedDebugEntries, {ManagedToJavaDebugEntries.Count} ManagedToJavaDebugEntries, FoundJniNativeRegistration: {FoundJniNativeRegistration}")
    // …
}

@jpobst
Copy link
Contributor Author

jpobst commented Apr 22, 2025

Addressed feedback.

@jpobst jpobst requested a review from Copilot April 23, 2025 18:30
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the typemap generation process by moving the JLO scanning to a new linker step and updating the overall assembly pipeline to better support incremental builds. Key changes include:

  • Introducing a new FindTypeMapObjectsStep that writes typemap XML files beside source assemblies.
  • Refactoring TypeMapGenerator and TypeMapCecilAdapter to use new adapter interfaces for handling debug and release typemap generation.
  • Updating the AssemblyModifierPipeline to use void-based ProcessAssembly methods and adding a SaveChangedAssemblyStep.

Reviewed Changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs Introduces XML export logic for typemap objects with support for both debug and release entries.
src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs Refactors type mapping generation using new adapter abstractions and integrates the RunCheckedBuild flag.
src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs Updates debug and release entry generation and duplicate handling logic.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs Updates the task to generate typemaps from both managed and native states, with changes for safe file output accumulation.
src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs Modifies pipeline steps to use void-based ProcessAssembly and introduces a new step to save changed assemblies.
src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs Adds a new linker step that scans for Java types needed for typemap generation and writes them to an XML file.
Files not reviewed (2)
  • src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj: Language not supported
  • src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets: Language not supported

@jpobst jpobst force-pushed the dev/jpobst/link-step-typemaps branch from b5a211d to eee3b1c Compare April 25, 2025 21:37
}

void CopyIfChanged (ITaskItem source, ITaskItem destination)
class SaveChangedAssemblyStep : IAssemblyModifierPipelineStep
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason that this is a nested type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, unnested.

@jonpryor
Copy link
Member

Context: ea399edb872588c16b124c4b9ab2232243b93016
Context: 787a2a6238942094f4a3fc62fb5595b46efa21ce
Context: b54ec05c4041ea2cb1e70058888a81e398df1e25
COntext: b11471bc574876e148ff40757c5d273fd6fbde9e

Move the process of scanning for `Java.Lang.Object` (JLO) and
`Java.Lang.Throwable` subclasses needed for the typemap generation
task to a new `FindTypeMapObjectsStep` "linker step".

The types are serialized to new `<assembly>.typemap.xml` files that
sit beside the source assembly.  A new file was used rather than adding
more sections to the `.jlo.xml` file because this file contains a lot
more types and will likely change more often on incremental builds, and
we don't want to trigger ie: creating new Java stubs when not needed.

This file is then read in the existing `<GenerateTypeMappings/>` task
to generate the final typemap file.

The existing method of JLO scanning for typemaps is still used when
using LLVM marshal methods, as the marshal method rewriter runs after
the linker steps, and when it changes the assemblies the MVID and type
tokens no longer match the values saved in the `.typemap.xml` files.

Like ea399edb, this temporarily leaves the old typemap generation code
in place, guarded behind the `$(_AndroidJLOCheckedBuild)` flag.
This flag generates the typemap both the new and old way, and errors
out if there are differences.

Note that b54ec05c updated our "pipeline" to save assemblies after
*all* steps have been run.  However, looking for JLOs needed for
typemap generation has to be run *after* assemblies are saved.
This is because Release typemaps rely on assembly MVID and type tokens
which change when the assemblies are modified.

Update the pipeline's "modified assembly saving" logic to itself be a
pipeline step, so we can insert it *before* the new
`FindTypeMapObjectsStep` step.

*Note*: previously, we scanned for all JLO subclasses from *all*
assemblies in a single loop, so [duplicate detection logic][0] finds
duplicates even if they are in different assemblies.

Consider `java.lang.Object`, which is bound as:

  * `Java.Lang.Object, Mono.Android`
  * `Java.Interop.JavaObject, Java.Interop`
  * *and also* several other unit test types as "aliases" (b11471bc)

When processing `Java.Interop.dll`, the `TypeMapDebugEntry` fields
would be:

  - JavaName: "java/lang/Object" 
  - ManagedName: "Java.Interop.JavaObject, Java.Interop" 
  - SkipInJavaToManaged: "False" 
  - DuplicateForJavaToManaged
    - JavaName: "java/lang/Object" 
    - ManagedName: "Java.Lang.Object, Mono.Android" 

In particular, `TypeMapDebugEntry.DuplicateForJavaToManaged` mentions
`Java.Lang.Object, Mono.Android` as the "main" typemap.

*Now* scanning is performed *per-assembly*; it will find duplicates in
the same assembly, but not duplicates that may exist in other
assemblies.  The `TypeMapDebugEntry` fields when processing
`Java.Interop.dll` are instead:

  - JavaName: "java/lang/Object" 
  - ManagedName: "Java.Interop.JavaObject, Java.Interop" 
  - SkipInJavaToManaged: "False" 
  - DuplicateForJavaToManaged: null

Note that `TypeMapDebugEntry.DuplicateForJavaToManaged` is null.


We have two ordering semantics we have to preserve in replicating
the original logic:

  - A declared type should take precedence over an "invoker" type.
  - Types in `Mono.Android` have precedence over other assemblies.

Aside from these two cases, which type is considered the "primary"
and which type(s) are considered the "duplicates" seems to be luck
of the draw.

Note that "luck" only enters the picture when using the non-generic
`JniRuntime.JniValueManager.GetPeer(JniObjectReference, Type?)` API
and providing `null` for the `Type?` parameter.  Whenever a `Type`
*is provided*, or a generic wrapper such as
`Java.Lang.Object.GetObject<T>()` is used, then value returned is
*constrained* to the specified type, reducing the likelihood of
obtaining an unexpected type.

[0]: https://github.com/dotnet/android/blob/8c741263550cc57bafc30193fa61f39b7df019bc/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs#L141-L152

@jonpryor jonpryor merged commit f03662c into main Apr 30, 2025
59 checks passed
@jonpryor jonpryor deleted the dev/jpobst/link-step-typemaps branch April 30, 2025 01:17
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.

4 participants