diff --git a/eng/Versions.props b/eng/Versions.props index d87b6a2fe34..bf1e2b58feb 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 + 11.0.0-preview.1.26104.118 36.1.30 $(MicrosoftNETSdkAndroidManifest100100PackageVersion) diff --git a/src-ThirdParty/System.Runtime.CompilerServices/CompilerFeaturePolyfills.cs b/src-ThirdParty/System.Runtime.CompilerServices/CompilerFeaturePolyfills.cs new file mode 100644 index 00000000000..c33ab5025c2 --- /dev/null +++ b/src-ThirdParty/System.Runtime.CompilerServices/CompilerFeaturePolyfills.cs @@ -0,0 +1,23 @@ +// 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 (string featureName) : Attribute + { + public string FeatureName { get; } = featureName; + 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/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj new file mode 100644 index 00000000000..d75d4d7df1e --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -0,0 +1,20 @@ + + + + + $(TargetFrameworkNETStandard) + enable + Nullable + Microsoft.Android.Sdk.TrimmableTypeMap + + + + + + + + + + + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs new file mode 100644 index 00000000000..cf8bd977a71 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions, +/// all subsequent lookups are O(1) dictionary lookups. +/// +sealed class AssemblyIndex : IDisposable +{ + readonly PEReader peReader; + readonly CustomAttributeTypeProvider customAttributeTypeProvider; + + public MetadataReader Reader { get; } + public string AssemblyName { get; } + public string FilePath { get; } + + /// + /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. + /// + public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal); + + /// + /// Cached [Register] attribute data per type. + /// + public Dictionary RegisterInfoByType { get; } = new (); + + /// + /// All custom attribute data per type, pre-parsed for the attributes we care about. + /// + public Dictionary AttributesByType { get; } = new (); + + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath) + { + this.peReader = peReader; + this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); + Reader = reader; + AssemblyName = assemblyName; + FilePath = filePath; + } + + public static AssemblyIndex Create (string filePath) + { + var peReader = new PEReader (File.OpenRead (filePath)); + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); + index.Build (); + return index; + } + + void Build () + { + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader); + if (fullName.Length == 0) { + continue; + } + + TypesByFullName [fullName] = typeHandle; + + var (registerInfo, attrInfo) = ParseAttributes (typeDef); + + if (attrInfo is not null) { + AttributesByType [typeHandle] = attrInfo; + } + + if (registerInfo is not null) { + RegisterInfoByType [typeHandle] = registerInfo; + } + } + } + + (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) + { + RegisterInfo? registerInfo = null; + TypeAttributeInfo? attrInfo = null; + + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = Reader.GetCustomAttribute (caHandle); + var attrName = GetCustomAttributeName (ca, Reader); + + if (attrName is null) { + continue; + } + + if (attrName == "RegisterAttribute") { + registerInfo = ParseRegisterAttribute (ca); + } else if (attrName == "ExportAttribute") { + // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner + } else if (IsKnownComponentAttribute (attrName)) { + attrInfo ??= CreateTypeAttributeInfo (attrName); + var name = TryGetNameProperty (ca); + if (name is not null) { + attrInfo.JniName = name.Replace ('.', '/'); + } + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); + } + } + } + + return (registerInfo, attrInfo); + } + + static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { + "ActivityAttribute", + "ServiceAttribute", + "BroadcastReceiverAttribute", + "ContentProviderAttribute", + "ApplicationAttribute", + "InstrumentationAttribute", + }; + + static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) + { + return attrName == "ApplicationAttribute" + ? new ApplicationAttributeInfo () + : new TypeAttributeInfo (attrName); + } + + static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName); + + internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) + { + if (ca.Constructor.Kind == HandleKind.MemberReference) { + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeReference) { + var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent); + return reader.GetString (typeRef.Name); + } + } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) { + var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); + var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + return reader.GetString (declaringType.Name); + } + return null; + } + + internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca) + { + return ParseRegisterInfo (DecodeAttribute (ca)); + } + + internal CustomAttributeValue DecodeAttribute (CustomAttribute ca) + { + return ca.DecodeValue (customAttributeTypeProvider); + } + + RegisterInfo ParseRegisterInfo (CustomAttributeValue value) + { + + string jniName = ""; + string? signature = null; + string? connector = null; + bool doNotGenerateAcw = false; + + if (value.FixedArguments.Length > 0) { + jniName = (string?)value.FixedArguments [0].Value ?? ""; + } + if (value.FixedArguments.Length > 1) { + signature = (string?)value.FixedArguments [1].Value; + } + if (value.FixedArguments.Length > 2) { + connector = (string?)value.FixedArguments [2].Value; + } + + if (TryGetNamedArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { + doNotGenerateAcw = doNotGenerateAcwValue; + } + + return new RegisterInfo { + JniName = jniName, + Signature = signature, + Connector = connector, + DoNotGenerateAcw = doNotGenerateAcw, + }; + } + + string? TryGetTypeProperty (CustomAttribute ca, string propertyName) + { + var value = DecodeAttribute (ca); + if (TryGetNamedArgument (value, propertyName, out var typeName) && !string.IsNullOrEmpty (typeName)) { + return typeName; + } + return null; + } + + string? TryGetNameProperty (CustomAttribute ca) + { + var name = TryGetTypeProperty (ca, "Name"); + if (!string.IsNullOrEmpty (name)) { + return name; + } + + var value = DecodeAttribute (ca); + + // Fall back to first constructor argument (e.g., [CustomJniName("...")]) + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) { + return ctorName; + } + + return null; + } + + static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is T typedValue) { + argumentValue = typedValue; + return true; + } + } + argumentValue = default; + return false; + } + + public void Dispose () + { + peReader.Dispose (); + } +} + +/// +/// Parsed [Register] attribute data for a type or method. +/// +sealed record RegisterInfo +{ + public required string JniName { get; init; } + public string? Signature { get; init; } + public string? Connector { get; init; } + public bool DoNotGenerateAcw { get; init; } +} + +/// +/// Parsed [Export] attribute data for a method. +/// +sealed record ExportInfo +{ + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } +} + +class TypeAttributeInfo (string attributeName) +{ + public string AttributeName { get; } = attributeName; + public string? JniName { get; set; } +} + +sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") +{ + public string? BackupAgent { get; set; } + public string? ManageSpaceActivity { get; set; } +} 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..f3441d1a475 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ICustomAttributeTypeProvider implementation for decoding +/// custom attribute values via System.Reflection.Metadata. +/// +sealed class CustomAttributeTypeProvider (MetadataReader reader) : ICustomAttributeTypeProvider +{ + Dictionary? enumTypeCache; + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); + + public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromDefinition (metadataReader, handle, rawTypeKind); + + public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromReference (metadataReader, handle, rawTypeKind); + + public string GetTypeFromSerializedName (string name) => name; + + public PrimitiveTypeCode GetUnderlyingEnumType (string 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); + + // Only process enum types + if (!IsEnum (typeDef)) + continue; + + var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0); + cache [fullName] = GetEnumUnderlyingTypeCode (typeDef); + } + + 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, + }; + } + + 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..85472f1b3ba --- /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 record JavaPeerInfo +{ + /// + /// JNI type name, e.g., "android/app/Activity". + /// Extracted from the [Register] attribute. + /// + 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 required string CompatJniName { get; init; } + + /// + /// Full managed type name, e.g., "Android.App.Activity". + /// + public required string ManagedTypeName { get; init; } + + /// + /// Assembly name the type belongs to, e.g., "Mono.Android". + /// + 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; 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; init; } = Array.Empty (); + + 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; 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; init; } + + /// + /// 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; 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; init; } + + /// + /// For interfaces and abstract types, the name of the invoker type + /// used to instantiate instances from Java. + /// + 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; init; } +} + +/// +/// 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 record MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public required string JniName { get; init; } + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + 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; init; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public required string ManagedMethodName { get; init; } + + /// + /// True if this is a constructor registration. + /// + 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; init; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; init; } +} + +/// +/// Describes how to call the activation constructor for a Java peer type. +/// +sealed record ActivationCtorInfo +{ + /// + /// The type that declares the activation constructor. + /// May be the type itself or a base type. + /// + public required string DeclaringTypeName { get; init; } + + /// + /// The assembly containing the declaring type. + /// + public required string DeclaringAssemblyName { get; init; } + + /// + /// The style of activation constructor found. + /// + public required ActivationCtorStyle Style { get; init; } +} + +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/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs new file mode 100644 index 00000000000..469d6345596 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -0,0 +1,735 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Scans assemblies for Java peer types using System.Reflection.Metadata. +/// Two-phase architecture: +/// Phase 1: Build per-assembly indices (fast, O(1) lookups) +/// Phase 2: Analyze types using cached indices +/// +sealed class JavaPeerScanner : IDisposable +{ + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); + readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + + /// + /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. + /// Checks the specified assembly (by name) in the assembly cache. + /// + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) + { + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && + resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { + return true; + } + handle = default; + resolvedIndex = null; + return false; + } + + /// + /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling + /// nested types whose ResolutionScope is another TypeReference. + /// + static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index) + { + var typeRef = index.Reader.GetTypeReference (handle); + var name = index.Reader.GetString (typeRef.Name); + var ns = index.Reader.GetString (typeRef.Namespace); + + var scope = typeRef.ResolutionScope; + switch (scope.Kind) { + case HandleKind.AssemblyReference: { + var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); + var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); + return (fullName, index.Reader.GetString (asmRef.Name)); + } + case HandleKind.TypeReference: { + // Nested type: recurse to get the declaring type's full name and assembly + var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); + return (MetadataTypeNameResolver.JoinNestedTypeName (parentFullName, name), assemblyName); + } + default: { + var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); + return (fullName, index.AssemblyName); + } + } + } + + /// + /// Looks up the [Register] JNI name for a type identified by name + assembly. + /// + string? ResolveRegisterJniName (string typeName, string assemblyName) + { + if (TryResolveType (typeName, assemblyName, out var handle, out var resolvedIndex) && + resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo)) { + return regInfo.JniName; + } + return null; + } + + /// + /// Phase 1: Build indices for all assemblies. + /// Phase 2: Scan all types and produce JavaPeerInfo records. + /// + public List Scan (IReadOnlyList assemblyPaths) + { + // Phase 1: Build indices for all assemblies + foreach (var path in assemblyPaths) { + var index = AssemblyIndex.Create (path); + assemblyCache [index.AssemblyName] = index; + } + + // Phase 2: Analyze types using cached indices + var resultsByManagedName = new Dictionary (StringComparer.Ordinal); + + foreach (var index in assemblyCache.Values) { + ScanAssembly (index, resultsByManagedName); + } + + // Phase 3: Force unconditional on types referenced by [Application] attributes + ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); + + return new List (resultsByManagedName.Values); + } + + /// + /// Types referenced by [Application(BackupAgent = typeof(X))] or + /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, + /// because the manifest will reference them even if nothing else does. + /// + static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) + { + foreach (var index in assemblyCache.Values) { + foreach (var attrInfo in index.AttributesByType.Values) { + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); + } + } + } + } + + static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) + { + if (managedTypeName is null) { + return; + } + + managedTypeName = managedTypeName.Trim (); + if (managedTypeName.Length == 0) { + return; + } + + // Try exact match first (handles both plain and assembly-qualified names) + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; + return; + } + + // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." + // Strip to just the type name for lookup + var commaIndex = managedTypeName.IndexOf (','); + if (commaIndex <= 0) { + return; + } + + var typeName = managedTypeName.Substring (0, commaIndex).Trim (); + if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { + resultsByManagedName [typeName] = peer with { IsUnconditional = true }; + } + } + + void ScanAssembly (AssemblyIndex index, Dictionary results) + { + foreach (var typeHandle in index.Reader.TypeDefinitions) { + var typeDef = index.Reader.GetTypeDefinition (typeHandle); + + // Skip module-level types + if (index.Reader.GetString (typeDef.Name) == "") { + continue; + } + + // Determine the JNI name and whether this is a known Java peer. + // Priority: + // 1. [Register] attribute → use JNI name from attribute + // 2. Component attribute Name property → convert dots to slashes + // 3. Extends a known Java peer → auto-compute JNI name via CRC64 + // 4. None of the above → not a Java peer, skip + string? jniName = null; + string? compatJniName = null; + bool doNotGenerateAcw = false; + + index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); + index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); + + if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + jniName = registerInfo.JniName; + compatJniName = jniName; + doNotGenerateAcw = registerInfo.DoNotGenerateAcw; + } else if (attrInfo?.JniName is not null) { + // User type with [Activity(Name = "...")] but no [Register] + jniName = attrInfo.JniName; + compatJniName = jniName; + } else { + // No explicit JNI name — check if this type extends a known Java peer. + // If so, auto-compute JNI name from the managed type name via CRC64. + if (ExtendsJavaPeer (typeDef, index)) { + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); + } else { + continue; + } + } + + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + + var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; + var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; + var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; + + var isUnconditional = attrInfo is not null; + string? invokerTypeName = null; + + // Resolve base Java type name + var baseJavaName = ResolveBaseJavaName (typeDef, index, results); + + // Resolve implemented Java interface names + var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); + + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + + // Resolve activation constructor + var activationCtor = ResolveActivationCtor (fullName, typeDef, index); + + // For interfaces/abstract types, try to find invoker type name + if (isInterface || isAbstract) { + invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); + } + + var peer = new JavaPeerInfo { + JavaName = jniName, + CompatJniName = compatJniName, + ManagedTypeName = fullName, + AssemblyName = index.AssemblyName, + BaseJavaName = baseJavaName, + ImplementedInterfaceJavaNames = implementedInterfaces, + IsInterface = isInterface, + IsAbstract = isAbstract, + DoNotGenerateAcw = doNotGenerateAcw, + IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, + ActivationCtor = activationCtor, + InvokerTypeName = invokerTypeName, + IsGenericDefinition = isGenericDefinition, + }; + + results [fullName] = peer; + } + } + + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister is null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature is null && registerInfo.Connector is null) { + return; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, + }); + } + + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is null) { + return null; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + // First try [Register] attribute + var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); + if (registerJniName is not null) { + return registerJniName; + } + + // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + if (results.TryGetValue (baseTypeName, out var basePeer)) { + return basePeer.JavaName; + } + + return null; + } + + List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index) + { + var result = new List (); + var interfaceImpls = typeDef.GetInterfaceImplementations (); + + foreach (var implHandle in interfaceImpls) { + var impl = index.Reader.GetInterfaceImplementation (implHandle); + var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); + if (ifaceJniName is not null) { + result.Add (ifaceJniName); + } + } + + return result; + } + + string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) + { + var resolved = ResolveEntityHandle (interfaceHandle, index); + return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + } + + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + { + exportInfo = null; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = index.ParseRegisterAttribute (ca); + return true; + } + + if (attrName == "ExportAttribute") { + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; + } + } + registerInfo = null; + return false; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return index.ParseRegisterAttribute (ca); + } + } + return null; + } + + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = index.DecodeAttribute (ca); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (string.IsNullOrEmpty (exportName)) { + exportName = index.Reader.GetString (methodDef.Name); + } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + } + return "Ljava/lang/Object;"; + } + } + + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) + { + var cacheKey = (typeName, index.AssemblyName); + if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { + return cached; + } + + // Check this type's constructors + var ownCtor = FindActivationCtorOnType (typeDef, index); + if (ownCtor is not null) { + var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; + activationCtorCache [cacheKey] = info; + return info; + } + + // Walk base type hierarchy + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is not null) { + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); + if (result is not null) { + activationCtorCache [cacheKey] = result; + } + return result; + } + } + + return null; + } + + static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var method = index.Reader.GetMethodDefinition (methodHandle); + var name = index.Reader.GetString (method.Name); + + if (name != ".ctor") { + continue; + } + + var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + + // XI style: (IntPtr, JniHandleOwnership) + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0] == "System.IntPtr" && + sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") { + return ActivationCtorStyle.XamarinAndroid; + } + + // JI style: (ref JniObjectReference, JniObjectReferenceOptions) + if (sig.ParameterTypes.Length == 2 && + (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") && + sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") { + return ActivationCtorStyle.JavaInterop; + } + } + + return null; + } + + /// + /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying + /// type's (fullName, assemblyName) by reading the raw signature blob. + /// + static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) + { + var typeSpec = index.Reader.GetTypeSpecification (specHandle); + var blobReader = index.Reader.GetBlobReader (typeSpec.Signature); + + // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args... + var elementType = blobReader.ReadByte (); + if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST + return null; + } + + var classOrValueType = blobReader.ReadByte (); + if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE + return null; + } + + // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec) + var codedToken = blobReader.ReadCompressedInteger (); + var tag = codedToken & 0x3; + var row = codedToken >> 2; + + switch (tag) { + case 0: { // TypeDef + var handle = MetadataTokens.TypeDefinitionHandle (row); + var baseDef = index.Reader.GetTypeDefinition (handle); + return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); + } + case 1: // TypeRef + return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); + default: + return null; + } + } + + /// + /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName). + /// Shared by base type resolution, interface resolution, and any handle-to-name lookup. + /// + (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) + { + switch (handle.Kind) { + case HandleKind.TypeDefinition: { + var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); + return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); + } + case HandleKind.TypeReference: + return ResolveTypeReference ((TypeReferenceHandle)handle, index); + case HandleKind.TypeSpecification: + return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index); + default: + return null; + } + } + + (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index) + { + return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); + } + + string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) + { + // First, check the [Register] attribute's connector arg (3rd arg). + // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] + // where the connector contains the assembly-qualified invoker type name. + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { + var connector = registerInfo.Connector; + // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." + // We want just the type name (before the first comma, if any) + var commaIndex = connector.IndexOf (','); + if (commaIndex > 0) { + return connector.Substring (0, commaIndex).Trim (); + } + if (connector.Length > 0) { + return connector; + } + } + + // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" + var invokerName = $"{typeName}Invoker"; + if (index.TypesByFullName.ContainsKey (invokerName)) { + return invokerName; + } + return null; + } + + public void Dispose () + { + foreach (var index in assemblyCache.Values) { + index.Dispose (); + } + assemblyCache.Clear (); + } + + readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + + /// + /// Check if a type extends a known Java peer (has [Register] or component attribute) + /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles. + /// + bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) + { + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var key = $"{index.AssemblyName}:{fullName}"; + + if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { + return cached; + } + + // Mark as false to prevent cycles, then compute + extendsJavaPeerCache [key] = false; + + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is null) { + return false; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + return false; + } + + // Direct hit: base has [Register] or component attribute + if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) { + extendsJavaPeerCache [key] = true; + return true; + } + if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { + extendsJavaPeerCache [key] = true; + return true; + } + + // Recurse up the hierarchy + var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ExtendsJavaPeer (baseDef, baseIndex); + extendsJavaPeerCache [key] = result; + return result; + } + + /// + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. + /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. + /// Generic backticks are replaced with _. + /// + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + { + var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); + + if (parentJniName is not null) { + var name = $"{parentJniName}_{typeName}"; + return (name, name); + } + + var packageName = GetCrc64PackageName (ns, index.AssemblyName); + var jniName = $"{packageName}/{typeName}"; + + string compatName = ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + + return (jniName, compatName); + } + + /// + /// Builds the type name part (handling nesting) and returns either a parent's + /// registered JNI name or the outermost namespace. + /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types + /// and if a parent has [Register] or a component attribute JNI name, uses that + /// as prefix instead of computing CRC64 from the namespace. + /// + static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) + { + var firstName = index.Reader.GetString (typeDef.Name).Replace ('`', '_'); + + // Fast path: non-nested types (the vast majority) + if (!typeDef.IsNested) { + return (firstName, null, index.Reader.GetString (typeDef.Namespace)); + } + + // Nested type: walk up declaring types, collecting name parts + var nameParts = new List (4) { firstName }; + var current = typeDef; + string? parentJniName = null; + + do { + var parentHandle = current.GetDeclaringType (); + current = index.Reader.GetTypeDefinition (parentHandle); + + // Check if the parent has a registered JNI name + if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !string.IsNullOrEmpty (parentRegister.JniName)) { + parentJniName = parentRegister.JniName; + break; + } + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { + parentJniName = parentAttr.JniName; + break; + } + + nameParts.Add (index.Reader.GetString (current.Name).Replace ('`', '_')); + } while (current.IsNested); + + nameParts.Reverse (); + var typeName = string.Join ("_", nameParts); + var ns = index.Reader.GetString (current.Namespace); + + return (typeName, parentJniName, ns); + } + + static string GetCrc64PackageName (string ns, string assemblyName) + { + // Only Mono.Android preserves the namespace directly + if (assemblyName == "Mono.Android") { + return ns.ToLowerInvariant ().Replace ('.', '/'); + } + + var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); + var hash = System.IO.Hashing.Crc64.Hash (data); + return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs new file mode 100644 index 00000000000..41394034f51 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs @@ -0,0 +1,49 @@ +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Shared logic for resolving fully qualified type names from metadata handles. +/// Used by both and . +/// +static class MetadataTypeNameResolver +{ + internal static string JoinNamespaceAndName (string ns, string name) + { + return ns.Length > 0 ? $"{ns}.{name}" : name; + } + + internal static string JoinNestedTypeName (string parentName, string name) + { + return $"{parentName}+{name}"; + } + + public static string GetFullName (TypeDefinition typeDef, MetadataReader reader) + { + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + if (typeDef.IsNested) { + var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); + var parentName = GetFullName (declaringType, reader); + return JoinNestedTypeName (parentName, name); + } + return JoinNamespaceAndName (ns, name); + } + + public static string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + return GetFullName (reader.GetTypeDefinition (handle), reader); + } + + public static string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference (handle); + var name = reader.GetString (typeRef.Name); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return JoinNestedTypeName (parent, name); + } + var ns = reader.GetString (typeRef.Namespace); + return JoinNamespaceAndName (ns, name); + } +} 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..87ed078adf2 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -0,0 +1,66 @@ +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) + => MetadataTypeNameResolver.GetTypeFromDefinition (reader, handle, rawTypeKind); + + public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromReference (reader, handle, rawTypeKind); + + 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*"; +}