From 2ed13332d22686fc485abe74bc8dc4cdf254f4d8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:01:27 +0100 Subject: [PATCH 1/6] [TrimmableTypeMap][Core A] Foundation model + metadata type providers --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 19 ++ .../Scanner/CustomAttributeTypeProvider.cs | 90 +++++++++ .../Scanner/JavaPeerInfo.cs | 173 ++++++++++++++++++ .../Scanner/SignatureTypeProvider.cs | 87 +++++++++ 4 files changed, 369 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj new file mode 100644 index 00000000000..ac24040ffa4 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -0,0 +1,19 @@ + + + + + $(TargetFrameworkNETStandard) + latest + enable + Nullable + Microsoft.Android.Sdk.TrimmableTypeMap + + + + + + + + + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs new file mode 100644 index 00000000000..2152e557bb8 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ICustomAttributeTypeProvider implementation for decoding +/// custom attribute values via System.Reflection.Metadata. +/// +sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider +{ + readonly MetadataReader reader; + + public CustomAttributeTypeProvider (MetadataReader reader) + { + this.reader = reader; + } + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); + + public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = metadataReader.GetTypeDefinition (handle); + var name = metadataReader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; + } + var ns = metadataReader.GetString (typeDef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = metadataReader.GetTypeReference (handle); + var name = metadataReader.GetString (typeRef.Name); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; + } + var ns = metadataReader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromSerializedName (string name) => name; + + public PrimitiveTypeCode GetUnderlyingEnumType (string type) + { + // Find the enum type in this assembly's metadata and read its value__ field type. + foreach (var typeHandle in reader.TypeDefinitions) { + var typeDef = reader.GetTypeDefinition (typeHandle); + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + var fullName = ns.Length > 0 ? ns + "." + name : name; + + if (fullName != type) + continue; + + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; + } + } + + // Default to Int32 for enums defined in other assemblies + return PrimitiveTypeCode.Int32; + } + + public string GetSystemType () => "System.Type"; + + public string GetSZArrayType (string elementType) => elementType + "[]"; + + public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs new file mode 100644 index 00000000000..5b926186d9f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Represents a Java peer type discovered during assembly scanning. +/// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). +/// Generators consume this data model — they never touch PEReader/MetadataReader. +/// +sealed class JavaPeerInfo +{ + /// + /// JNI type name, e.g., "android/app/Activity". + /// Extracted from the [Register] attribute. + /// + public string JavaName { get; set; } = ""; + + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public string CompatJniName { get; set; } = ""; + + /// + /// Full managed type name, e.g., "Android.App.Activity". + /// + public string ManagedTypeName { get; set; } = ""; + + /// + /// Assembly name the type belongs to, e.g., "Mono.Android". + /// + public string AssemblyName { get; set; } = ""; + + /// + /// JNI name of the base Java type, e.g., "android/app/Activity" for a type + /// that extends Activity. Null for java/lang/Object or types without a Java base. + /// Needed by JCW Java source generation ("extends" clause). + /// + public string? BaseJavaName { get; set; } + + /// + /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. + /// Needed by JCW Java source generation ("implements" clause). + /// + public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); + + public bool IsInterface { get; set; } + public bool IsAbstract { get; set; } + + /// + /// If true, this is a Managed Callable Wrapper (MCW) binding type. + /// No JCW or RegisterNatives will be generated for it. + /// + public bool DoNotGenerateAcw { get; set; } + + /// + /// Types with component attributes ([Activity], [Service], etc.), + /// custom views from layout XML, or manifest-declared components + /// are unconditionally preserved (not trimmable). + /// + public bool IsUnconditional { get; set; } + + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; set; } = Array.Empty (); + + /// + /// Information about the activation constructor for this type. + /// May reference a base type's constructor if the type doesn't define its own. + /// + public ActivationCtorInfo? ActivationCtor { get; set; } + + /// + /// For interfaces and abstract types, the name of the invoker type + /// used to instantiate instances from Java. + /// + public string? InvokerTypeName { get; set; } + + /// + /// True if this is an open generic type definition. + /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. + /// + public bool IsGenericDefinition { get; set; } +} + +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed class MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public string JniName { get; set; } = ""; + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public string JniSignature { get; set; } = ""; + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; set; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public string ManagedMethodName { get; set; } = ""; + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; set; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; set; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; set; } +} + +/// +/// Describes how to call the activation constructor for a Java peer type. +/// +sealed class ActivationCtorInfo +{ + /// + /// The type that declares the activation constructor. + /// May be the type itself or a base type. + /// + public string DeclaringTypeName { get; set; } = ""; + + /// + /// The assembly containing the declaring type. + /// + public string DeclaringAssemblyName { get; set; } = ""; + + /// + /// The style of activation constructor found. + /// + public ActivationCtorStyle Style { get; set; } +} + +enum ActivationCtorStyle +{ + /// + /// Xamarin.Android style: (IntPtr handle, JniHandleOwnership transfer) + /// + XamarinAndroid, + + /// + /// Java.Interop style: (ref JniObjectReference reference, JniObjectReferenceOptions options) + /// + JavaInterop, +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs new file mode 100644 index 00000000000..185f10c89bd --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ISignatureTypeProvider implementation for decoding method +/// signatures via System.Reflection.Metadata. +/// Returns fully qualified type name strings. +/// +sealed class SignatureTypeProvider : ISignatureTypeProvider +{ + public static readonly SignatureTypeProvider Instance = new (); + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch { + PrimitiveTypeCode.Void => "System.Void", + PrimitiveTypeCode.Boolean => "System.Boolean", + PrimitiveTypeCode.Char => "System.Char", + PrimitiveTypeCode.SByte => "System.SByte", + PrimitiveTypeCode.Byte => "System.Byte", + PrimitiveTypeCode.Int16 => "System.Int16", + PrimitiveTypeCode.UInt16 => "System.UInt16", + PrimitiveTypeCode.Int32 => "System.Int32", + PrimitiveTypeCode.UInt32 => "System.UInt32", + PrimitiveTypeCode.Int64 => "System.Int64", + PrimitiveTypeCode.UInt64 => "System.UInt64", + PrimitiveTypeCode.Single => "System.Single", + PrimitiveTypeCode.Double => "System.Double", + PrimitiveTypeCode.String => "System.String", + PrimitiveTypeCode.Object => "System.Object", + PrimitiveTypeCode.IntPtr => "System.IntPtr", + PrimitiveTypeCode.UIntPtr => "System.UIntPtr", + PrimitiveTypeCode.TypedReference => "System.TypedReference", + _ => typeCode.ToString (), + }; + + public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = reader.GetTypeDefinition (handle); + var ns = reader.GetString (typeDef.Namespace); + var name = reader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; + } + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference (handle); + var name = reader.GetString (typeRef.Name); + + // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; + } + + var ns = reader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } + + public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var typeSpec = reader.GetTypeSpecification (handle); + return typeSpec.DecodeSignature (this, genericContext); + } + + public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]"; + public string GetByReferenceType (string elementType) => elementType + "&"; + public string GetPointerType (string elementType) => elementType + "*"; + public string GetPinnedType (string elementType) => elementType; + public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + + public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) + { + return genericType + "<" + string.Join (",", typeArguments) + ">"; + } + + public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index; + public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index; + + public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; +} From 5ad9210140e87ee8063c7c35217c44518152874f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 08:36:28 +0100 Subject: [PATCH 2/6] [TrimmableTypeMap][Core A] Prefer interpolation in scanner providers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/CustomAttributeTypeProvider.cs | 12 +++++----- .../Scanner/SignatureTypeProvider.cs | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs index 2152e557bb8..021e074ec6b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -25,10 +25,10 @@ public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefiniti var name = metadataReader.GetString (typeDef.Name); if (typeDef.IsNested) { var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind); - return parent + "+" + name; + return $"{parent}+{name}"; } var ns = metadataReader.GetString (typeDef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; + return ns.Length > 0 ? $"{ns}.{name}" : name; } public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) @@ -37,10 +37,10 @@ public string GetTypeFromReference (MetadataReader metadataReader, TypeReference var name = metadataReader.GetString (typeRef.Name); if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); - return parent + "+" + name; + return $"{parent}+{name}"; } var ns = metadataReader.GetString (typeRef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; + return ns.Length > 0 ? $"{ns}.{name}" : name; } public string GetTypeFromSerializedName (string name) => name; @@ -52,7 +52,7 @@ public PrimitiveTypeCode GetUnderlyingEnumType (string type) var typeDef = reader.GetTypeDefinition (typeHandle); var name = reader.GetString (typeDef.Name); var ns = reader.GetString (typeDef.Namespace); - var fullName = ns.Length > 0 ? ns + "." + name : name; + var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; if (fullName != type) continue; @@ -84,7 +84,7 @@ public PrimitiveTypeCode GetUnderlyingEnumType (string type) public string GetSystemType () => "System.Type"; - public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetSZArrayType (string elementType) => $"{elementType}[]"; public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs index 185f10c89bd..24ff6045787 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -42,9 +42,9 @@ public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle var name = reader.GetString (typeDef.Name); if (typeDef.IsNested) { var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind); - return parent + "+" + name; + return $"{parent}+{name}"; } - return ns.Length > 0 ? ns + "." + name : name; + return ns.Length > 0 ? $"{ns}.{name}" : name; } public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) @@ -55,11 +55,11 @@ public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle h // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); - return parent + "+" + name; + return $"{parent}+{name}"; } var ns = reader.GetString (typeRef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; + return ns.Length > 0 ? $"{ns}.{name}" : name; } public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) @@ -68,20 +68,20 @@ public string GetTypeFromSpecification (MetadataReader reader, object? genericCo return typeSpec.DecodeSignature (this, genericContext); } - public string GetSZArrayType (string elementType) => elementType + "[]"; - public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]"; - public string GetByReferenceType (string elementType) => elementType + "&"; - public string GetPointerType (string elementType) => elementType + "*"; + public string GetSZArrayType (string elementType) => $"{elementType}[]"; + public string GetArrayType (string elementType, ArrayShape shape) => $"{elementType}[{new string (',', shape.Rank - 1)}]"; + public string GetByReferenceType (string elementType) => $"{elementType}&"; + public string GetPointerType (string elementType) => $"{elementType}*"; public string GetPinnedType (string elementType) => elementType; public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) { - return genericType + "<" + string.Join (",", typeArguments) + ">"; + return $"{genericType}<{string.Join (",", typeArguments)}>"; } - public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index; - public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index; + public string GetGenericTypeParameter (object? genericContext, int index) => $"!{index}"; + public string GetGenericMethodParameter (object? genericContext, int index) => $"!!{index}"; public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; } From 5d9a8b7e446aba8e2e8f079dade9085a9cfc50f4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 15 Feb 2026 16:36:48 +0100 Subject: [PATCH 3/6] [TrimmableTypeMap][Core A] Use version variable for System.Reflection.Metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 1 + .../Microsoft.Android.Sdk.TrimmableTypeMap.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index d87b6a2fe34..5db15246f70 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -16,6 +16,7 @@ 11.0.100-preview.1.26076.102 0.11.5-preview.26076.102 9.0.4 + 9.0.3 36.1.30 $(MicrosoftNETSdkAndroidManifest100100PackageVersion) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index ac24040ffa4..76b37435368 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -11,7 +11,7 @@ - + From 70861e186d21a5c468934f345894f7e0807c0683 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 14:13:54 +0100 Subject: [PATCH 4/6] [TrimmableTypeMap][Core A] Address review feedback - Remove unused 'using System.Collections.Immutable' import - Fix nested enum type resolution in GetUnderlyingEnumType by reusing GetTypeFromDefinition which correctly handles nested types - Cache enum type lookups to avoid O(N) scan on each call - Remove redundant LangVersion=latest from csproj (set by Configuration.props) - Convert JavaPeerInfo, MarshalMethodInfo, ActivationCtorInfo to records Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 1 - .../Scanner/CustomAttributeTypeProvider.cs | 89 +++++++++++++------ .../Scanner/JavaPeerInfo.cs | 6 +- 3 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 76b37435368..a8ba3b210e7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -3,7 +3,6 @@ $(TargetFrameworkNETStandard) - latest enable Nullable Microsoft.Android.Sdk.TrimmableTypeMap diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs index 021e074ec6b..0d12aa176fb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Immutable; +using System.Collections.Generic; using System.Reflection.Metadata; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -11,6 +11,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider { readonly MetadataReader reader; + Dictionary? enumTypeCache; public CustomAttributeTypeProvider (MetadataReader reader) { @@ -47,38 +48,74 @@ public string GetTypeFromReference (MetadataReader metadataReader, TypeReference public PrimitiveTypeCode GetUnderlyingEnumType (string type) { - // Find the enum type in this assembly's metadata and read its value__ field type. + if (enumTypeCache == null) { + enumTypeCache = BuildEnumTypeCache (); + } + + if (enumTypeCache.TryGetValue (type, out var code)) { + return code; + } + + // Default to Int32 for enums defined in other assemblies + return PrimitiveTypeCode.Int32; + } + + Dictionary BuildEnumTypeCache () + { + var cache = new Dictionary (); + foreach (var typeHandle in reader.TypeDefinitions) { var typeDef = reader.GetTypeDefinition (typeHandle); - var name = reader.GetString (typeDef.Name); - var ns = reader.GetString (typeDef.Namespace); - var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; - if (fullName != type) + // Only process enum types + if (!IsEnum (typeDef)) continue; - // For enums, the first instance field is the underlying value__ field - foreach (var fieldHandle in typeDef.GetFields ()) { - var field = reader.GetFieldDefinition (fieldHandle); - if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) - continue; - - var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); - return sig switch { - "System.Byte" => PrimitiveTypeCode.Byte, - "System.SByte" => PrimitiveTypeCode.SByte, - "System.Int16" => PrimitiveTypeCode.Int16, - "System.UInt16" => PrimitiveTypeCode.UInt16, - "System.Int32" => PrimitiveTypeCode.Int32, - "System.UInt32" => PrimitiveTypeCode.UInt32, - "System.Int64" => PrimitiveTypeCode.Int64, - "System.UInt64" => PrimitiveTypeCode.UInt64, - _ => PrimitiveTypeCode.Int32, - }; - } + var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0); + var code = GetEnumUnderlyingTypeCode (typeDef); + cache [fullName] = code; + } + + return cache; + } + + bool IsEnum (TypeDefinition typeDef) + { + var baseType = typeDef.BaseType; + if (baseType.IsNil) + return false; + + string? baseFullName = baseType.Kind switch { + HandleKind.TypeReference => GetTypeFromReference (reader, (TypeReferenceHandle)baseType, rawTypeKind: 0), + HandleKind.TypeDefinition => GetTypeFromDefinition (reader, (TypeDefinitionHandle)baseType, rawTypeKind: 0), + _ => null, + }; + + return baseFullName == "System.Enum"; + } + + PrimitiveTypeCode GetEnumUnderlyingTypeCode (TypeDefinition typeDef) + { + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; } - // Default to Int32 for enums defined in other assemblies return PrimitiveTypeCode.Int32; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 5b926186d9f..5f8ed8bffa3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -8,7 +8,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). /// Generators consume this data model — they never touch PEReader/MetadataReader. /// -sealed class JavaPeerInfo +sealed record JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". @@ -94,7 +94,7 @@ sealed class JavaPeerInfo /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, /// and a RegisterNatives call. /// -sealed class MarshalMethodInfo +sealed record MarshalMethodInfo { /// /// JNI method name, e.g., "onCreate". @@ -140,7 +140,7 @@ sealed class MarshalMethodInfo /// /// Describes how to call the activation constructor for a Java peer type. /// -sealed class ActivationCtorInfo +sealed record ActivationCtorInfo { /// /// The type that declares the activation constructor. From e2f1c060f0b983769c49f621c13a4f315311a577 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 14:26:53 +0100 Subject: [PATCH 5/6] [TrimmableTypeMap][Core A] Bump System.Reflection.Metadata to 11.0.0-preview.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 5db15246f70..bf1e2b58feb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -16,7 +16,7 @@ 11.0.100-preview.1.26076.102 0.11.5-preview.26076.102 9.0.4 - 9.0.3 + 11.0.0-preview.1.26104.118 36.1.30 $(MicrosoftNETSdkAndroidManifest100100PackageVersion) From a2ac4362ca485a76a47fcd911752f0e9afa30da6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 14:31:55 +0100 Subject: [PATCH 6/6] [TrimmableTypeMap][Core A] Use required init properties on records Add netstandard2.0 polyfills for IsExternalInit, RequiredMemberAttribute, CompilerFeatureRequiredAttribute, and SetsRequiredMembersAttribute. Convert all record properties to init-only, with 'required' on non-nullable string and essential properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompilerFeaturePolyfills.cs | 28 +++++++++++ .../Scanner/JavaPeerInfo.cs | 48 +++++++++---------- 2 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs new file mode 100644 index 00000000000..6b5fc126876 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs @@ -0,0 +1,28 @@ +// Polyfills for C# language features on netstandard2.0 + +// Required for init-only setters +namespace System.Runtime.CompilerServices +{ + static class IsExternalInit { } + + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed class RequiredMemberAttribute : Attribute { } + + [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] + sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute (string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + public bool IsOptional { get; init; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage (AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + sealed class SetsRequiredMembersAttribute : Attribute { } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 5f8ed8bffa3..85472f1b3ba 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -14,53 +14,53 @@ sealed record JavaPeerInfo /// JNI type name, e.g., "android/app/Activity". /// Extracted from the [Register] attribute. /// - public string JavaName { get; set; } = ""; + public required string JavaName { get; init; } /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). /// For MCW binding types (with [Register]), this equals . /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. /// - public string CompatJniName { get; set; } = ""; + public required string CompatJniName { get; init; } /// /// Full managed type name, e.g., "Android.App.Activity". /// - public string ManagedTypeName { get; set; } = ""; + public required string ManagedTypeName { get; init; } /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// - public string AssemblyName { get; set; } = ""; + public required string AssemblyName { get; init; } /// /// JNI name of the base Java type, e.g., "android/app/Activity" for a type /// that extends Activity. Null for java/lang/Object or types without a Java base. /// Needed by JCW Java source generation ("extends" clause). /// - public string? BaseJavaName { get; set; } + public string? BaseJavaName { get; init; } /// /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. /// Needed by JCW Java source generation ("implements" clause). /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); + public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); - public bool IsInterface { get; set; } - public bool IsAbstract { get; set; } + public bool IsInterface { get; init; } + public bool IsAbstract { get; init; } /// /// If true, this is a Managed Callable Wrapper (MCW) binding type. /// No JCW or RegisterNatives will be generated for it. /// - public bool DoNotGenerateAcw { get; set; } + public bool DoNotGenerateAcw { get; init; } /// /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). /// - public bool IsUnconditional { get; set; } + public bool IsUnconditional { get; init; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or @@ -68,25 +68,25 @@ sealed record JavaPeerInfo /// Constructors are identified by . /// Ordered — the index in this list is the method's ordinal for RegisterNatives. /// - public IReadOnlyList MarshalMethods { get; set; } = Array.Empty (); + public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); /// /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. /// - public ActivationCtorInfo? ActivationCtor { get; set; } + public ActivationCtorInfo? ActivationCtor { get; init; } /// /// For interfaces and abstract types, the name of the invoker type /// used to instantiate instances from Java. /// - public string? InvokerTypeName { get; set; } + public string? InvokerTypeName { get; init; } /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// - public bool IsGenericDefinition { get; set; } + public bool IsGenericDefinition { get; init; } } /// @@ -100,41 +100,41 @@ sealed record MarshalMethodInfo /// JNI method name, e.g., "onCreate". /// This is the Java method name (without n_ prefix). /// - public string JniName { get; set; } = ""; + public required string JniName { get; init; } /// /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". /// Contains both parameter types and return type. /// - public string JniSignature { get; set; } = ""; + public required string JniSignature { get; init; } /// /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". /// Null for [Export] methods. /// - public string? Connector { get; set; } + public string? Connector { get; init; } /// /// Name of the managed method this maps to, e.g., "OnCreate". /// - public string ManagedMethodName { get; set; } = ""; + public required string ManagedMethodName { get; init; } /// /// True if this is a constructor registration. /// - public bool IsConstructor { get; set; } + public bool IsConstructor { get; init; } /// /// For [Export] methods: Java exception types that the method declares it can throw. /// Null for [Register] methods. /// - public IReadOnlyList? ThrownNames { get; set; } + public IReadOnlyList? ThrownNames { get; init; } /// /// For [Export] methods: super constructor arguments string. /// Null for [Register] methods. /// - public string? SuperArgumentsString { get; set; } + public string? SuperArgumentsString { get; init; } } /// @@ -146,17 +146,17 @@ sealed record ActivationCtorInfo /// The type that declares the activation constructor. /// May be the type itself or a base type. /// - public string DeclaringTypeName { get; set; } = ""; + public required string DeclaringTypeName { get; init; } /// /// The assembly containing the declaring type. /// - public string DeclaringAssemblyName { get; set; } = ""; + public required string DeclaringAssemblyName { get; init; } /// /// The style of activation constructor found. /// - public ActivationCtorStyle Style { get; set; } + public required ActivationCtorStyle Style { get; init; } } enum ActivationCtorStyle