Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,20 @@
<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>

<ItemGroup>
<Compile Include="..\..\src-ThirdParty\System.Runtime.CompilerServices\CompilerFeaturePolyfills.cs" Link="CompilerFeaturePolyfills.cs" />
</ItemGroup>

</Project>
262 changes: 262 additions & 0 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <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;
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);
} 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<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 RegisterInfo ParseRegisterAttribute (CustomAttribute ca)
{
return ParseRegisterInfo (DecodeAttribute (ca));
}

internal CustomAttributeValue<string> DecodeAttribute (CustomAttribute ca)
{
return ca.DecodeValue (customAttributeTypeProvider);
}

RegisterInfo ParseRegisterInfo (CustomAttributeValue<string> 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<bool> (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<string> (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<T> (CustomAttributeValue<string> 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 ();
}
}

/// <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