From da1d643b8e34f89cc7150ffac8887cd50369ae63 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 27 Jun 2026 15:23:07 +0200 Subject: [PATCH] [TrimmableTypeMap] Use non-generic JavaPeerProxy base for interface proxies The generated proxy for an interface peer (e.g. a binding listener interface like ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallback) derived from the closed generic JavaPeerProxy. That base annotates its type parameter with [DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors)] and returns new JavaPeerContainerFactory() from GetContainerFactory(). Closing the generic over an interface -- which has no constructors -- makes ILC fail to load the closed type ("Failed to load type JavaPeerProxy1<...INonMarshalingPreviewCallback> from assembly Mono.Android"), which fails the whole NativeAOT build (ManifestTest.RemovePermissionTest, which pulls in ZXing.Net.Mobile -> ApxLabs.FastAndroidCamera). Interface peers now derive from the non-generic JavaPeerProxy base (the same base already used for open generic definitions), passing the interface as the TargetType constructor argument so runtime TargetType identity is unchanged. Instances are still created from the InvokerType in CreateInstance, so behaviour is preserved; abstract classes keep the generic base since they have constructors. Reproduced locally with ZXing.Net.Mobile (3.0.0-beta5): NativeAOT build failed with the TypeLoadException before, builds successfully after. Basic Mono.Android listener apps (IOnClickListener/IOnLongClickListener) and AndroidX.Fragment still build clean. Fixes RemovePermissionTest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 10 +++++++ .../Generator/ModelBuilder.cs | 1 + .../Generator/TypeMapAssemblyEmitter.cs | 17 ++++++++---- .../TypeMapAssemblyGeneratorTests.cs | 27 +++++++++++++++++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index a90f0d7dbeb..2476b2eddfe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -142,6 +142,16 @@ sealed class JavaPeerProxyData /// public bool IsGenericDefinition { get; init; } + /// + /// True if the proxied peer type is a Java interface. Interfaces have no constructors, so + /// the proxy must derive from the non-generic JavaPeerProxy base instead of + /// JavaPeerProxy<T>: closing the generic (whose T is annotated with + /// [DynamicallyAccessedMembers(PublicConstructors|NonPublicConstructors)]) over an + /// interface makes ILC fail to load the type (TypeLoadException). Instances are still created + /// from in CreateInstance. + /// + public bool IsInterface { get; init; } + /// /// True when the Java stub must not call RegisterNatives from a static initializer because /// the type can be instantiated before the runtime is fully ready (for example Application diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4671394e936..b6edba82265 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -290,6 +290,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash }, IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, + IsInterface = peer.IsInterface, CannotRegisterInStaticConstructor = peer.CannotRegisterInStaticConstructor, }; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8ab18800ea5..a68a256ecf1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -665,9 +665,16 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary` annotates T with + // [DynamicallyAccessedMembers(PublicConstructors|NonPublicConstructors)], and closing it + // over an interface (which has no constructors) makes ILC fail to load the type + // (TypeLoadException: "Failed to load type 'JavaPeerProxy`1'"). The peer is + // still activated from its InvokerType in CreateInstance, so behaviour is unchanged. + bool useNonGenericBase = proxy.IsGenericDefinition || proxy.IsInterface; EntityHandle proxyBaseType; MemberReferenceHandle baseCtorRef; - if (proxy.IsGenericDefinition) { + if (useNonGenericBase) { proxyBaseType = _javaPeerProxyNonGenericRef; baseCtorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, @@ -709,9 +716,9 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { encoder.OpCode (ILOpCode.Ldarg_0); encoder.LoadString (metadata.GetOrAddUserString (proxy.JniName)); - if (proxy.IsGenericDefinition) { - // Non-generic base ctor signature: (string, Type, Type?). Push the open-generic - // target type as the second argument. + if (useNonGenericBase) { + // Non-generic base ctor signature: (string, Type, Type?). Push the + // target type (open-generic definition or interface) as the second argument. encoder.LoadToken (targetTypeRef); encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); } @@ -721,7 +728,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { switch (proxyType.BaseType.Kind) { case HandleKind.TypeSpecification: - // Non-generic target types derive from the closed `JavaPeerProxy`. + // Concrete (constructible) target types derive from the closed `JavaPeerProxy`. var baseTypeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) proxyType.BaseType); var baseTypeName = baseTypeSpec.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); Assert.StartsWith ("Java.Interop.JavaPeerProxy`1<", baseTypeName, StringComparison.Ordinal); break; case HandleKind.TypeReference: - // Open generic target types derive from the non-generic `JavaPeerProxy`. + // Open generic definitions and interfaces derive from the non-generic `JavaPeerProxy`. var baseTypeRef = reader.GetTypeReference ((TypeReferenceHandle) proxyType.BaseType); Assert.Equal ("Java.Interop", reader.GetString (baseTypeRef.Namespace)); Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name)); @@ -165,6 +165,29 @@ public void Generate_ProxyType_UsesGenericJavaPeerProxyBase () objectProxyBaseType.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null)); } + [Fact] + public void Generate_InterfaceProxyType_UsesNonGenericJavaPeerProxyBase () + { + // JavaPeerProxy annotates T with [DynamicallyAccessedMembers(Constructors)]. Closing it + // over an interface (which has no constructors) makes ILC fail to load the closed generic + // type ("Failed to load type 'JavaPeerProxy`1'"). Interface proxies must + // therefore derive from the non-generic JavaPeerProxy base (a plain TypeReference). + 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: 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