From 59897dd239ecae30fff3da19854d9072aa3cd533 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 01:07:10 +0100 Subject: [PATCH 1/4] [TrimmableTypeMap][Core C] Scanner execution logic preview (<1k) --- .../Scanner/JavaPeerScanner.cs | 718 ++++++++++++++++++ 1 file changed, 718 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs 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..fe15dfb159b --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +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 activationCtorCache = new (StringComparer.Ordinal); + + /// + /// 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, 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 = ns.Length > 0 ? ns + "." + name : 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 (parentFullName + "+" + name, assemblyName); + } + default: { + var fullName = ns.Length > 0 ? ns + "." + name : 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 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies + // and re-classify any attributes that weren't recognized in the initial pass + // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). + var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); + foreach (var index in assemblyCache.Values) { + mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); + } + foreach (var index in assemblyCache.Values) { + index.ReclassifyAttributes (mergedJniNameProviders); + } + + // 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) { + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); + } + } + } + + static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) + { + if (managedTypeName == null) { + 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) { + managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); + } + + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + peer.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 != null && !string.IsNullOrEmpty (registerInfo.JniName)) { + jniName = registerInfo.JniName; + compatJniName = jniName; + doNotGenerateAcw = registerInfo.DoNotGenerateAcw; + } else if (attrInfo?.ComponentAttributeJniName != null) { + // User type with [Activity(Name = "...")] but no [Register] + jniName = attrInfo.ComponentAttributeJniName; + 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 = AssemblyIndex.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?.HasComponentAttribute ?? false; + 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); + var registerInfo = TryGetMethodRegisterInfo (methodDef, index); + if (registerInfo == null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index); + } + + // 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 == 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) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature == null && registerInfo.Connector == 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 = registerInfo.ThrownNames, + SuperArgumentsString = registerInfo.SuperArgumentsString, + }); + } + + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo == null) { + return null; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + // First try [Register] attribute + var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); + if (registerJniName != 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 != null) { + result.Add (ifaceJniName); + } + } + + return result; + } + + string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) + { + var resolved = ResolveEntityHandle (interfaceHandle, index); + return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + } + + static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + { + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + } + + if (attrName == "ExportAttribute") { + return ParseExportAttribute (ca, methodDef, index); + } + } + return null; + } + + 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 AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + } + } + return null; + } + + static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = ca.DecodeValue (index.customAttributeTypeProvider); + + // [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 string[] names) { + thrownNames = new List (names); + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (exportName == null || exportName.Length == 0) { + exportName = index.Reader.GetString (methodDef.Name); + } + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return new RegisterInfo (exportName, jniSig, null, false, + 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) + { + if (activationCtorCache.TryGetValue (typeName, out var cached)) { + return cached; + } + + // Check this type's constructors + var ownCtor = FindActivationCtorOnType (typeDef, index); + if (ownCtor != null) { + var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; + activationCtorCache [typeName] = info; + return info; + } + + // Walk base type hierarchy + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo != 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 != null) { + activationCtorCache [typeName] = 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 (AssemblyIndex.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 (AssemblyIndex.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 != 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 = AssemblyIndex.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 == 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.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { + 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 != 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.ComponentAttributeJniName != null) { + parentJniName = parentAttr.ComponentAttributeJniName; + 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 (); + } +} From e905c7e53c410e63abaf5a61a08963986d8d3204 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 09:09:26 +0100 Subject: [PATCH 2/4] [TrimmableTypeMap][Core C] Apply review style and type-model updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fe15dfb159b..6a607fb42c5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -46,16 +46,16 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan switch (scope.Kind) { case HandleKind.AssemblyReference: { var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); - var fullName = ns.Length > 0 ? ns + "." + name : name; + var fullName = ns.Length > 0 ? $"{ns}.{name}" : 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 (parentFullName + "+" + name, assemblyName); + return ($"{parentFullName}+{name}", assemblyName); } default: { - var fullName = ns.Length > 0 ? ns + "." + name : name; + var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; return (fullName, index.AssemblyName); } } @@ -118,15 +118,17 @@ static void ForceUnconditionalCrossReferences (Dictionary { foreach (var index in assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { - ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); - ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); + } } } } static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) { - if (managedTypeName == null) { + if (managedTypeName is null) { return; } @@ -165,13 +167,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); - if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) { + if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; - } else if (attrInfo?.ComponentAttributeJniName != null) { + } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] - jniName = attrInfo.ComponentAttributeJniName; + jniName = attrInfo.JniName; compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. @@ -189,7 +191,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; - var isUnconditional = attrInfo?.HasComponentAttribute ?? false; + var isUnconditional = attrInfo is not null; string? invokerTypeName = null; // Resolve base Java type name @@ -237,19 +239,18 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI // Single pass over methods: collect marshal methods (including constructors) foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - var registerInfo = TryGetMethodRegisterInfo (methodDef, index); - if (registerInfo == null) { + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo)) { continue; } - AddMarshalMethod (methods, registerInfo, methodDef, index); + 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 == null) { + if (propRegister is null) { continue; } @@ -263,10 +264,10 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI return methods; } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) + 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 == null && registerInfo.Connector == null) { + if (registerInfo.Signature is null && registerInfo.Connector is null) { return; } @@ -276,15 +277,15 @@ static void AddMarshalMethod (List methods, RegisterInfo regi Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", - ThrownNames = registerInfo.ThrownNames, - SuperArgumentsString = registerInfo.SuperArgumentsString, + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, }); } string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) { var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo == null) { + if (baseInfo is null) { return null; } @@ -292,7 +293,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi // First try [Register] attribute var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); - if (registerJniName != null) { + if (registerJniName is not null) { return registerJniName; } @@ -323,24 +324,28 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) { var resolved = ResolveEntityHandle (interfaceHandle, index); - return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + 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") { - return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + return true; } if (attrName == "ExportAttribute") { - return ParseExportAttribute (ca, methodDef, index); + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; } } - return null; + registerInfo = null; + return false; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) @@ -356,7 +361,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem return null; } - static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = ca.DecodeValue (index.customAttributeTypeProvider); @@ -380,7 +385,7 @@ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition m } } - if (exportName == null || exportName.Length == 0) { + if (string.IsNullOrEmpty (exportName)) { exportName = index.Reader.GetString (methodDef.Name); } @@ -388,8 +393,10 @@ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition m var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); var jniSig = BuildJniSignatureFromManaged (sig); - return new RegisterInfo (exportName, jniSig, null, false, - thrownNames: thrownNames, superArgumentsString: superArguments); + return ( + new RegisterInfo (exportName, jniSig, null, false), + new ExportInfo (thrownNames, superArguments) + ); } static string BuildJniSignatureFromManaged (MethodSignature sig) @@ -423,7 +430,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case "System.String": return "Ljava/lang/String;"; default: if (managedType.EndsWith ("[]")) { - return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; } return "Ljava/lang/Object;"; } @@ -437,7 +444,7 @@ static string ManagedTypeToJniDescriptor (string managedType) // Check this type's constructors var ownCtor = FindActivationCtorOnType (typeDef, index); - if (ownCtor != null) { + if (ownCtor is not null) { var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; activationCtorCache [typeName] = info; return info; @@ -445,12 +452,12 @@ static string ManagedTypeToJniDescriptor (string managedType) // Walk base type hierarchy var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo != null) { + 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 != null) { + if (result is not null) { activationCtorCache [typeName] = result; } return result; @@ -558,7 +565,7 @@ static string ManagedTypeToJniDescriptor (string managedType) // 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 != null) { + 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) @@ -572,7 +579,7 @@ static string ManagedTypeToJniDescriptor (string managedType) } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" - var invokerName = typeName + "Invoker"; + var invokerName = $"{typeName}Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { return invokerName; } @@ -596,7 +603,7 @@ public void Dispose () bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); - var key = index.AssemblyName + ":" + fullName; + var key = $"{index.AssemblyName}:{fullName}"; if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; @@ -606,7 +613,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = false; var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo == null) { + if (baseInfo is null) { return false; } @@ -621,7 +628,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = true; return true; } - if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { + if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { extendsJavaPeerCache [key] = true; return true; } @@ -644,17 +651,17 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); - if (parentJniName != null) { - var name = parentJniName + "_" + typeName; + if (parentJniName is not null) { + var name = $"{parentJniName}_{typeName}"; return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - var jniName = packageName + "/" + typeName; + var jniName = $"{packageName}/{typeName}"; string compatName = ns.Length == 0 ? typeName - : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName; + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; return (jniName, compatName); } @@ -689,8 +696,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) parentJniName = parentRegister.JniName; break; } - if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) { - parentJniName = parentAttr.ComponentAttributeJniName; + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { + parentJniName = parentAttr.JniName; break; } @@ -711,8 +718,8 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName); + var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); var hash = System.IO.Hashing.Crc64.Hash (data); - return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } } From 6445238900e9a77e10d98883e64a1fa9cb6df6e2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 09:11:45 +0100 Subject: [PATCH 3/4] [TrimmableTypeMap][Core C] Use null pattern in interface JNI check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 6a607fb42c5..3a283321c8f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -313,7 +313,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem foreach (var implHandle in interfaceImpls) { var impl = index.Reader.GetInterfaceImplementation (implHandle); var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); - if (ifaceJniName != null) { + if (ifaceJniName is not null) { result.Add (ifaceJniName); } } From b6895af3fb6a0ff7a8e5b1bf73a4df553c0afeee Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:08:21 +0100 Subject: [PATCH 4/4] [TrimmableTypeMap][Core C] Adapt scanner to refactored AssemblyIndex - Remove Phase 1b (JniNameProviderAttribute merging/reclassification) - Use record 'with' expression for immutable ForceUnconditionalIfPresent - Use .IsNullOrEmpty() extension method - Use record init syntax for RegisterInfo and ExportInfo construction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 3a283321c8f..87885ecdce3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -85,17 +85,6 @@ public List Scan (IReadOnlyList assemblyPaths) assemblyCache [index.AssemblyName] = index; } - // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies - // and re-classify any attributes that weren't recognized in the initial pass - // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). - var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); - foreach (var index in assemblyCache.Values) { - mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); - } - foreach (var index in assemblyCache.Values) { - index.ReclassifyAttributes (mergedJniNameProviders); - } - // Phase 2: Analyze types using cached indices var resultsByManagedName = new Dictionary (StringComparer.Ordinal); @@ -140,7 +129,7 @@ static void ForceUnconditionalIfPresent (Dictionary result } if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { - peer.IsUnconditional = true; + resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; } } @@ -167,7 +156,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); - if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + if (registerInfo is not null && !registerInfo.JniName.IsNullOrEmpty ()) { jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; @@ -385,7 +374,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex } } - if (string.IsNullOrEmpty (exportName)) { + if (exportName.IsNullOrEmpty ()) { exportName = index.Reader.GetString (methodDef.Name); } @@ -394,8 +383,8 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex var jniSig = BuildJniSignatureFromManaged (sig); return ( - new RegisterInfo (exportName, jniSig, null, false), - new ExportInfo (thrownNames, superArguments) + new RegisterInfo { JniName = exportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } ); } @@ -692,7 +681,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) 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)) { + if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !parentRegister.JniName.IsNullOrEmpty ()) { parentJniName = parentRegister.JniName; break; }