-
Notifications
You must be signed in to change notification settings - Fork 569
[TrimmableTypeMap][Core B] Foundation and AssemblyIndex #10817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
simonrozsival
wants to merge
3
commits into
dotnet:dev/simonrozsival/ttm-core-a-foundation
from
simonrozsival:dev/simonrozsival/ttm-core-b-index
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap; | ||
|
|
||
| static class NullableExtensions | ||
| { | ||
| // The static methods in System.String are not NRT annotated in netstandard2.0, | ||
| // so we need to add our own extension methods to make them nullable aware. | ||
| public static bool IsNullOrEmpty ([NotNullWhen (false)] this string? str) | ||
| { | ||
| return string.IsNullOrEmpty (str); | ||
| } | ||
|
|
||
| public static bool IsNullOrWhiteSpace ([NotNullWhen (false)] this string? str) | ||
| { | ||
| return string.IsNullOrWhiteSpace (str); | ||
| } | ||
| } |
333 changes: 333 additions & 0 deletions
333
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,333 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Reflection.Metadata; | ||
| using System.Reflection.PortableExecutable; | ||
|
|
||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap; | ||
|
|
||
| /// <summary> | ||
| /// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions, | ||
| /// all subsequent lookups are O(1) dictionary lookups. | ||
| /// </summary> | ||
| sealed class AssemblyIndex : IDisposable | ||
| { | ||
| readonly PEReader peReader; | ||
| internal readonly CustomAttributeTypeProvider customAttributeTypeProvider; | ||
|
|
||
| public MetadataReader Reader { get; } | ||
| public string AssemblyName { get; } | ||
| public string FilePath { get; } | ||
|
|
||
| /// <summary> | ||
| /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. | ||
| /// </summary> | ||
| public Dictionary<string, TypeDefinitionHandle> TypesByFullName { get; } = new (StringComparer.Ordinal); | ||
|
|
||
| /// <summary> | ||
| /// Cached [Register] attribute data per type. | ||
| /// </summary> | ||
| public Dictionary<TypeDefinitionHandle, RegisterInfo> RegisterInfoByType { get; } = new (); | ||
|
|
||
| /// <summary> | ||
| /// All custom attribute data per type, pre-parsed for the attributes we care about. | ||
| /// </summary> | ||
| public Dictionary<TypeDefinitionHandle, TypeAttributeInfo> 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 = GetFullName (typeDef, Reader); | ||
| if (fullName.Length == 0) { | ||
| continue; | ||
| } | ||
|
|
||
| TypesByFullName [fullName] = typeHandle; | ||
simonrozsival marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var (registerInfo, attrInfo) = ParseAttributes (typeDef); | ||
|
|
||
| if (attrInfo is not null) { | ||
| AttributesByType [typeHandle] = attrInfo; | ||
| } | ||
|
|
||
| if (registerInfo is not null) { | ||
| RegisterInfoByType [typeHandle] = registerInfo; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| internal 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 $"{parentName}+{name}"; | ||
| } | ||
|
|
||
| if (ns.Length == 0) { | ||
| return name; | ||
| } | ||
|
|
||
| return $"{ns}.{name}"; | ||
| } | ||
|
|
||
| (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) | ||
| { | ||
| RegisterInfo? registerInfo = null; | ||
| TypeAttributeInfo? attrInfo = null; | ||
|
|
||
| foreach (var caHandle in typeDef.GetCustomAttributes ()) { | ||
| var ca = Reader.GetCustomAttribute (caHandle); | ||
simonrozsival marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| var attrName = GetCustomAttributeName (ca, Reader); | ||
|
|
||
| if (attrName is null) { | ||
| continue; | ||
| } | ||
|
|
||
| if (attrName == "RegisterAttribute") { | ||
| registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); | ||
| } else if (attrName == "ExportAttribute") { | ||
| // [Export] methods are not handled yet and supporting them will be implemented later | ||
| } else if (IsKnownComponentAttribute (attrName)) { | ||
| attrInfo ??= CreateTypeAttributeInfo (attrName); | ||
| var componentName = TryGetNameProperty (ca); | ||
| if (componentName is not null) { | ||
| attrInfo.JniName = componentName.Replace ('.', '/'); | ||
| } | ||
| if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { | ||
| applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); | ||
| applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return (registerInfo, attrInfo); | ||
| } | ||
|
|
||
| static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) | ||
| { | ||
| return attrName switch { | ||
| "ActivityAttribute" => new ActivityAttributeInfo (), | ||
| "ServiceAttribute" => new ServiceAttributeInfo (), | ||
| "BroadcastReceiverAttribute" => new BroadcastReceiverAttributeInfo (), | ||
| "ContentProviderAttribute" => new ContentProviderAttributeInfo (), | ||
| "ApplicationAttribute" => new ApplicationAttributeInfo (), | ||
| "InstrumentationAttribute" => new InstrumentationAttributeInfo (), | ||
| _ => throw new ArgumentException ($"Unknown component attribute: {attrName}"), | ||
| }; | ||
| } | ||
|
|
||
| static bool IsKnownComponentAttribute (string attrName) | ||
| { | ||
| return attrName == "ActivityAttribute" | ||
| || attrName == "ServiceAttribute" | ||
| || attrName == "BroadcastReceiverAttribute" | ||
| || attrName == "ContentProviderAttribute" | ||
| || attrName == "ApplicationAttribute" | ||
| || attrName == "InstrumentationAttribute"; | ||
| } | ||
|
|
||
| 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 static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider<string> provider) | ||
| { | ||
| var value = ca.DecodeValue (provider); | ||
|
|
||
| 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 (TryGetNamedBooleanArgument (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 = ca.DecodeValue (customAttributeTypeProvider); | ||
| var typeName = TryGetNamedStringArgument (value, propertyName); | ||
| if (!typeName.IsNullOrEmpty ()) { | ||
| return typeName; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| string? TryGetNameProperty (CustomAttribute ca) | ||
| { | ||
| var value = ca.DecodeValue (customAttributeTypeProvider); | ||
|
|
||
| // Check named arguments first (e.g., [Activity(Name = "...")]) | ||
| var name = TryGetNamedStringArgument (value, "Name"); | ||
| if (!name.IsNullOrEmpty ()) { | ||
| return name; | ||
| } | ||
|
|
||
| // Fall back to first constructor argument (e.g., [CustomJniName("...")]) | ||
| if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !ctorName.IsNullOrEmpty ()) { | ||
| return ctorName; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| static bool TryGetNamedBooleanArgument (CustomAttributeValue<string> value, string argumentName, out bool argumentValue) | ||
| { | ||
| foreach (var named in value.NamedArguments) { | ||
| if (named.Name == argumentName && named.Value is bool boolValue) { | ||
| argumentValue = boolValue; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| argumentValue = false; | ||
| return false; | ||
| } | ||
|
|
||
| static string? TryGetNamedStringArgument (CustomAttributeValue<string> value, string argumentName) | ||
| { | ||
| foreach (var named in value.NamedArguments) { | ||
| if (named.Name == argumentName && named.Value is string stringValue) { | ||
| return stringValue; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public void Dispose () | ||
| { | ||
| peReader.Dispose (); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parsed [Register] attribute data for a type or method. | ||
| /// </summary> | ||
| sealed record RegisterInfo | ||
| { | ||
| public required string JniName { get; init; } | ||
| public string? Signature { get; init; } | ||
| public string? Connector { get; init; } | ||
| public bool DoNotGenerateAcw { get; init; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parsed [Export] attribute data for a method. | ||
| /// </summary> | ||
| sealed record ExportInfo | ||
| { | ||
| public IReadOnlyList<string>? ThrownNames { get; init; } | ||
| public string? SuperArgumentsString { get; init; } | ||
| } | ||
|
|
||
| abstract class TypeAttributeInfo | ||
| { | ||
| protected TypeAttributeInfo (string attributeName) | ||
| { | ||
| AttributeName = attributeName; | ||
| } | ||
|
|
||
| public string AttributeName { get; } | ||
| public string? JniName { get; set; } | ||
| } | ||
|
|
||
| sealed class ActivityAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public ActivityAttributeInfo () : base ("ActivityAttribute") | ||
| { | ||
| } | ||
| } | ||
|
|
||
| sealed class ServiceAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public ServiceAttributeInfo () : base ("ServiceAttribute") | ||
| { | ||
| } | ||
| } | ||
|
|
||
| sealed class BroadcastReceiverAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public BroadcastReceiverAttributeInfo () : base ("BroadcastReceiverAttribute") | ||
| { | ||
| } | ||
| } | ||
|
|
||
| sealed class ContentProviderAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public ContentProviderAttributeInfo () : base ("ContentProviderAttribute") | ||
| { | ||
| } | ||
| } | ||
|
|
||
| sealed class InstrumentationAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public InstrumentationAttributeInfo () : base ("InstrumentationAttribute") | ||
| { | ||
| } | ||
| } | ||
|
|
||
| sealed class ApplicationAttributeInfo : TypeAttributeInfo | ||
| { | ||
| public ApplicationAttributeInfo () : base ("ApplicationAttribute") | ||
| { | ||
| } | ||
|
|
||
| public string? BackupAgent { get; set; } | ||
| public string? ManageSpaceActivity { get; set; } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.