Skip to content

[TrimmableTypeMap] Detect overrides and constructors in JavaPeerScanner#10932

Open
simonrozsival wants to merge 15 commits intomainfrom
dev/simonrozsival/trimmable-typemap-override-detection
Open

[TrimmableTypeMap] Detect overrides and constructors in JavaPeerScanner#10932
simonrozsival wants to merge 15 commits intomainfrom
dev/simonrozsival/trimmable-typemap-override-detection

Conversation

@simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Mar 13, 2026

Part of #10789

Context

The JavaPeerScanner was missing two categories of methods that the legacy CecilImporter finds:

  1. Method overrides without [Register] — User types (e.g., MainActivity) override Activity.OnCreate() without [Register] on the override. The attribute is only on the base class method in Mono.Android.

  2. Java constructors via base ctor chaining — User types have non-activation constructors without [Register]. The legacy CecilImporter walks the base type hierarchy, seeds registered ctors, and accepts derived ctors with compatible parameters.

Missing constructors meant no JCW Java constructors, no UCO constructor wrappers, and no nctor_N native declarations.

Changes

Scanner (JavaPeerScanner.cs)

Pass 3 — Override detection: For each virtual override without [Register], walks the base hierarchy to find the registered base method and copies its registration info. Also handles property overrides (e.g., Throwable.Message). FindBaseRegisteredMethodInfo and FindBaseRegisteredProperty walk all base types with no DoNotGenerateAcw boundary — matching legacy GetBaseDefinition/GetBaseRegisteredMethod behavior. (Only constructor chaining has the DoNotGenerateAcw boundary.)

Pass 4 — Constructor chain detection: Mirrors legacy CecilImporter behavior:

  • Seeds all base registered ctors directly into the type's marshal methods (e.g., Implementor types get the base ()V ctor from Object)
  • Matches derived ctors with compatible parameters against base registered ctors
  • Fallback: if a base has a registered ()V ctor, accepts derived ctors with novel params (generates super() call via SuperArgumentsString=""). Rejects ctors with non-Java parameter types (System.Object, System.IntPtr, System.Action, etc.) matching legacy GetJniSignature null behavior.
  • Handles [JniConstructorSignature] (Java.Interop-style) in addition to [Register] in TryGetMethodRegisterInfo
  • Stops base walk at DoNotGenerateAcw boundary (matching legacy)
  • Only runs for ACW types (gated behind detectBaseOverrides), matching legacy which only runs CecilImporter.CreateType for non-interface, non-DoNotGenerateAcw types

Helper methods:

  • TryResolveBaseType — encapsulates the 3-step base type resolution pattern (GetBaseTypeInfo → TryResolveType → GetTypeDefinition)
  • CollectBaseRegisteredCtors — walks base hierarchy collecting registered ctors
  • ManagedTypeToJniDescriptorOrNull — nullable variant of ManagedTypeToJniDescriptor for fallback signature computation; returns null for types without a proper JNI mapping
  • BuildJniCtorSignature — computes JNI ctor signature from managed params, returns null when params aren't Java-mappable

Integration tests

ScannerRunner.cs — Rewrote ExtractMethodRegistrations to use the real legacy JCW pipeline (CecilImporter.CreateType) instead of manual [Register] attribute scanning. This catches overrides and ctor-chained methods that the manual approach missed. Key changes:

  • Uses CecilImporter.CreateType for non-interface, non-DoNotGenerateAcw types
  • Falls back to direct attribute extraction for interfaces and MCW types
  • Filters interface method implementations from CecilImporter output (new scanner places these on the interface peer type)
  • Parses connectors from CallableWrapperMethod.Method string format
  • Excludes constructors from marshal method comparison (constructors are compared separately in ExactJavaConstructors)
  • Made HasDoNotGenerateAcw internal for reuse by TypeDataBuilder

TypeDataBuilder.cs — Replaced manual [Register] attribute scanning for constructors with CecilImporter.CreateType to get the accurate legacy constructor list including the full base ctor chain. Uses ExtractDirectRegisterCtors for interfaces and DoNotGenerateAcw types.

Test fixtures

  • Added [Register(".ctor", "()V", "")] to Activity fixture (matches real Mono.Android auto-generated bindings)
  • Added JniConstructorSignatureAttribute stub in StubAttributes.cs
  • Added override/ctor detection types: UserActivity, FullActivity, DeeplyDerived, MixedMethods, NewSlotActivity, CustomException, JiStyleView, ActivityWithCustomCtor, BaseFragment, DerivedFragment, GrandchildFragment, DialogBase, CustomDialog, ActivityWithMultiParamCtor

Unit tests

  • OverrideDetectionTests (10 tests) — overrides without [Register], deep inheritance (3 levels), ACW→ACW→MCW hierarchy, property overrides, mixed methods, new-slot exclusion, DeclaringTypeName verification
  • ConstructorDetectionTests (9 tests) — base ctor chain, implicit default ctor, base ctor seed, JniConstructorSignature, parameterless fallback with super(), same-arity type mismatch, multi-parameter signature computation, direct [Register] regression guard

Related issue

Filed #10931JcwJavaSourceGenerator must skip registerNatives in the static block for Application/Instrumentation types (the static initializer runs before the runtime ContentProvider loads the native library).

Test results

  • 247 unit tests pass
  • 10 integration tests pass (including ExactJavaConstructors_MonoAndroid and ExactMarshalMethods_MonoAndroid which validate against real Mono.Android.dll with ~8000 types)

Legacy comparison notes

Deep comparison with the legacy CecilImporter / GetBaseRegisteredMethod / CallableWrapperType pipeline:

Scanner behavior (matches legacy)

  • Override detection matches legacy GetBaseDefinitionGetBaseRegisteredMethod: walks all base types (no DoNotGenerateAcw boundary), checks Virtual || Abstract, matches by name + parameter compatibility. Only run for user ACW types (gated by detectBaseOverrides), matching how JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration filters DoNotGenerateAcw types.
  • Constructor chaining matches legacy ctorTypes loop (CecilImporter.cs:124-160): stops at DoNotGenerateAcw, reverses to process base-first, seeds registered base ctors, checks AreParametersCompatibleWith, falls back to parameterless super() when baseCtors.Any(m => !m.HasParameters).
  • Parameter compatibility uses string comparison of decoded S.R.M. signatures instead of Cecil's richer IsParameterCompatibleWith (which handles generic constraints, modifiers, TypeSpecification). This is equivalent for the non-generic, non-modifier cases that Android APIs use — validated against ~8000 real Mono.Android types with zero divergence.

JCW Java source (intentional differences)

  • Constructor body: Legacy calls mono.android.TypeManager.Activate(...), new calls nctor_N(...) (UCO dispatches to TrimmableNativeRegistration.ActivateInstance). Intentional architectural change.
  • Static initializer: Legacy builds __md_methods string and calls Runtime.register(...), new calls Runtime.registerNatives(ClassName.class). Intentional architectural change.
  • Native method visibility: Legacy emits private native n_*, new emits public native n_* (needed for UCO registerNatives dispatch).
  • @Override: Legacy only emits annotations from managed custom attributes. New hard-codes @Override on registered method overrides (more correct for Java compiler).
  • Application/Instrumentation: Legacy defers register() to generated onCreate() and emits ApplicationConstructor with MonoPackageManager.setContext(). New doesn't handle this yet — tracked in [TrimmableTypeMap] JcwJavaSourceGenerator must skip registerNatives for Application/Instrumentation types #10931.
  • Nested types: Legacy recursively emits nested JCW types with separate __md_N_methods. Not yet implemented in the new generator.
  • Type/method annotations: Legacy converts managed custom attributes to Java annotations (@Activity(label=...) etc.). New generator doesn't emit these — they're handled by the AndroidManifest pipeline, not the JCW source files.

simonrozsival and others added 9 commits March 13, 2026 09:46
The scanner only found marshal methods with [Register] directly on them.
User types (e.g., MainActivity) that override Activity.OnCreate() don't
have [Register] on the override — the attribute is only on the base
class method in Mono.Android. This caused missing UCO wrappers,
RegisterNatives, and empty JCW Java files.

Add a third pass in CollectMarshalMethods that walks the base type
find a matching registered base method. Also handles property overrides
(e.g., Throwable.Message). Override detection is gated behind
DoNotGenerateAcw — MCW framework types already have [Register] on
every method that matters.

Integration parity: ExactMarshalMethods_UserTypesFixture passes (exact
parity for user types). ExactMarshalMethods_MonoAndroid has 8 known
extras — internal Mono.Android types (InputStreamAdapter, JavaObject,
OutputStreamAdapter) that override registered base methods without
their own [Register]. The legacy scanner skips these because its JCW
pipeline doesn't process them. These extras are harmless — the types
have hand-written JCWs. A follow-up can update the parity test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The scanner doesn't detect Java constructors for user types whose
constructors lack [Register]. The legacy CecilImporter chains from
base registered ctors to derived unregistered ctors, but the new
scanner skips constructors in override detection (Pass 3) and
FindBaseRegisteredMethodInfo requires Virtual/Abstract (which ctors
are not).

Add [Register(".ctor", "()V", "")] to the Activity test fixture
to match real Mono.Android bindings, and add 10 tests:
- 7 failing: MainActivity/SimpleActivity should get JavaConstructors
- 3 passing: UserActivity/FullActivity (activation-only) and CustomView

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tors

The scanner only found constructors with [Register] directly on them.
User types (e.g., MainActivity) that have a parameterless constructor
without [Register] were missing JavaConstructors, which meant no Java
constructor in the JCW and no UCO constructor wrapper.

Mirror the legacy CecilImporter behavior: walk the base type hierarchy
to collect registered ctors, then accept non-activation ctors on the
user type whose parameters are compatible with an accepted base ctor.

Algorithm (Pass 4 in CollectMarshalMethods):
1. Collect registered ctors from base type hierarchy
2. For each non-activation ctor without [Register] on the user type
3. Find a base registered ctor with compatible parameters
4. Add it as a constructor marshal method using the base registration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests:
- Consolidate 10 individual [Fact] into 5 focused tests. Each
  FindFixtureByJavaName call now has a single test with all assertions.

Scanner (CollectBaseConstructorChain):
- Remove explicit IsActivationCtor check. Activation ctors (IntPtr,
  JniHandleOwnership) will never match a base registered ctor because
  AreParametersCompatible already filters on type+count. Legacy
  CecilImporter doesn't skip them explicitly either.
- Add DoNotGenerateAcw stop in CollectBaseRegisteredCtors: stop walking
  after the first MCW base type, matching legacy CecilImporter behavior
  (CecilImporter.cs:133-134).
- Extract TryResolveBaseType helper to eliminate while(true) loop.
- Document known remaining differences from legacy CecilImporter in the
  method summary (parameterless fallback, outerType, managed params).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three changes to match legacy CecilImporter behavior exactly:

1. Decouple ctor chain from detectBaseOverrides flag
   Pass 4 (CollectBaseConstructorChain) now runs for all types including
   MCW types, matching legacy which runs constructor detection
   unconditionally. Override detection (Pass 3) remains gated.

2. Handle JniConstructorSignatureAttribute
   Java.Interop-style types use [JniConstructorSignature("()V")]
   instead of [Register(".ctor", "()V", "")]. Add handling in
   TryGetMethodRegisterInfo to convert it to a RegisterInfo with
   JniName=".ctor" and the signature from the attribute arg.
   Add JniConstructorSignatureAttribute stub and test fixture.

3. Implement parameterless base ctor fallback
   Legacy CecilImporter (line 394-397) accepts a user ctor with novel
   parameter types when any base has a registered parameterless ctor.
   Compute the JNI signature from managed parameter types using
   BuildJniCtorSignature. Add ActivityWithCustomCtor test fixture.

Update OverrideDetectionTests.MultipleOverrides_AllDetected to filter
out constructor marshal methods from the count assertion, since
FullActivity now correctly gets a ctor via the fallback path.

Known remaining generator-level differences (tracked separately):
- outerType / nested inner-class constructor handling
- Application synthetic constructor (MonoPackageManager.setContext)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The legacy side of the parity test (ScannerRunner.ExtractMethodRegistrations)
only read [Register] attributes directly from a type's own methods. This
missed overrides of registered base methods that the real legacy pipeline
(CecilImporter.CreateType → GetBaseRegisteredMethod) correctly detects.

Replace the hand-rolled attribute reader with CecilImporter.CreateType(),
filtering out interface method implementations (which the new scanner
places on the interface peer type, not the implementing type).

Results after this fix:
- ExactMarshalMethods_UserTypesFixture: PASSES (exact parity for user types)
- ExactMarshalMethods_MonoAndroid: 22 remaining MISSING (down from 9047)
  These are covariant return type overrides and overloaded method variants
  that the new scanner's AreParametersCompatible doesn't yet handle.
  The legacy GetBaseDefinition uses looser matching.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scanner:
- Set SuperArgumentsString="" on fallback-path ctors so the generator
  emits super() instead of super(p0, ...). The Java ctor calls the
  base parameterless ctor, then delegates args to nctor_N(). Matches
  legacy CecilImporter which passes superCall="" for the fallback
  (CecilImporter.cs:396) vs null for compatible-params (line 391).

Tests:
- Use Assert.Single where exactly one item is expected
- Assert specific JNI signatures and SuperArgumentsString on activation
  ctor fallback tests (UserActivity, FullActivity)
- Move count assertion before Contains in OverrideDetectionTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The legacy side of the parity test (ScannerRunner.ExtractMethodRegistrations)
only read [Register] attributes directly from a type's own methods. This
missed overrides of registered base methods that the real legacy pipeline
(CecilImporter.CreateType → GetBaseRegisteredMethod) correctly detects.

Replace the hand-rolled attribute reader with CecilImporter.CreateType()
for non-DoNotGenerateAcw class types (matching the real build pipeline).
Interface types and DoNotGenerateAcw types use direct attribute extraction
since CecilImporter doesn't process them. Filter interface method
implementations from the CecilImporter output since the new scanner
places those on the interface peer type.

Constructors are excluded from both sides of the comparison (compared
separately in ExactJavaConstructors).

Results: ExactMarshalMethods_MonoAndroid and ExactMarshalMethods_UserTypesFixture
both pass with exact parity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scanner:
- Seed all base registered ctors directly into JavaConstructors, matching
  legacy CecilImporter which adds base ctors to the wrapper during the
  reversed ctorTypes walk. This fixes Implementor types (e.g.,
  IOnNavigationListenerImplementor) that have no registered ctors
  themselves but need the base ()V ctor.
- Re-gate Pass 4 behind detectBaseOverrides. Legacy CecilImporter only
  processes ctors for types that go through CreateType (non-interface,
  non-DoNotGenerateAcw). MCW types use direct [Register] only.
- Add ManagedTypeToJniDescriptorOrNull for the fallback path. Legacy
  GetJniSignature returns null for non-Java types (System.Object,
  System.IntPtr, System.Action, etc.), rejecting ctors with such
  params. Our fallback was incorrectly accepting them via the default
  Ljava/lang/Object; mapping. Now ctors with non-primitive,
  non-string params are skipped in the fallback, matching legacy.

Integration tests:
- Replace manual [Register] attribute scanning in TypeDataBuilder with
  real CecilImporter.CreateType to get accurate legacy ctor list
  including the base ctor chain. Add ExtractDirectRegisterCtors as
  fallback for interfaces and DoNotGenerateAcw types.

Unit tests:
- Update UserActivity/FullActivity to expect single ()V base seed
  (activation ctors rejected by the fallback's null-signature check).
- Fix MixedMethods_NoDuplicates to dedupe by (JniName, JniSignature)
  since .ctor can appear multiple times with different signatures.

All 235 unit tests and 10 integration tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI enforces xUnit analyzers as errors. Replace
Assert.Single(collection.Where(predicate)) with
Assert.Single(collection, predicate).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival marked this pull request as ready for review March 13, 2026 13:02
Copilot AI review requested due to automatic review settings March 13, 2026 13:02
Copy link
Contributor

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

Updates the TrimmableTypeMap Java peer scanning pipeline to match legacy JCW behavior for (1) overrides without [Register] and (2) constructor discovery via base-ctor chaining, and expands the test suite to validate parity against the real legacy Cecil-based pipeline.

Changes:

  • Extend JavaPeerScanner to detect base-registered method/property overrides and derive Java constructors from base registered constructors (including parameterless fallback).
  • Rework integration-test legacy extraction to use CecilImporter.CreateType (instead of manual [Register] scanning) for accurate baseline comparison.
  • Add new fixtures and unit tests covering override and constructor-chain scenarios (including JniConstructorSignatureAttribute).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Adds override detection and constructor chain detection passes; recognizes JniConstructorSignatureAttribute.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs Adds new fixture types for override + ctor-chain scenarios; adjusts Activity ctor registration.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs Adds stub JniConstructorSignatureAttribute for unit tests.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs New unit tests validating override detection behavior.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs New unit tests validating constructor chain detection behavior.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs Legacy method extraction now uses CecilImporter.CreateType, with interface/MCW fallbacks and interface-impl filtering.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs Legacy ctor extraction now uses CecilImporter.CreateType, with fallback to direct attribute scanning.

…ies, key consistency

- Remove Signature! null-forgiving operator in CollectBaseConstructorChain;
  extract to local variable with null guard instead
- Stop FindBaseRegisteredMethodInfo and FindBaseRegisteredProperty recursion
  at the DoNotGenerateAcw boundary (matching CollectBaseRegisteredCtors)
- Use consistent signature-based key format in CollectBasePropertyOverrides
  (name + decoded params) to match Pass 1/2 key format
- Merge duplicate if (detectBaseOverrides) blocks into a single block
- Remove unnecessary try/catch fallback in TypeDataBuilder; let
  CecilImporter.CreateType failures propagate rather than silently
  falling back to different extraction logic
- Replace default! with default in test fixture constructors (both
  params are value types, ! is unnecessary)
- Consolidate OverrideDetectionTests: merge tests that look up the
  same fixture type into single Facts to reduce redundant lookups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Member Author

@simonrozsival simonrozsival left a comment

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Verdict: ✅ LGTM

Found 0 issues in the updated diff.

The previous round of review feedback has been fully addressed:

  • Signature! null-forgiving operators replaced with proper null guards
  • FindBaseRegisteredMethodInfo and FindBaseRegisteredProperty now stop at DoNotGenerateAcw boundary
  • Property override key format made consistent with Pass 1/2
  • default! replaced with default in test fixtures
  • Bare catch in TypeDataBuilder removed
  • Duplicate if (detectBaseOverrides) blocks merged
  • Override detection tests consolidated (14→7 Facts, all assertions preserved)

👍 The override detection and constructor chaining logic is well-structured, thoroughly documented, and correctly mirrors legacy CecilImporter behavior. The 4-pass architecture (direct [Register], property [Register], base overrides, base ctor chain) has clear separation of concerns. Test coverage is comprehensive across edge cases (deep inheritance, new-slot exclusion, property overrides, JI-style attributes, parameterless fallback).

⚠️ CI is still pending (build 1334063) — cannot confirm merge-readiness until checks pass.


Review generated by android-reviewer from review guidelines.

simonrozsival and others added 3 commits March 13, 2026 14:44
FindBaseRegisteredMethod and FindBaseRegisteredProperty now populate
DeclaringTypeName and DeclaringAssemblyName on MarshalMethodInfo so
UCO wrappers call n_* callbacks on the correct base type (e.g.,
Activity.n_OnCreate) rather than the derived user type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add deep hierarchy fixtures: BaseFragment → DerivedFragment → GrandchildFragment
  to test DeclaringTypeName resolution across ACW → ACW → MCW boundaries
- Add DialogBase with registered ctor(Context) to test same-arity parameter
  type mismatch in AreParametersCompatible
- Add ActivityWithMultiParamCtor to test multi-parameter JNI signature
  computation in BuildJniCtorSignature (string, int, bool → Ljava/lang/String;IZ)
- Strengthen DeepInheritance test to verify DeclaringTypeName == Activity
- Strengthen MultipleOverrides test comment: exact count proves non-registered
  Object virtuals (ToString/Equals/GetHashCode) are correctly excluded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Legacy GetBaseRegisteredMethod (via GetBaseDefinition) walks ALL base
types with no DoNotGenerateAcw stop — only the constructor chain has
that boundary. Remove the boundary check from FindBaseRegisteredMethodInfo
and FindBaseRegisteredProperty to match legacy behavior exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
}
}
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine ($"CecilImporter.CreateType failed for {typeDef.FullName}: {ex.Message}");
Copy link
Member

Choose a reason for hiding this comment

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

Does this code have some kind of logging infrastructure?

We would need MSBuild tasks to be able to pass in a delegate for logging, similar to:

internal static Action<TraceLevel, string> CreateTaskLogger (Task task)
{
Action<TraceLevel, string> logger = (level, value) => {
switch (level) {
case TraceLevel.Error:
task.Log.LogError (value);
break;
case TraceLevel.Warning:
task.Log.LogWarning (value);
break;
default:
task.Log.LogMessage (MessageImportance.Low, "{0}", value);
break;
}
};
return logger;
}

We can file an issue to do this in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this log is probably unnecessary going forward. I will remove it.

CecilImporter.CreateType failures should propagate — this is test
infrastructure matching legacy behavior, not extending it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants