Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<MicrosoftTemplateEngineAuthoringTasksPackageVersion>11.0.100-preview.1.26076.102</MicrosoftTemplateEngineAuthoringTasksPackageVersion>
<MicrosoftDotNetCecilPackageVersion>0.11.5-preview.26076.102</MicrosoftDotNetCecilPackageVersion>
<SystemIOHashingPackageVersion>9.0.4</SystemIOHashingPackageVersion>
<SystemReflectionMetadataPackageVersion>11.0.0-preview.1.26104.118</SystemReflectionMetadataPackageVersion>
<!-- Previous .NET Android version -->
<MicrosoftNETSdkAndroidManifest100100PackageVersion>36.1.30</MicrosoftNETSdkAndroidManifest100100PackageVersion>
<AndroidNetPreviousVersion>$(MicrosoftNETSdkAndroidManifest100100PackageVersion)</AndroidNetPreviousVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Configuration.props" />

<PropertyGroup>
<TargetFramework>$(TargetFrameworkNETStandard)</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<RootNamespace>Microsoft.Android.Sdk.TrimmableTypeMap</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.IO.Hashing" Version="$(SystemIOHashingPackageVersion)" />
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataPackageVersion)" />
</ItemGroup>

</Project>
265 changes: 265 additions & 0 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
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 = 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, customAttributeTypeProvider);
} 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 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 readonly HashSet<string> KnownComponentAttributes = new (StringComparer.Ordinal) {
"ActivityAttribute",
"ServiceAttribute",
"BroadcastReceiverAttribute",
"ContentProviderAttribute",
"ApplicationAttribute",
"InstrumentationAttribute",
};
Comment on lines +114 to +121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I believe there is an interface, Java.Interop.IJniNameProviderAttribute, that the old code was looking for? All these attributes implement it. I think the idea was if Android introduced a new attribute, it could work without updating this list?

There is probably not any customers using Java.Interop.IJniNameProviderAttribute, but do we need a code path that looks for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already had this implemented but it complicates things quite a lot. It's not trivial to check if a type implements an interface with SRM. For now, I chose to ignore it. It shouldn't have practical implications, but I agree that we shold eventually implement it for completeness. I'll make sure to add this to the issue for future work. Is that OK?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So efficient way to implement this:

  • only check custom attributes on Java.Lang.Object or Java.Lang.Throwable subclasses
  • ignore attributes which don't have the Name named argument
  • ignore well known attributes (Register, Application, Activity, ...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the "Future work" section of #10788


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 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 = TryGetNamedArgument<string> (value, propertyName);
if (!string.IsNullOrEmpty (typeName)) {
return typeName;
}
return null;
}

string? TryGetNameProperty (CustomAttribute ca)
{
var value = ca.DecodeValue (customAttributeTypeProvider);

// Check named arguments first (e.g., [Activity(Name = "...")])
var name = TryGetNamedArgument<string> (value, "Name");
if (!string.IsNullOrEmpty (name)) {
return name;
}

// 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 T? TryGetNamedArgument<T> (CustomAttributeValue<string> value, string argumentName) where T : class
{
foreach (var named in value.NamedArguments) {
if (named.Name == argumentName && named.Value is T typedValue) {
return typedValue;
}
}
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;
}

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; }
}

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; }
}
Loading