From 888146268cd60d55eaedace1b98897fc8ae16fb7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 10:50:26 +0200 Subject: [PATCH 1/7] Remove redundant trimmable typemap generator tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../RootTypeMapAssemblyGeneratorTests.cs | 8 -- .../TypeMapAssemblyGeneratorTests.cs | 73 ------------------- 2 files changed, 81 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index b609e77deb5..2a16cfea033 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -23,14 +23,6 @@ static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames static MethodDefinitionHandle FindMethodDefinition (MetadataReader reader, string methodName) => reader.MethodDefinitions.First (h => reader.GetString (reader.GetMethodDefinition (h).Name) == methodName); - [Fact] - public void Generate_ProducesValidPEAssembly () - { - using var stream = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); - using var pe = new PEReader (stream); - Assert.True (pe.HasMetadata); - } - [Theory] [InlineData (false)] [InlineData (true)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d448de126f5..3954f02ee44 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -209,28 +209,6 @@ public void Generate_ProxyType_UsesJavaPeerProxyBase () }); } - [Fact] - public void Generate_InterfaceProxyType_UsesJavaPeerProxyBase () - { - // Interface proxies derive from the `JavaPeerProxy` base (a plain TypeReference), like - // every other proxy. Interfaces have no constructors and are activated from their - // InvokerType in CreateInstance. - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var interfaceProxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .First (t => reader.GetString (t.Name) == "Android_Views_IOnClickListener_Proxy"); - - Assert.Equal (HandleKind.TypeReference, interfaceProxy.BaseType.Kind); - var baseTypeRef = reader.GetTypeReference ((TypeReferenceHandle) interfaceProxy.BaseType); - Assert.Equal ("Java.Interop", reader.GetString (baseTypeRef.Namespace)); - Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name)); - } - // Regression test: decode the emitted proxy `.ctor` *body* (not just member presence) // and verify it chains to the correct base constructor. Every proxy derives from // `JavaPeerProxy` and calls the two-arg base ctor `(string, Type)` — pushing the target @@ -947,27 +925,6 @@ public void Generate_AcwProxy_RegisterNativesUsesComputedMaxStack () Assert.InRange (body.MaxStack, 5, 16); } - [Fact] - public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () - { - var peers = ScanFixtures (); - var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity"); - - using var stream = GenerateAssembly (new [] { acwPeer }, "UcoTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("UnmanagedCallersOnlyAttribute", typeNames); - - // Verify UCO wrapper methods exist — they should have names like n__uco_ - var methodDefs = reader.MethodDefinitions - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - Assert.Contains (methodDefs, name => name.Contains ("_uco_")); - } - [Theory] [InlineData (1, 0x05)] // Boolean → byte (unsigned) for JNI ABI [InlineData (2, 0x04)] // Byte → sbyte @@ -2380,36 +2337,6 @@ public void TrackedInstructionEncoder_UnconditionalBranchIsUnsupported () Assert.Throws (() => encoder.Branch (ILOpCode.Br_s, label)); } - [Fact] - public void Generate_ProxyTypes_HaveSelfAppliedAttribute () - { - var peers = ScanFixtures (); - var activityPeer = peers.First (p => p.JavaName == "android/app/Activity"); - - using var stream = GenerateAssembly (new [] { activityPeer }, "SelfApply"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var proxyTypeDef = reader.TypeDefinitions - .First (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Proxies"); - - // The proxy type should have a custom attribute applied to itself (self-application) - var attrs = reader.GetCustomAttributes (proxyTypeDef); - Assert.NotEmpty (attrs); - - // Verify the attribute's constructor is a MethodDef (i.e., defined in this assembly, - // meaning it's the proxy's own .ctor — self-application) - bool hasSelfApplied = false; - foreach (var attrHandle in attrs) { - var attr = reader.GetCustomAttribute (attrHandle); - if (attr.Constructor.Kind == HandleKind.MethodDefinition) { - hasSelfApplied = true; - break; - } - } - Assert.True (hasSelfApplied, "Proxy type should have a self-applied attribute (ctor is MethodDefinition)"); - } - [Fact] public void Generate_UcoConstructor_Parameterless_InvokesUserVisibleCtorViaActivationPeerReference () { From 76f2c52e2a955aa7b661f6175efb7f373cdfe29c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 10:55:08 +0200 Subject: [PATCH 2/7] Rely on runtime tests for typemap constructors The on-device constructor activation suite covers parameterless, primitive, object, array, and mixed Java-to-managed constructor dispatch under both trimmable runtimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../TypeMapAssemblyGeneratorTests.cs | 441 ------------------ 1 file changed, 441 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 3954f02ee44..c41ff84f770 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -2098,105 +2098,6 @@ public void Generate_UcoConstructor_HasMarshalMethodMetadataAndExceptionRegions Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Finally); } - [Fact] - public void Generate_UcoConstructor_ParameterizedPrimitiveCtorCallsManagedConstructor () - { - string jniSignature = "(ZBCSIJFD)V"; - var managedTypes = new [] { - "System.Boolean", - "System.SByte", - "System.Char", - "System.Int16", - "System.Int32", - "System.Int64", - "System.Single", - "System.Double", - }; - var managedTypeRefs = managedTypes.Select (TypeRef).ToArray (); - var peer = MakeAcwPeer ("test/PrimitiveCtorArgs", "Test.PrimitiveCtorArgs", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = jniSignature, - ManagedParameterTypes = managedTypeRefs, - HasMatchingManagedCtor = true, - }, - }, - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = jniSignature, - ManagedMethodName = ".ctor", - IsConstructor = true, - ManagedParameterTypes = managedTypeRefs, - }, - }, - }; - - using var stream = GenerateAssembly (new [] { peer }, "ParameterizedPrimitiveCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "PrimitiveCtorArgs", managedTypes)); - - var nctorMethod = reader.GetMethodDefinition (FindNctorUcoMethod (reader)); - var nctorSignature = nctorMethod.DecodeSignature (SignatureTypeProvider.Instance, null); - Assert.Equal ( - new [] { "System.IntPtr", "System.IntPtr", "System.Byte", "System.SByte", "System.UInt16", "System.Int16", "System.Int32", "System.Int64", "System.Single", "System.Double" }, - nctorSignature.ParameterTypes); - } - - [Fact] - public void Generate_UcoConstructor_ParameterizedObjectCtorUsesExplicitMarshalHelpers () - { - string jniSignature = "(Ljava/lang/String;[I[Ljava/lang/String;Landroid/content/Context;)V"; - var managedTypes = new [] { - "System.String", - "System.Int32[]", - "System.String[]", - "Android.Content.Context", - }; - var managedTypeRefs = managedTypes.Select (TypeRef).ToArray (); - var peer = MakeAcwPeer ("test/ObjectCtorArgs", "Test.ObjectCtorArgs", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = jniSignature, - ManagedParameterTypes = managedTypeRefs, - HasMatchingManagedCtor = true, - }, - }, - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = jniSignature, - ManagedMethodName = ".ctor", - IsConstructor = true, - ManagedParameterTypes = managedTypeRefs, - }, - }, - }; - - using var stream = GenerateAssembly (new [] { peer }, "ParameterizedObjectCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "ObjectCtorArgs", managedTypes)); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("GetString", memberNames); - Assert.Contains ("GetArray", memberNames); - Assert.Contains ("GetObject", memberNames); - var ilBytes = GetNctorUcoIL (pe, reader); - var fromJniHandleTokens = AllMemberRefHandles (reader) - .Where (h => reader.GetString (reader.GetMemberReference (h).Name) == "FromJniHandle") - .Select (h => MetadataTokens.GetToken (h)) - .ToList (); - Assert.DoesNotContain (fromJniHandleTokens, t => ILContainsCallToken (ilBytes, t)); - } - [Fact] public void Generate_UcoConstructor_JiStyle_HasExceptionRegions () { @@ -2337,281 +2238,6 @@ public void TrackedInstructionEncoder_UnconditionalBranchIsUnsupported () Assert.Throws (() => encoder.Branch (ILOpCode.Br_s, label)); } - [Fact] - public void Generate_UcoConstructor_Parameterless_InvokesUserVisibleCtorViaActivationPeerReference () - { - // Regression test for ContainsExportedMethods (JnienvTest.ActivatedDirectObjectSubclassesShouldBeRegistered): - // for the parameterless `()V` UCO constructor wrapper, the emitter must mirror - // TypeManager.Activate (Mono.Android/Java.Interop/TypeManager.cs): - // - // 1. RuntimeHelpers.GetUninitializedObject(typeof(T)) - // 2. JavaPeerProxy.SetActivationPeerReference(obj, self) - // 3. obj..ctor() // user-visible parameterless ctor - // - // The legacy implementation called the inherited activation ctor `(IntPtr, - // JniHandleOwnership)` instead, so user-visible ctor bodies (e.g. `Constructed = true`) - // never ran when the peer was created from the Java side. - var peer = MakeAcwPeer ("test/UcoCtorPeer", "Test.UcoCtorPeer", "TestAsm"); - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorParameterlessTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // SetActivationPeerReference member ref must exist. - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("SetActivationPeerReference", memberNames); - Assert.Contains ("GetUninitializedObject", memberNames); - - var nctorMethodHandle = FindNctorUcoMethod (reader); - Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); - - var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); - var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); - Assert.NotNull (body); - var ilBytes = body.GetILBytes (); - Assert.NotNull (ilBytes); - - var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => MetadataTokens.MemberReferenceHandle (i)) - .ToList (); - - // 1. The body must call SetActivationPeerReference (the new behavior). - var setPeerHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "SetActivationPeerReference"); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (setPeerHandle)), - "nctor_*_uco IL should call JavaPeerProxy.SetActivationPeerReference for parameterless ctor"); - - // 2. The body must call GetUninitializedObject (no `newobj` of the activation ctor). - var getUninitHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetUninitializedObject"); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getUninitHandle)), - "nctor_*_uco IL should call RuntimeHelpers.GetUninitializedObject for parameterless ctor"); - - // 3. The body must call the user-visible parameterless ctor on the target type — and - // NOT the (IntPtr, JniHandleOwnership) activation ctor. We disambiguate by signature. - var targetCtorRefs = memberRefHandles - .Where (h => { - var mref = reader.GetMemberReference (h); - if (reader.GetString (mref.Name) != ".ctor") - return false; - if (mref.Parent.Kind != HandleKind.TypeReference) - return false; - var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); - return reader.GetString (typeRef.Name) == "UcoCtorPeer"; - }) - .ToList (); - Assert.NotEmpty (targetCtorRefs); - - var ctorSigDecoder = new MethodSignatureDecoder (); - MemberReferenceHandle? userCtorHandle = null; - MemberReferenceHandle? activationCtorHandle = null; - foreach (var h in targetCtorRefs) { - var mref = reader.GetMemberReference (h); - int paramCount = mref.DecodeMethodSignature (ctorSigDecoder, genericContext: null).RequiredParameterCount; - if (paramCount == 0) userCtorHandle = h; - else if (paramCount == 2) activationCtorHandle = h; - } - - Assert.NotNull (userCtorHandle); - var resolvedUserCtorHandle = userCtorHandle.Value; - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (resolvedUserCtorHandle)), - "nctor_*_uco IL should call the user-visible parameterless ctor on the target type"); - if (activationCtorHandle.HasValue) { - Assert.False (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (activationCtorHandle.Value)), - "nctor_*_uco IL should NOT call the (IntPtr, JniHandleOwnership) activation ctor for parameterless `()V`"); - } - } - - [Fact] - public void Generate_UcoConstructor_ObjectRefParam_MarshalsViaJavaLangObjectGetObject () - { - // (Ljava/lang/Throwable;)V — verifies ref-arg marshalling delegates to - // Java.Lang.Object.GetObject (jniHandle, DoNotTransfer, paramType) - // and that the user-visible (Throwable) ctor is invoked. - var paramType = new TypeRefData { ManagedTypeName = "Java.Lang.Throwable", AssemblyName = "Mono.Android" }; - var peer = MakeAcwPeer ("test/UcoCtorObjArg", "Test.UcoCtorObjArg", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = "(Ljava/lang/Throwable;)V", - HasMatchingManagedCtor = true, - ManagedParameterTypes = new [] { paramType }, - }, - }, - }; - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorObjArg"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - Assert.Contains ("GetObject", GetMemberRefNames (reader)); - - var ilBytes = GetNctorUcoIL (pe, reader); - var memberRefHandles = AllMemberRefHandles (reader); - - var getObjectHandles = memberRefHandles - .Where (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject") - .ToList (); - Assert.Contains (getObjectHandles, h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))); - Assert.True (getObjectHandles.Count > 0, - "nctor_*_uco IL should call Java.Lang.Object.GetObject for an object-ref ctor arg"); - - var userCtor = FindUserCtorRefByFirstParam (reader, "UcoCtorObjArg", paramCount: 1, firstParamTypeName: "Java.Lang.Throwable"); - Assert.NotNull (userCtor); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), - "nctor_*_uco IL should call the user-visible (Throwable) ctor"); - } - - [Fact] - public void Generate_UcoConstructor_PrimitiveIntParam_LoadsArgDirectly () - { - // (I)V — verifies primitive int args are loaded directly without GetObject. - var paramType = new TypeRefData { ManagedTypeName = "System.Int32", AssemblyName = "System.Runtime" }; - var peer = MakeAcwPeer ("test/UcoCtorIntArg", "Test.UcoCtorIntArg", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = "(I)V", - HasMatchingManagedCtor = true, - ManagedParameterTypes = new [] { paramType }, - }, - }, - }; - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorIntArg"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var ilBytes = GetNctorUcoIL (pe, reader); - - // The IL must NOT call GetObject — primitive int is loaded directly via Ldarg. - var memberRefHandles = AllMemberRefHandles (reader); - var getObjectHandle = memberRefHandles.FirstOrDefault (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject"); - if (!getObjectHandle.IsNil) { - Assert.False (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getObjectHandle)), - "nctor_*_uco IL should NOT call GetObject for a primitive int ctor arg"); - } - - var userCtor = FindUserCtorRef (reader, "UcoCtorIntArg", new [] { "System.Int32" }); - Assert.NotNull (userCtor); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), - "nctor_*_uco IL should call the user-visible (int) ctor"); - } - - [Fact] - public void Generate_UcoConstructor_BooleanParam_EmitsByteToBoolConversion () - { - // (Z)V — verifies byte→bool conversion (Ldc.I4.0 + Cgt.Un) is emitted for - // System.Boolean params, matching ExportMethodDispatchEmitter's primitive marshalling. - var paramType = new TypeRefData { ManagedTypeName = "System.Boolean", AssemblyName = "System.Runtime" }; - var peer = MakeAcwPeer ("test/UcoCtorBoolArg", "Test.UcoCtorBoolArg", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = "(Z)V", - HasMatchingManagedCtor = true, - ManagedParameterTypes = new [] { paramType }, - }, - }, - }; - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorBoolArg"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var ilBytes = GetNctorUcoIL (pe, reader); - - // Look for the bool conversion sequence: Ldc_I4_0 (0x16) ; Cgt_Un (0xFE 0x03) - bool foundBoolConv = false; - for (int i = 0; i < ilBytes.Length - 2; i++) { - if (ilBytes [i] == 0x16 && ilBytes [i + 1] == 0xFE && ilBytes [i + 2] == 0x03) { - foundBoolConv = true; - break; - } - } - Assert.True (foundBoolConv, "nctor_*_uco IL should emit Ldc.I4.0 + Cgt.Un to convert byte→bool for Boolean ctor arg"); - - var userCtor = FindUserCtorRef (reader, "UcoCtorBoolArg", new [] { "System.Boolean" }); - Assert.NotNull (userCtor); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), - "nctor_*_uco IL should call the user-visible (bool) ctor"); - } - - [Fact] - public void Generate_UcoConstructor_StringParam_MarshalsViaJniEnvGetString () - { - // (Ljava/lang/String;)V — verifies String args marshal via JNIEnv.GetString. - var paramType = new TypeRefData { ManagedTypeName = "System.String", AssemblyName = "System.Runtime" }; - var peer = MakeAcwPeer ("test/UcoCtorStrArg", "Test.UcoCtorStrArg", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = "(Ljava/lang/String;)V", - HasMatchingManagedCtor = true, - ManagedParameterTypes = new [] { paramType }, - }, - }, - }; - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorStrArg"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var ilBytes = GetNctorUcoIL (pe, reader); - var memberRefHandles = AllMemberRefHandles (reader); - - // JNIEnv.GetString member ref must be present and called. - var getStringHandles = memberRefHandles.Where (h => { - var mref = reader.GetMemberReference (h); - if (reader.GetString (mref.Name) != "GetString") - return false; - if (mref.Parent.Kind != HandleKind.TypeReference) - return false; - var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); - return reader.GetString (typeRef.Name) == "JNIEnv"; - }).ToList (); - Assert.NotEmpty (getStringHandles); - Assert.Contains (getStringHandles, h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))); - - var userCtor = FindUserCtorRef (reader, "UcoCtorStrArg", new [] { "System.String" }); - Assert.NotNull (userCtor); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), - "nctor_*_uco IL should call the user-visible (string) ctor"); - } - - [Fact] - public void Generate_UcoConstructor_MixedSignature_MarshalsBothPrimitiveAndObjectArgs () - { - // (ILjava/lang/Throwable;)V — verifies int passes through and Throwable goes via GetObject. - var intParam = new TypeRefData { ManagedTypeName = "System.Int32", AssemblyName = "System.Runtime" }; - var throwableParam = new TypeRefData { ManagedTypeName = "Java.Lang.Throwable", AssemblyName = "Mono.Android" }; - var peer = MakeAcwPeer ("test/UcoCtorMixed", "Test.UcoCtorMixed", "TestAsm") with { - JavaConstructors = new List { - new JavaConstructorInfo { - ConstructorIndex = 0, - JniSignature = "(ILjava/lang/Throwable;)V", - HasMatchingManagedCtor = true, - ManagedParameterTypes = new [] { intParam, throwableParam }, - }, - }, - }; - using var stream = GenerateAssembly (new [] { peer }, "UcoCtorMixed"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("GetObject", memberNames); - - var ilBytes = GetNctorUcoIL (pe, reader); - var memberRefHandles = AllMemberRefHandles (reader); - var getObjectHandles = memberRefHandles - .Where (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject") - .ToList (); - Assert.Contains (getObjectHandles, h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))); - Assert.True (getObjectHandles.Count > 0, - "nctor_*_uco IL should call GetObject for the Throwable arg in the mixed signature"); - - // User ctor: (Int32, Java.Lang.Throwable). Need a signature-discriminated lookup - // because the activation ctor (IntPtr, JniHandleOwnership) also has 2 params. - var userCtor = FindUserCtorRefByFirstParam (reader, "UcoCtorMixed", paramCount: 2, firstParamTypeName: "System.Int32"); - Assert.NotNull (userCtor); - Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), - "nctor_*_uco IL should call the user-visible (int, Throwable) ctor"); - } - static byte[] GetNctorUcoIL (PEReader pe, MetadataReader reader) { var nctorMethodHandle = FindNctorUcoMethod (reader); @@ -2647,55 +2273,6 @@ static List AllMemberRefHandles (MetadataReader reader) = .Select (i => MetadataTokens.MemberReferenceHandle (i)) .ToList (); - static MemberReferenceHandle? FindUserCtorRef (MetadataReader reader, string typeShortName, IReadOnlyList paramTypeNames) - { - var decoder = new TypeNameSignatureDecoder (reader); - foreach (var h in AllMemberRefHandles (reader)) { - var mref = reader.GetMemberReference (h); - if (reader.GetString (mref.Name) != ".ctor") - continue; - if (mref.Parent.Kind != HandleKind.TypeReference) - continue; - var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); - if (reader.GetString (typeRef.Name) != typeShortName) - continue; - var sig = mref.DecodeMethodSignature (decoder, genericContext: null); - if (sig.RequiredParameterCount != paramTypeNames.Count) - continue; - bool match = true; - for (int i = 0; i < paramTypeNames.Count; i++) { - if (sig.ParameterTypes [i] != paramTypeNames [i]) { - match = false; - break; - } - } - if (match) - return h; - } - return null; - } - - static MemberReferenceHandle? FindUserCtorRefByFirstParam (MetadataReader reader, string typeShortName, int paramCount, string firstParamTypeName) - { - var decoder = new TypeNameSignatureDecoder (reader); - foreach (var h in AllMemberRefHandles (reader)) { - var mref = reader.GetMemberReference (h); - if (reader.GetString (mref.Name) != ".ctor") - continue; - if (mref.Parent.Kind != HandleKind.TypeReference) - continue; - var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); - if (reader.GetString (typeRef.Name) != typeShortName) - continue; - var sig = mref.DecodeMethodSignature (decoder, genericContext: null); - if (sig.RequiredParameterCount != paramCount) - continue; - if (sig.ParameterTypes [0] == firstParamTypeName) - return h; - } - return null; - } - // SignatureTypeProvider returning a stringified type name for primitives and typerefs. sealed class TypeNameSignatureDecoder : ISignatureTypeProvider { @@ -2723,22 +2300,4 @@ public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle h public string GetPointerType (string elementType) => elementType + "*"; } - // Minimal SignatureTypeProvider used only to count required parameters of a member ref. - sealed class MethodSignatureDecoder : ISignatureTypeProvider - { - public int GetArrayType (int elementType, ArrayShape shape) => 0; - public int GetByReferenceType (int elementType) => 0; - public int GetFunctionPointerType (MethodSignature signature) => 0; - public int GetGenericInstantiation (int genericType, ImmutableArray typeArguments) => 0; - public int GetGenericMethodParameter (object? genericContext, int index) => 0; - public int GetGenericTypeParameter (object? genericContext, int index) => 0; - public int GetModifiedType (int modifier, int unmodifiedType, bool isRequired) => 0; - public int GetPinnedType (int elementType) => 0; - public int GetPointerType (int elementType) => 0; - public int GetPrimitiveType (PrimitiveTypeCode typeCode) => 0; - public int GetSZArrayType (int elementType) => 0; - public int GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => 0; - public int GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => 0; - public int GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => 0; - } } From cc13e14d3c3a034f02233851d9bcbb80b0b24688 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 11:09:44 +0200 Subject: [PATCH 3/7] Test trimmable typemap callbacks at runtime Exercise modern boolean callback returns and constructed generic base callbacks through Java and JNI instead of inspecting emitted member references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../TypeMapAssemblyGeneratorTests.cs | 52 ------------------- .../Android.Widget/AdapterTests.cs | 8 ++- .../TrimmableTypeMapRuntimeCoverageTests.cs | 28 +++++++++- 3 files changed, 34 insertions(+), 54 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index c41ff84f770..0ad6dee9f24 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -117,31 +117,6 @@ public void Generate_CreatesProxyTypes () Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); } - [Theory] - [InlineData ("my/app/GenericSelectableList")] - [InlineData ("my/app/GenericForwardingSelectableList")] - public void Generate_InheritedGenericBaseCallback_UsesConstructedBaseMemberRef (string javaName) - { - var peer = ScanFixtures ().Single (p => p.JavaName == javaName); - using var stream = GenerateAssembly (new [] { peer }); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var member = reader.MemberReferences - .Select (h => reader.GetMemberReference (h)) - .Single (m => reader.GetString (m.Name) == "n_SetSelection_I"); - - Assert.Equal (HandleKind.TypeSpecification, member.Parent.Kind); - var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) member.Parent); - var blob = reader.GetBlobReader (typeSpec.Signature); - - Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST - Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS - Assert.Equal ("GenericSelectionHost`1", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); - Assert.Equal (1, blob.ReadCompressedInteger ()); - Assert.Equal (0x0E, blob.ReadByte ()); // ELEMENT_TYPE_STRING - } - [Fact] public void Generate_InheritedGenericBaseCallback_UsesValueTypeGenericArgument () { @@ -1020,7 +995,6 @@ public void Generate_UcoMethod_BooleanReturn_WrapperUsesByte_CallbackUsesBoolean // The callback MemberRef must mirror the real n_* method's signature. The TouchHandler fixture // models a *pre-#1296* binding whose n_OnTouch declares JNI boolean as System.Boolean, so the // emitted ref must be Boolean — while the UCO entry keeps the blittable byte for the JNI ABI. - // (The modern sbyte counterpart is covered by Generate_UcoMethod_BooleanReturn_ModernBinding_*.) var peer = MakeTouchHandlerCallbackDispatchPeer (); using var stream = GenerateAssembly (new [] { peer }, "BoolReturnTest"); using var pe = new PEReader (stream); @@ -1066,32 +1040,6 @@ public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesBoolean Assert.Equal ("System.Boolean", callbackSig.ParameterTypes.Last ()); } - [Fact] - public void Generate_UcoMethod_BooleanReturn_ModernBinding_CallbackUsesSByte () - { - // Counterpart to the pre-#1296 TouchHandler tests: the IOnLongClickListenerInvoker fixture - // models a *post-#1296* binding whose n_OnLongClick declares JNI boolean as the blittable - // sbyte. ImplicitMultiListener inherits that interface callback (no own [Register]) and thus - // forwards to the invoker's n_*, so the emitted callback MemberRef must be SByte — proving the - // boolean encoding is taken from each real n_* method rather than hardcoded to one form. - var peers = ScanFixtures (); - var peer = peers.First (p => p.JavaName == "my/app/ImplicitMultiListener"); - using var stream = GenerateAssembly (new [] { peer }, "ModernBoolReturnTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var ucoMethod = reader.MethodDefinitions - .Select (h => reader.GetMethodDefinition (h)) - .First (m => reader.GetString (m.Name).Contains ("onLongClick") && - reader.GetString (m.Name).Contains ("_uco_")); - var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null); - Assert.Equal ("System.Byte", ucoSig.ReturnType); - - var callbackRefHandle = FindCallbackMemberRefHandle (reader, "n_OnLongClick_Landroid_view_View_", "Android.Views", "IOnLongClickListenerInvoker"); - var callbackSig = reader.GetMemberReference (callbackRefHandle).DecodeMethodSignature (SignatureTypeProvider.Instance, null); - Assert.Equal ("System.SByte", callbackSig.ReturnType); - } - [Fact] public void Generate_MixedBindings_EachCallbackMirrorsItsOwnNativeSignature () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/AdapterTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/AdapterTests.cs index d4b74983f3d..b19480f24ca 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/AdapterTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/AdapterTests.cs @@ -27,6 +27,7 @@ public void InvokeOverriddenAbsListView_AdapterProperty () IntPtr grefAdapterView_class = JNIEnv.FindClass ("android/widget/AdapterView"); IntPtr AdapterView_getAdapter = JNIEnv.GetMethodID (grefAdapterView_class, "getAdapter", "()Landroid/widget/Adapter;"); IntPtr AdapterView_setAdapter = JNIEnv.GetMethodID (grefAdapterView_class, "setAdapter", "(Landroid/widget/Adapter;)V"); + IntPtr AdapterView_setSelection = JNIEnv.GetMethodID (grefAdapterView_class, "setSelection", "(I)V"); JNIEnv.DeleteGlobalRef (grefAbsListView_class); JNIEnv.DeleteGlobalRef (grefAdapterView_class); @@ -45,6 +46,9 @@ public void InvokeOverriddenAbsListView_AdapterProperty () adapter.AdapterSetterInvoked = false; JNIEnv.CallVoidMethod (adapter.Handle, AdapterView_setAdapter, new JValue (IntPtr.Zero)); Assert.IsTrue (adapter.AdapterSetterInvoked); + + JNIEnv.CallVoidMethod (adapter.Handle, AdapterView_setSelection, new JValue (23)); + Assert.AreEqual (23, adapter.SelectionPosition); } } @@ -77,6 +81,8 @@ protected override void Dispose (bool disposing) public bool AdapterSetterInvoked; + public int SelectionPosition; + public override IListAdapter Adapter { get {return AdapterValue;} set { @@ -86,7 +92,7 @@ public override IListAdapter Adapter { public override void SetSelection (int position) { - throw new NotImplementedException(); + SelectionPosition = position; } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index bfd4175bab4..bf2a29d5bfd 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -57,6 +57,23 @@ public void JavaToManagedClickCallback_MarshalsObjectParameter () Assert.AreEqual (view.Handle, listener.ViewHandle); } + [Test] + public void JavaToManagedLongClickCallback_MarshalsBooleanReturn () + { + AssumeTrimmableTypeMapEnabled (); + TrimmableRuntimeClickListener.Reset (); + + using var listener = new TrimmableRuntimeClickListener (); + using var view = new View (Android.App.Application.Context); + + var method = JNIEnv.GetMethodID (listener.Class.Handle, "onLongClick", "(Landroid/view/View;)Z"); + var handled = JNIEnv.CallBooleanMethod (listener.Handle, method, new JValue (view.Handle)); + + Assert.IsTrue (handled); + Assert.AreEqual (1, TrimmableRuntimeClickListener.OnLongClickInvocations); + Assert.AreEqual (view.Handle, listener.ViewHandle); + } + [Test] public void JavaToManagedInvocationHandlerCallback_MarshalsObjectArrayParameter () { @@ -259,9 +276,10 @@ public static void Reset () } } - class TrimmableRuntimeClickListener : Java.Lang.Object, View.IOnClickListener + class TrimmableRuntimeClickListener : Java.Lang.Object, View.IOnClickListener, View.IOnLongClickListener { public static int OnClickInvocations; + public static int OnLongClickInvocations; public IntPtr ViewHandle; @@ -271,9 +289,17 @@ public void OnClick (View v) ViewHandle = v.Handle; } + public bool OnLongClick (View v) + { + OnLongClickInvocations++; + ViewHandle = v.Handle; + return true; + } + public static void Reset () { OnClickInvocations = 0; + OnLongClickInvocations = 0; } } From 259e11add4de5b54274a5f61a0685ac6fcbe73b5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 11:20:42 +0200 Subject: [PATCH 4/7] Test trimmable exports through JNI Cover static exports and primitive and object ExportField initialization at runtime, and rely on existing device tests for instance export dispatch and exception routing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../Generator/ExportFieldTests.cs | 21 +---- .../TypeMapAssemblyGeneratorTests.cs | 84 ------------------- .../Java.Interop/ExportTests.cs | 65 ++++++++++++-- 3 files changed, 58 insertions(+), 112 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 3c61b9cc765..6e8d30087ce 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -1,13 +1,10 @@ -using System.IO; using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; /// -/// Tests for [ExportField] support: the scanner must detect [ExportField] attributes -/// and the JCW generator must emit Java field declarations initialized by calling -/// the annotated method. +/// Tests for the [ExportField] scanner and model contracts. /// public class ExportFieldTests : FixtureTestBase { @@ -39,20 +36,4 @@ public void Scanner_ExportFieldMethod_HasExportConnectorAndFlag () Assert.True (getValue.IsExport); Assert.Equal ("public", getValue.JavaAccess); } - - [Fact] - public void JcwGenerator_EmitsFieldDeclarationsAndMethodWrappers () - { - var peer = FindFixtureByJavaName ("my/app/ExportFieldExample"); - var generator = new JcwJavaSourceGenerator (); - using var writer = new StringWriter (); - generator.Generate (peer, writer); - var java = writer.ToString (); - - Assert.Contains ("public static", java); - Assert.Contains ("STATIC_INSTANCE = GetInstance ();", java); - Assert.Contains ("VALUE = GetValue ();", java); - Assert.Contains ("GetValue ()", java); - Assert.Contains ("n_GetValue", java); - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 0ad6dee9f24..f1e4d42a051 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1181,27 +1181,6 @@ public void Generate_UcoMethod_Char_ModernBinding_CallbackUsesUInt16 () Assert.Equal ("System.UInt16", callbackSig.ParameterTypes.Last ()); } - [Fact] - public void Generate_ExportUcoMethod_HasCatchAndFinallyRegions () - { - var peer = FindFixtureByJavaName ("my/app/ExportExample"); - using var stream = GenerateAssembly (new [] { peer }, "UcoLegacyWrapperShape"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var ucoMethodHandle = reader.MethodDefinitions - .First (h => { - var method = reader.GetMethodDefinition (h); - var name = reader.GetString (method.Name); - return name.Contains ("myExportedMethod") && name.Contains ("_uco_"); - }); - var ucoMethod = reader.GetMethodDefinition (ucoMethodHandle); - var body = pe.GetMethodBody (ucoMethod.RelativeVirtualAddress); - Assert.NotNull (body); - Assert.Contains (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Catch); - Assert.Contains (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Finally); - } - [Fact] public void Generate_UcoMethod_UsesDefaultUnmanagedCallersOnlyAttribute () { @@ -1647,69 +1626,6 @@ public void Generate_AcwProxy_HasPrivateImplementationDetails () Assert.Contains ("", typeDefNames); } - [Fact] - public void Generate_ExportProxy_CallsManagedMethodDirectly () - { - var peers = ScanFixtures (); - var exportPeer = peers.First (p => p.JavaName == "my/app/ExportExample"); - - using var stream = GenerateAssembly (new [] { exportPeer }, "ExportDispatch"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("MyExportedMethod", memberNames); - Assert.DoesNotContain ("n_MyExportedMethod", memberNames); - } - - [Fact] - public void Generate_StaticExportProxy_CallsManagedMethodDirectly () - { - var peers = ScanFixtures (); - var exportPeer = peers.First (p => p.JavaName == "my/app/StaticExportExample"); - - using var stream = GenerateAssembly (new [] { exportPeer }, "StaticExportDispatch"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("ComputeLabel", memberNames); - Assert.DoesNotContain ("n_ComputeLabel", memberNames); - } - - [Fact] - public void Generate_ExportProxy_UsesStaticMarshallingHelpers () - { - var peers = ScanFixtures (); - var exportPeer = peers.First (p => p.JavaName == "my/app/ExportWithJavaBoundParams"); - - using var stream = GenerateAssembly (new [] { exportPeer }, "ExportMarshalling"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("GetObject", memberNames); - Assert.Contains ("NewString", memberNames); - Assert.Contains ("HandleClick", memberNames); - Assert.Contains ("ProcessView", memberNames); - Assert.Contains ("GetViewName", memberNames); - } - - [Fact] - public void Generate_ExportFieldProxy_UsesToLocalJniHandleForObjectReturn () - { - var peers = ScanFixtures (); - var exportFieldPeer = peers.First (p => p.JavaName == "my/app/ExportFieldExample"); - - using var stream = GenerateAssembly (new [] { exportFieldPeer }, "ExportFieldDispatch"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var memberNames = GetMemberRefNames (reader); - Assert.Contains ("ToLocalJniHandle", memberNames); - Assert.Contains ("GetInstance", memberNames); - } - [Fact] public void Generate_ExportProxy_SupportsArrayAndLegacyMarshallerHelpers () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index be1ee4b06f5..cead5fa6b4e 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -259,17 +259,51 @@ static void AssumeTrimmableExportExceptionRouting () } } + // --------------------------------------------------------------- + // Group C — static [Export] dispatch + // --------------------------------------------------------------- + + [Test, Category ("Export")] + public void Export_StaticMethod_Primitive_RoundTrip () + { + using var klass = Java.Lang.Class.FromType (typeof (ExportStatic)); + var m = JNIEnv.GetStaticMethodID (klass.Handle, "Add", "(II)I"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI static method id for Add not found"); + int result = JNIEnv.CallStaticIntMethod (klass.Handle, m, new JValue (20), new JValue (22)); + Assert.AreEqual (42, result); + } + // --------------------------------------------------------------- // Group D — [ExportField] runtime visibility from Java // --------------------------------------------------------------- - // NOTE: device-level [ExportField] tests are deferred. The JCW - // generator (legacy and trimmable) currently emits a static field - // initializer that calls the [ExportField] method as a non-static - // member (`public static int FOO = InitialFoo();`), which fails - // javac when the C# method is `static`, and is unreachable at - // runtime when the C# method is an instance member because there - // is no peer instance during class init. Add runtime [ExportField] - // coverage once the JCW emitter handles both shapes correctly. + + [Test, Category ("Export")] + public void ExportField_StaticPrimitive_InitializerVisibleFromJava () + { + using var klass = Java.Lang.Class.FromType (typeof (ExportStaticField)); + var field = JNIEnv.GetStaticFieldID (klass.Handle, "ANSWER", "I"); + Assert.AreNotEqual (IntPtr.Zero, field, "JNI static field id for ANSWER not found"); + Assert.AreEqual (42, JNIEnv.GetStaticIntField (klass.Handle, field)); + } + + [Test, Category ("Export")] + public void ExportField_StaticObject_InitializerVisibleFromJava () + { + using var klass = Java.Lang.Class.FromType (typeof (ExportStaticField)); + var field = JNIEnv.GetStaticFieldID (klass.Handle, "OBJECT_ANSWER", "Ljava/lang/Integer;"); + Assert.AreNotEqual (IntPtr.Zero, field, "JNI static field id for OBJECT_ANSWER not found"); + + IntPtr valueHandle = JNIEnv.GetStaticObjectField (klass.Handle, field); + Assert.AreNotEqual (IntPtr.Zero, valueHandle, "OBJECT_ANSWER should contain a Java peer"); + var value = Java.Lang.Object.GetObject (valueHandle, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (value, "OBJECT_ANSWER should wrap as java.lang.Integer"); + if (value is null) { + throw new InvalidOperationException ("OBJECT_ANSWER should wrap as java.lang.Integer"); + } + using (value) { + Assert.AreEqual (43, value.IntValue ()); + } + } } // --------------------------------------------------------------- @@ -291,6 +325,21 @@ class ExportPrimitiveBool : Java.Lang.Object public bool EchoBool (bool x) => !x; } + class ExportStatic : Java.Lang.Object + { + [Export] + public static int Add (int x, int y) => x + y; + } + + class ExportStaticField : Java.Lang.Object + { + [ExportField ("ANSWER")] + public static int GetAnswer () => 42; + + [ExportField ("OBJECT_ANSWER")] + public static Java.Lang.Integer GetObjectAnswer () => new Java.Lang.Integer (43); + } + class ExportString : Java.Lang.Object { [Export] From daaa380a893b4da5b3e03ba2de1f4df0b49f51b5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 11:35:10 +0200 Subject: [PATCH 5/7] Test typemap proxy resolution at runtime Exercise generated alias groups with Java-created ArrayList handles and verify Java.Interop-style activation through a Java-owned peer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../TypeMapAssemblyGeneratorTests.cs | 315 ------------------ .../TrimmableTypeMapRuntimeCoverageTests.cs | 69 +++- .../test/TrimmableRuntimeJavaInteropPeer.java | 5 + 3 files changed, 64 insertions(+), 325 deletions(-) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/TrimmableRuntimeJavaInteropPeer.java diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index f1e4d42a051..41ab0669675 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; @@ -42,9 +41,6 @@ static List FindCtorMemberRefs (MetadataReader reader, st }) .ToList (); - static MemberReferenceHandle FindCtorMemberRef (MetadataReader reader, string parentNamespace, string parentName, params string [] parameterTypes) => - FindCtorMemberRefs (reader, parentNamespace, parentName, parameterTypes).First (); - static string GetTypeDefOrRefName (MetadataReader reader, int codedToken) { int tag = codedToken & 0x3; @@ -101,22 +97,6 @@ public void Generate_HasRequiredAssemblyReferences () Assert.Contains ("System.Runtime.InteropServices", asmRefs); } - [Fact] - public void Generate_CreatesProxyTypes () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - Assert.NotEmpty (proxyTypes); - Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - } - [Fact] public void Generate_InheritedGenericBaseCallback_UsesValueTypeGenericArgument () { @@ -141,158 +121,6 @@ public void Generate_InheritedGenericBaseCallback_UsesValueTypeGenericArgument ( Assert.Equal ("SelectionMode", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); } - [Fact] - public void Generate_ProxyType_HasCtorAndCreateInstance () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var objectProxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - - var methods = objectProxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains (".ctor", methods); - Assert.Contains ("CreateInstance", methods); - } - - [Fact] - public void Generate_ProxyType_UsesJavaPeerProxyBase () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - Assert.NotEmpty (proxyTypes); - // Every proxy — concrete class, interface, or open generic definition — derives from - // the `JavaPeerProxy` base (a plain TypeReference), taking `targetType` as a ctor parameter. - Assert.All (proxyTypes, proxyType => { - Assert.Equal (HandleKind.TypeReference, proxyType.BaseType.Kind); - var baseTypeRef = reader.GetTypeReference ((TypeReferenceHandle) proxyType.BaseType); - Assert.Equal ("Java.Interop", reader.GetString (baseTypeRef.Namespace)); - Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name)); - }); - } - - // Regression test: decode the emitted proxy `.ctor` *body* (not just member presence) - // and verify it chains to the correct base constructor. Every proxy derives from - // `JavaPeerProxy` and calls the two-arg base ctor `(string, Type)` — pushing the target - // type via `Type.GetTypeFromHandle`. A base-ctor arity/token mismatch here would pass - // metadata inspection but blow up as an InvalidProgramException/TypeLoadException on the - // CoreCLR device legs, so assert it directly. - [Theory] - [InlineData ("Java_Lang_Object_Proxy", 2, true)] // concrete class -> base ctor (string, Type) - [InlineData ("Android_Views_IOnClickListener_Proxy", 2, true)] // interface -> base ctor (string, Type) - [InlineData ("MyApp_Generic_GenericHolder_1_Proxy", 2, true)] // open generic -> base ctor (string, Type) - public void Generate_ProxyCtor_ChainsToExpectedBaseConstructor (string proxyTypeName, int expectedBaseCtorArity, bool expectsGetTypeFromHandle) - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var proxyTypeHandle = reader.TypeDefinitions.First (h => { - var type = reader.GetTypeDefinition (h); - return reader.GetString (type.Namespace) == "_TypeMap.Proxies" && - reader.GetString (type.Name) == proxyTypeName; - }); - var proxyType = reader.GetTypeDefinition (proxyTypeHandle); - var ctorHandle = proxyType.GetMethods ().First (h => - reader.GetString (reader.GetMethodDefinition (h).Name) == ".ctor"); - var ctor = reader.GetMethodDefinition (ctorHandle); - var body = pe.GetMethodBody (ctor.RelativeVirtualAddress); - Assert.NotNull (body); - var ilBytes = body.GetILBytes (); - Assert.NotNull (ilBytes); - - var decoder = new TypeNameSignatureDecoder (reader); - int? baseCtorArity = null; - bool sawGetTypeFromHandle = false; - foreach (var token in ReadCallTokens (ilBytes!)) { - var handle = MetadataTokens.EntityHandle (token); - if (handle.Kind != HandleKind.MemberReference) - continue; - var mref = reader.GetMemberReference ((MemberReferenceHandle) handle); - var name = reader.GetString (mref.Name); - if (name == "GetTypeFromHandle") { - sawGetTypeFromHandle = true; - } else if (name == ".ctor") { - var sig = mref.DecodeMethodSignature (decoder, genericContext: null); - baseCtorArity = sig.RequiredParameterCount; - } - } - - Assert.True (baseCtorArity.HasValue, $"Proxy '{proxyTypeName}' .ctor must chain to a base ctor"); - Assert.Equal (expectedBaseCtorArity, baseCtorArity!.Value); - Assert.Equal (expectsGetTypeFromHandle, sawGetTypeFromHandle); - } - - // Regression test: every generated proxy type must carry a custom attribute whose - // constructor points at the proxy's own TypeDefinitionHandle (either as a MemberRef - // parented on the TypeDef, or as a MethodDefinition on the TypeDef). This is how - // JavaPeerProxy instances are resolved at runtime via - // type.GetCustomAttribute() — losing the self-application means the - // runtime can't construct the proxy. This has regressed twice; keep it covered. - [Fact] - public void Generate_ProxyType_IsSelfAppliedAsCustomAttribute () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var proxyTypeHandles = reader.TypeDefinitions - .Where (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Proxies") - .ToList (); - - Assert.NotEmpty (proxyTypeHandles); - - foreach (var proxyHandle in proxyTypeHandles) { - var proxy = reader.GetTypeDefinition (proxyHandle); - var proxyName = reader.GetString (proxy.Name); - - bool selfApplied = false; - foreach (var caHandle in proxy.GetCustomAttributes ()) { - var ca = reader.GetCustomAttribute (caHandle); - - switch (ca.Constructor.Kind) { - case HandleKind.MemberReference: - var ctorRef = reader.GetMemberReference ((MemberReferenceHandle) ca.Constructor); - if (ctorRef.Parent.Kind == HandleKind.TypeDefinition && - (TypeDefinitionHandle) ctorRef.Parent == proxyHandle) { - selfApplied = true; - } - break; - case HandleKind.MethodDefinition: - var ctorDef = reader.GetMethodDefinition ((MethodDefinitionHandle) ca.Constructor); - if (ctorDef.GetDeclaringType () == proxyHandle) { - selfApplied = true; - } - break; - } - - if (selfApplied) { - break; - } - } - - Assert.True (selfApplied, - $"Proxy type '{proxyName}' is missing its self-applied custom attribute. " + - "Every proxy must carry itself as a [JavaPeerProxy] attribute so the runtime " + - "can instantiate it via Type.GetCustomAttribute ()."); - } - } - [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { @@ -308,43 +136,6 @@ public void Generate_HasIgnoresAccessChecksToAttribute () reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); } - [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () - { - var peers = new List { - new JavaPeerInfo { - JavaName = "test/Duplicate", - CompatJniName = "test/Duplicate", - ManagedTypeName = "Test.Duplicate1", - ManagedTypeNamespace = "Test", - ManagedTypeShortName = "Duplicate1", - AssemblyName = "TestAssembly", - ActivationCtor = new ActivationCtorInfo { - DeclaringTypeName = "Test.Duplicate1", - DeclaringAssemblyName = "TestAssembly", - Style = ActivationCtorStyle.XamarinAndroid, - }, - }, - new JavaPeerInfo { - JavaName = "test/Duplicate", - CompatJniName = "test/Duplicate", - ManagedTypeName = "Test.Duplicate2", - ManagedTypeNamespace = "Test", - ManagedTypeShortName = "Duplicate2", - AssemblyName = "TestAssembly", - }, - }; - - using var stream = GenerateAssembly (peers, "AliasTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.True (assemblyAttrs.Count () >= 3); - - var typeNames = GetTypeRefNames (reader); - Assert.Contains (typeNames, name => name.StartsWith ("TypeMapAssociationAttribute", StringComparison.Ordinal)); - } - [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { @@ -356,28 +147,6 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } - [Fact] - public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () - { - var peers = ScanFixtures (); - // ClickableView has its own (IntPtr, JniHandleOwnership) ctor - var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); - Assert.NotNull (clickableView.ActivationCtor); - Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); - } - [Fact] public void Generate_InheritedCtor_CreateInstanceDoesNotActivate () { @@ -536,31 +305,6 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); } - [Fact] - public void Generate_JiStyleCtor_EmitsJavaInteropActivation () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.NotNull (jiPeer.ActivationCtor); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("JniObjectReference", typeNames); - Assert.Contains ("JniObjectReferenceOptions", typeNames); - - // The proxy still exists (with a TargetType property) - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - Assert.Single (proxyTypes); - } - [Fact] public void Emit_CalledTwice_Throws () { @@ -624,38 +368,6 @@ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () Assert.Equal ("System.Int32", paramType); } - [Fact] - public void Generate_JiStyleCtor_FirstParamIsByRef () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor!.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // Find the .ctor member reference whose parent type is the JI peer's declaring type - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - - // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) - bool foundByRefCtor = false; - foreach (var ctor in ctorRefs) { - var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); - if (sig.ParameterTypes.Length == 2 && - sig.ParameterTypes [0].Contains ("JniObjectReference")) { - // The byref encoding should produce "Java.Interop.JniObjectReference&" - Assert.True (sig.ParameterTypes [0].EndsWith ("&"), - $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); - foundByRefCtor = true; - } - } - Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); - } - [Fact] public void Generate_JiStyleInvoker_FirstParamIsByRef () { @@ -2137,31 +1849,4 @@ static List AllMemberRefHandles (MetadataReader reader) = .Select (i => MetadataTokens.MemberReferenceHandle (i)) .ToList (); - // SignatureTypeProvider returning a stringified type name for primitives and typerefs. - sealed class TypeNameSignatureDecoder : ISignatureTypeProvider - { - readonly MetadataReader _reader; - public TypeNameSignatureDecoder (MetadataReader reader) => _reader = reader; - public string GetPrimitiveType (PrimitiveTypeCode typeCode) => "System." + typeCode; - public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - { - var tr = reader.GetTypeReference (handle); - var name = reader.GetString (tr.Name); - var ns = tr.Namespace.IsNil ? "" : reader.GetString (tr.Namespace); - return ns.Length == 0 ? name : ns + "." + name; - } - public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => ""; - public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => ""; - public string GetSZArrayType (string elementType) => elementType + "[]"; - public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[*]"; - public string GetByReferenceType (string elementType) => elementType + "&"; - public string GetFunctionPointerType (MethodSignature signature) => ""; - public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) => genericType; - public string GetGenericMethodParameter (object? genericContext, int index) => ""; - public string GetGenericTypeParameter (object? genericContext, int index) => ""; - public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; - public string GetPinnedType (string elementType) => elementType; - public string GetPointerType (string elementType) => elementType + "*"; - } - } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index bf2a29d5bfd..171bb2fa7bf 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -116,30 +116,58 @@ public void JavaActivatedPeer_DisposeCanAccessThisAndInvokeVirtualMember () } [Test] - public void ClosedGenericJavaList_CanWrapJavaCreatedArrayListHandle () + public void JavaListAliasGroup_TargetHintSelectsGenericAndNonGenericManagedTypes () { AssumeTrimmableTypeMapEnabled (); + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetTargetTypes ("java/util/ArrayList", out var targetTypes)); + if (targetTypes is null) { + throw new InvalidOperationException ("The java/util/ArrayList alias group was not generated."); + } + CollectionAssert.Contains (targetTypes, typeof (JavaList)); + CollectionAssert.Contains (targetTypes, typeof (JavaList<>)); + var arrayListClass = JniEnvironment.Types.FindClass ("java/util/ArrayList"); try { var constructor = JNIEnv.GetMethodID (arrayListClass.Handle, "", "()V"); - var handle = JNIEnv.NewObject (arrayListClass.Handle, constructor); - using (var list = Java.Lang.Object.GetObject> (handle, JniHandleOwnership.TransferLocalRef)) { - Assert.IsNotNull (list); - Assert.AreEqual (typeof (JavaList), list.GetType ()); + var genericHandle = JNIEnv.NewObject (arrayListClass.Handle, constructor); + using (var genericList = Java.Lang.Object.GetObject> (genericHandle, JniHandleOwnership.TransferLocalRef)) { + Assert.IsNotNull (genericList); + Assert.AreEqual (typeof (JavaList), genericList.GetType ()); + + genericList.Add ("alpha"); + Assert.AreEqual ("alpha", genericList [0]); + } - list.Add ("alpha"); - list.Add ("beta"); + var nonGenericHandle = JNIEnv.NewObject (arrayListClass.Handle, constructor); + using (var nonGenericList = Java.Lang.Object.GetObject (nonGenericHandle, JniHandleOwnership.TransferLocalRef)) { + Assert.IsNotNull (nonGenericList); + Assert.AreEqual (typeof (JavaList), nonGenericList.GetType ()); - Assert.AreEqual (2, list.Count); - Assert.AreEqual ("alpha", list [0]); - Assert.AreEqual ("beta", list [1]); + nonGenericList.Add ("beta"); + Assert.AreEqual ("beta", nonGenericList [0]); } } finally { JniObjectReference.Dispose (ref arrayListClass); } } + [Test] + public void JavaCreatedHandle_UsesJavaInteropStyleActivationConstructor () + { + AssumeTrimmableTypeMapEnabled (); + TrimmableRuntimeJavaInteropPeer.Reset (); + + var handle = JNIEnv.CreateInstance ("net/dot/android/test/TrimmableRuntimeJavaInteropPeer", "()V"); + using (var peer = Java.Lang.Object.GetObject (handle, JniHandleOwnership.TransferLocalRef)) { + Assert.IsNotNull (peer); + Assert.AreEqual (typeof (TrimmableRuntimeJavaInteropPeer), peer.GetType ()); + Assert.AreEqual (1, TrimmableRuntimeJavaInteropPeer.ConstructorInvocations); + Assert.AreEqual (JniObjectReferenceOptions.Copy, TrimmableRuntimeJavaInteropPeer.Options); + Assert.IsTrue (peer.PeerReference.IsValid); + } + } + [Test] public void NonGenericCollection_CopyTo_ViewArray_UsesTrimmableTypeMapForArrayElementConversion () { @@ -363,4 +391,25 @@ public static void Reset () DisposeIdentityHashCode = 0; } } + + [Register ("net/dot/android/test/TrimmableRuntimeJavaInteropPeer", DoNotGenerateAcw = true)] + sealed class TrimmableRuntimeJavaInteropPeer : Java.Lang.Object + { + public static int ConstructorInvocations; + public static JniObjectReferenceOptions Options; + + public TrimmableRuntimeJavaInteropPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) + { + ConstructorInvocations++; + Options = options; + Construct (ref reference, options); + } + + public static void Reset () + { + ConstructorInvocations = 0; + Options = JniObjectReferenceOptions.None; + } + } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/TrimmableRuntimeJavaInteropPeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/TrimmableRuntimeJavaInteropPeer.java new file mode 100644 index 00000000000..4d45cfba892 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/TrimmableRuntimeJavaInteropPeer.java @@ -0,0 +1,5 @@ +package net.dot.android.test; + +public class TrimmableRuntimeJavaInteropPeer { + public TrimmableRuntimeJavaInteropPeer() {} +} From 7be5748128c824f5f91c8e7bfb662b87da5f89ee Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 25 Aug 2026 12:39:27 +0200 Subject: [PATCH 6/7] Retain typemap build-time contracts Keep focused coverage for constructed generic callback references and instance ExportField Java generation, which runtime tests cannot establish. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85e194d3-06ac-4880-be33-40e8b6c266b3 --- .../Generator/ExportFieldTests.cs | 15 +++++++++++ .../TypeMapAssemblyGeneratorTests.cs | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index 6e8d30087ce..1f84df6b278 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using Xunit; @@ -36,4 +37,18 @@ public void Scanner_ExportFieldMethod_HasExportConnectorAndFlag () Assert.True (getValue.IsExport); Assert.Equal ("public", getValue.JavaAccess); } + + [Fact] + public void JcwGenerator_EmitsInstanceFieldDeclarationAndMethodWrapper () + { + var peer = FindFixtureByJavaName ("my/app/ExportFieldExample"); + var generator = new JcwJavaSourceGenerator (); + using var writer = new StringWriter (); + generator.Generate (peer, writer); + var java = writer.ToString (); + + Assert.Contains ("VALUE = GetValue ();", java); + Assert.Contains ("GetValue ()", java); + Assert.Contains ("n_GetValue", java); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 41ab0669675..dc60cd3060f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -121,6 +121,31 @@ public void Generate_InheritedGenericBaseCallback_UsesValueTypeGenericArgument ( Assert.Equal ("SelectionMode", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); } + [Theory] + [InlineData ("my/app/GenericSelectableList")] + [InlineData ("my/app/GenericForwardingSelectableList")] + public void Generate_InheritedGenericBaseCallback_UsesConstructedBaseMemberRef (string javaName) + { + var peer = ScanFixtures ().Single (p => p.JavaName == javaName); + using var stream = GenerateAssembly (new [] { peer }); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var member = reader.MemberReferences + .Select (h => reader.GetMemberReference (h)) + .Single (m => reader.GetString (m.Name) == "n_SetSelection_I"); + + Assert.Equal (HandleKind.TypeSpecification, member.Parent.Kind); + var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) member.Parent); + var blob = reader.GetBlobReader (typeSpec.Signature); + + Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + Assert.Equal ("GenericSelectionHost`1", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); + Assert.Equal (1, blob.ReadCompressedInteger ()); + Assert.Equal (0x0E, blob.ReadByte ()); // ELEMENT_TYPE_STRING + } + [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { From e5cce1bda2ca17a4ece7915731786ac5cbd081a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 25 Aug 2026 20:38:16 +0200 Subject: [PATCH 7/7] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Mono.Android-Tests/Java.Interop/ExportTests.cs | 3 ++- .../Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index cead5fa6b4e..188a14cc191 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -298,7 +298,8 @@ public void ExportField_StaticObject_InitializerVisibleFromJava () var value = Java.Lang.Object.GetObject (valueHandle, JniHandleOwnership.TransferLocalRef); Assert.IsNotNull (value, "OBJECT_ANSWER should wrap as java.lang.Integer"); if (value is null) { - throw new InvalidOperationException ("OBJECT_ANSWER should wrap as java.lang.Integer"); + Assert.Fail ("OBJECT_ANSWER should wrap as java.lang.Integer"); + return; } using (value) { Assert.AreEqual (43, value.IntValue ()); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index 171bb2fa7bf..d5d622b5acc 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -122,7 +122,8 @@ public void JavaListAliasGroup_TargetHintSelectsGenericAndNonGenericManagedTypes Assert.IsTrue (TrimmableTypeMap.Instance.TryGetTargetTypes ("java/util/ArrayList", out var targetTypes)); if (targetTypes is null) { - throw new InvalidOperationException ("The java/util/ArrayList alias group was not generated."); + Assert.Fail ("The java/util/ArrayList alias group was not generated."); + return; } CollectionAssert.Contains (targetTypes, typeof (JavaList)); CollectionAssert.Contains (targetTypes, typeof (JavaList<>));