[TrimmableTypeMap] Detect overrides and constructors in JavaPeerScanner#10932
[TrimmableTypeMap] Detect overrides and constructors in JavaPeerScanner#10932simonrozsival wants to merge 15 commits intomainfrom
Conversation
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>
There was a problem hiding this comment.
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
JavaPeerScannerto 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. |
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
Outdated
Show resolved
Hide resolved
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
Outdated
Show resolved
Hide resolved
tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs
Outdated
Show resolved
Hide resolved
…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>
simonrozsival
left a comment
There was a problem hiding this comment.
🤖 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 guardsFindBaseRegisteredMethodInfoandFindBaseRegisteredPropertynow stop atDoNotGenerateAcwboundary- Property override key format made consistent with Pass 1/2
default!replaced withdefaultin test fixtures- Bare
catchin 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).
Review generated by android-reviewer from review guidelines.
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}"); |
There was a problem hiding this comment.
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:
We can file an issue to do this in the future.
There was a problem hiding this comment.
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>
Part of #10789
Context
The
JavaPeerScannerwas missing two categories of methods that the legacyCecilImporterfinds:Method overrides without
[Register]— User types (e.g.,MainActivity) overrideActivity.OnCreate()without[Register]on the override. The attribute is only on the base class method in Mono.Android.Java constructors via base ctor chaining — User types have non-activation constructors without
[Register]. The legacyCecilImporterwalks 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_Nnative 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).FindBaseRegisteredMethodInfoandFindBaseRegisteredPropertywalk all base types with no DoNotGenerateAcw boundary — matching legacyGetBaseDefinition/GetBaseRegisteredMethodbehavior. (Only constructor chaining has the DoNotGenerateAcw boundary.)Pass 4 — Constructor chain detection: Mirrors legacy
CecilImporterbehavior:Implementortypes get the base()Vctor fromObject)()Vctor, accepts derived ctors with novel params (generatessuper()call viaSuperArgumentsString=""). Rejects ctors with non-Java parameter types (System.Object,System.IntPtr,System.Action, etc.) matching legacyGetJniSignaturenull behavior.[JniConstructorSignature](Java.Interop-style) in addition to[Register]inTryGetMethodRegisterInfoDoNotGenerateAcwboundary (matching legacy)detectBaseOverrides), matching legacy which only runsCecilImporter.CreateTypefor non-interface, non-DoNotGenerateAcw typesHelper methods:
TryResolveBaseType— encapsulates the 3-step base type resolution pattern (GetBaseTypeInfo → TryResolveType → GetTypeDefinition)CollectBaseRegisteredCtors— walks base hierarchy collecting registered ctorsManagedTypeToJniDescriptorOrNull— nullable variant ofManagedTypeToJniDescriptorfor fallback signature computation; returns null for types without a proper JNI mappingBuildJniCtorSignature— computes JNI ctor signature from managed params, returns null when params aren't Java-mappableIntegration tests
ScannerRunner.cs— RewroteExtractMethodRegistrationsto 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:CecilImporter.CreateTypefor non-interface, non-DoNotGenerateAcw typesCecilImporteroutput (new scanner places these on the interface peer type)CallableWrapperMethod.Methodstring formatExactJavaConstructors)HasDoNotGenerateAcwinternal for reuse byTypeDataBuilderTypeDataBuilder.cs— Replaced manual[Register]attribute scanning for constructors withCecilImporter.CreateTypeto get the accurate legacy constructor list including the full base ctor chain. UsesExtractDirectRegisterCtorsfor interfaces and DoNotGenerateAcw types.Test fixtures
[Register(".ctor", "()V", "")]toActivityfixture (matches real Mono.Android auto-generated bindings)JniConstructorSignatureAttributestub inStubAttributes.csUserActivity,FullActivity,DeeplyDerived,MixedMethods,NewSlotActivity,CustomException,JiStyleView,ActivityWithCustomCtor,BaseFragment,DerivedFragment,GrandchildFragment,DialogBase,CustomDialog,ActivityWithMultiParamCtorUnit tests
OverrideDetectionTests(10 tests) — overrides without[Register], deep inheritance (3 levels), ACW→ACW→MCW hierarchy, property overrides, mixed methods, new-slot exclusion, DeclaringTypeName verificationConstructorDetectionTests(9 tests) — base ctor chain, implicit default ctor, base ctor seed,JniConstructorSignature, parameterless fallback withsuper(), same-arity type mismatch, multi-parameter signature computation, direct[Register]regression guardRelated issue
Filed #10931 —
JcwJavaSourceGeneratormust skipregisterNativesin the static block for Application/Instrumentation types (the static initializer runs before the runtime ContentProvider loads the native library).Test results
ExactJavaConstructors_MonoAndroidandExactMarshalMethods_MonoAndroidwhich validate against real Mono.Android.dll with ~8000 types)Legacy comparison notes
Deep comparison with the legacy
CecilImporter/GetBaseRegisteredMethod/CallableWrapperTypepipeline:Scanner behavior (matches legacy)
GetBaseDefinition→GetBaseRegisteredMethod: walks all base types (no DoNotGenerateAcw boundary), checksVirtual || Abstract, matches by name + parameter compatibility. Only run for user ACW types (gated bydetectBaseOverrides), matching howJavaTypeScanner.ShouldSkipJavaCallableWrapperGenerationfilters DoNotGenerateAcw types.ctorTypesloop (CecilImporter.cs:124-160): stops at DoNotGenerateAcw, reverses to process base-first, seeds registered base ctors, checksAreParametersCompatibleWith, falls back to parameterlesssuper()whenbaseCtors.Any(m => !m.HasParameters).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)
mono.android.TypeManager.Activate(...), new callsnctor_N(...)(UCO dispatches toTrimmableNativeRegistration.ActivateInstance). Intentional architectural change.__md_methodsstring and callsRuntime.register(...), new callsRuntime.registerNatives(ClassName.class). Intentional architectural change.private native n_*, new emitspublic native n_*(needed for UCOregisterNativesdispatch).@Override: Legacy only emits annotations from managed custom attributes. New hard-codes@Overrideon registered method overrides (more correct for Java compiler).register()to generatedonCreate()and emitsApplicationConstructorwithMonoPackageManager.setContext(). New doesn't handle this yet — tracked in [TrimmableTypeMap] JcwJavaSourceGenerator must skip registerNatives for Application/Instrumentation types #10931.__md_N_methods. Not yet implemented in the new generator.@Activity(label=...)etc.). New generator doesn't emit these — they're handled by the AndroidManifest pipeline, not the JCW source files.