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 Xamarin.Android.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj" />
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj" />
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj" />
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj" />
<Project Path="tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj" />
<Project Path="tests/Xamarin.Android.Tools.Aidl-Tests/Xamarin.Android.Tools.Aidl-Tests.csproj" />
Expand Down
18 changes: 17 additions & 1 deletion src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Microsoft.Android.Build.Tasks
{
/// <summary>
/// Allocation-free helpers for rendering bytes as hexadecimal.
/// Allocation-free helpers for rendering values as hexadecimal.
/// </summary>
/// <remarks>
/// This file is also linked into <c>Microsoft.Android.Sdk.TrimmableTypeMap</c>, which
Expand Down Expand Up @@ -65,6 +66,21 @@ public static void WriteHex (TextWriter writer, byte value, bool upperCase = tru
writer.Write (GetHexValue (value & 0x0f, upperCase));
}

/// <summary>
/// Append <paramref name="value"/> to <paramref name="builder"/> as exactly four
/// hexadecimal digits, without allocating.
/// </summary>
public static void WriteHex (StringBuilder builder, ushort value, bool upperCase = true)
{
if (builder == null)
throw new ArgumentNullException (nameof (builder));

builder.Append (GetHexValue (value >> 12, upperCase));
builder.Append (GetHexValue ((value >> 8) & 0x0f, upperCase));
builder.Append (GetHexValue ((value >> 4) & 0x0f, upperCase));
builder.Append (GetHexValue (value & 0x0f, upperCase));
}

/// <summary>
/// Convert <paramref name="bytes"/> to a hexadecimal string, without allocating
/// intermediate strings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void Generate (JavaPeerInfo type, TextWriter writer, string? applicationJ
{
writer.NewLine = "\n";
WritePackageDeclaration (type, writer);
WriteAnnotations (type.Annotations, writer);
WriteClassDeclaration (type, writer, applicationJavaClass);
WriteStaticInitializer (type, writer);
WriteConstructors (type, writer);
Expand Down Expand Up @@ -176,6 +177,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams);
string args = FormatArgumentList (ctorParams);

WriteAnnotations (ctor.Annotations, writer);
writer.Write ($$"""
public {{simpleClassName}} ({{parameters}})
{
Expand Down Expand Up @@ -212,6 +214,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
static void WriteFields (JavaPeerInfo type, TextWriter writer)
{
foreach (var field in type.JavaFields) {
WriteAnnotations (field.Annotations, writer);
writer.Write ('\t');
writer.Write (field.Visibility);
writer.Write (' ');
Expand Down Expand Up @@ -257,8 +260,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
}

if (method.Connector != null && !method.IsExport) {
writer.WriteLine ();
WriteAnnotations (method.Annotations, writer);
writer.Write ($$"""

@Override
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
{
Expand All @@ -270,8 +274,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
} else {
string access = method.IsExport && method.JavaAccess != null ? method.JavaAccess : "public";
string staticKeyword = method.IsStatic ? "static " : "";
writer.WriteLine ();
WriteAnnotations (method.Annotations, writer);
writer.Write ($$"""

{{access}} {{staticKeyword}}{{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
{
{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
Expand All @@ -283,6 +288,29 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
}
}

static void WriteAnnotations (IReadOnlyList<JavaAnnotationInfo> annotations, TextWriter writer)
{
foreach (var annotation in annotations) {
writer.Write ('@');
writer.Write (annotation.Name);
if (annotation.Properties.Count > 0) {
writer.Write (" (");
bool first = true;
foreach (var property in annotation.Properties) {
if (!first) {
writer.Write (", ");
}
writer.Write (property.Key);
writer.Write (" = ");
writer.Write (property.Value);
first = false;
}
writer.Write (')');
}
writer.WriteLine ();
}
}

static void WriteGCUserPeerMethods (TextWriter writer)
{
writer.Write ("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ sealed record ExportInfo
{
public IReadOnlyList<string>? ThrownNames { get; init; }
public string? SuperArgumentsString { get; init; }
public bool IsField { get; init; }
public IReadOnlyList<ExportParameterKindInfo> ParameterKinds { get; init; } = [];
public ExportParameterKindInfo ReturnKind { get; init; }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection.Metadata;
using System.Text;
using Microsoft.Android.Build.Tasks;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

sealed class JavaAnnotationParser
{
sealed record AnnotationTypeInfo (string JavaName, IReadOnlyDictionary<string, string> PropertyNames);

static readonly IReadOnlyList<JavaAnnotationInfo> noAnnotations = [];

readonly IReadOnlyDictionary<string, AssemblyIndex> assemblies;
readonly Func<string, string?> resolveTypeName;
readonly Dictionary<(AssemblyIndex Index, EntityHandle Type), AnnotationTypeInfo?> annotationTypes = new ();

public JavaAnnotationParser (IReadOnlyDictionary<string, AssemblyIndex> assemblies, Func<string, string?> resolveTypeName)
{
this.assemblies = assemblies;
this.resolveTypeName = resolveTypeName;
}

public IReadOnlyList<JavaAnnotationInfo> Parse (CustomAttributeHandleCollection attributes, AssemblyIndex index)
{
List<JavaAnnotationInfo>? annotations = null;
foreach (var attributeHandle in attributes) {
var attribute = index.Reader.GetCustomAttribute (attributeHandle);
var annotationType = GetAnnotationType (attribute, index);
if (annotationType is null) {
continue;
}

annotations ??= [];
annotations.Add (new JavaAnnotationInfo {
Name = annotationType.JavaName,
Properties = GetProperties (attribute, index, annotationType),
});
}
return annotations ?? noAnnotations;
}

static string? GetJavaName (TypeDefinition attributeType, AssemblyIndex index)
{
foreach (var markerHandle in attributeType.GetCustomAttributes ()) {
var marker = index.Reader.GetCustomAttribute (markerHandle);
if (!AssemblyIndex.IsCustomAttributeMatch (marker, index.Reader, "Android.Runtime", "AnnotationAttribute")) {
continue;
}

var value = index.DecodeAttribute (marker);
return value.FixedArguments.Length > 0 ? value.FixedArguments [0].Value as string : null;
}
return null;
}

IReadOnlyList<KeyValuePair<string, string>> GetProperties (
CustomAttribute attribute,
AssemblyIndex index,
AnnotationTypeInfo annotationType)
{
var properties = new List<KeyValuePair<string, string>> ();
foreach (var property in index.DecodeAttribute (attribute).NamedArguments) {
if (property.Kind != CustomAttributeNamedArgumentKind.Property || property.Name is null) {
continue;
}
var propertyName = annotationType.PropertyNames.TryGetValue (property.Name, out var javaName)
? javaName
: property.Name;
properties.Add (new KeyValuePair<string, string> (
propertyName,
ManagedValueToJavaSource (property.Type, property.Value)
));
}
return properties;
}

static IReadOnlyDictionary<string, string> GetJavaPropertyNames (TypeDefinition attributeType, AssemblyIndex index)
{
var names = new Dictionary<string, string> (StringComparer.Ordinal);
foreach (var propertyHandle in attributeType.GetProperties ()) {
var property = index.Reader.GetPropertyDefinition (propertyHandle);
var managedName = index.Reader.GetString (property.Name);
foreach (var attributeHandle in property.GetCustomAttributes ()) {
var attribute = index.Reader.GetCustomAttribute (attributeHandle);
if (!AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Android.Runtime", "RegisterAttribute")) {
continue;
}
var value = index.DecodeAttribute (attribute);
if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string javaName) {
names [managedName] = javaName;
}
break;
}
}
return names;
}

AnnotationTypeInfo? GetAnnotationType (CustomAttribute attribute, AssemblyIndex index)
{
EntityHandle typeHandle = default;
if (attribute.Constructor.Kind == HandleKind.MethodDefinition) {
typeHandle = index.Reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType ();
} else if (attribute.Constructor.Kind == HandleKind.MemberReference) {
typeHandle = index.Reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor).Parent;
}

var key = (index, typeHandle);
if (typeHandle.IsNil || annotationTypes.TryGetValue (key, out var cached) && cached is null) {
return null;
}
if (cached is not null) {
return cached;
}

TypeDefinition attributeType;
AssemblyIndex attributeIndex;
if (typeHandle.Kind == HandleKind.TypeDefinition) {
attributeType = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)typeHandle);
attributeIndex = index;
} else if (typeHandle.Kind == HandleKind.TypeReference) {
var typeReference = MetadataTypeNameResolver.GetTypeRefFromReference (
index.Reader,
(TypeReferenceHandle)typeHandle,
index.AssemblyName,
rawTypeKind: 0
);
if (!assemblies.TryGetValue (typeReference.AssemblyName, out attributeIndex) ||
!attributeIndex.TypesByFullName.TryGetValue (typeReference.ManagedTypeName, out var resolvedHandle)) {
annotationTypes [key] = null;
return null;
}
attributeType = attributeIndex.Reader.GetTypeDefinition (resolvedHandle);
} else {
annotationTypes [key] = null;
return null;
}

var javaName = GetJavaName (attributeType, attributeIndex);
var result = javaName.IsNullOrEmpty ()
? null
: new AnnotationTypeInfo (javaName, GetJavaPropertyNames (attributeType, attributeIndex));
annotationTypes [key] = result;
return result;
}

string ManagedValueToJavaSource (string managedType, object? value)
{
if (value is null) {
return "null";
}
if (managedType == "String" || managedType == "System.String") {
return ToJavaStringLiteral (value.ToString () ?? "");
}
if (managedType == "System.Type" && value is string typeName) {
var javaName = resolveTypeName (typeName);
if (javaName is not null) {
return JniSignatureHelper.JniNameToJavaName (javaName) + ".class";
}
throw new InvalidOperationException ($"Java annotation type value '{typeName}' does not resolve to a Java peer.");
}
if (value is bool boolean) {
return boolean ? "true" : "false";
}
if (value is IFormattable formattable) {
return formattable.ToString (null, CultureInfo.InvariantCulture) ?? "";
}
return value.ToString () ?? "";
}

static string ToJavaStringLiteral (string value)
{
var builder = new StringBuilder (value.Length + 2);
builder.Append ('"');
foreach (char c in value) {
switch (c) {
case '"':
builder.Append ("\\\"");
break;
case '\\':
builder.Append ("\\\\");
break;
case '\b':
builder.Append ("\\b");
break;
case '\t':
builder.Append ("\\t");
break;
case '\n':
builder.Append ("\\n");
break;
case '\f':
builder.Append ("\\f");
break;
case '\r':
builder.Append ("\\r");
break;
default:
if (char.IsControl (c)) {
builder.Append ("\\u");
HexUtilities.WriteHex (builder, c, upperCase: false);
} else {
builder.Append (c);
}
break;
}
}
builder.Append ('"');
return builder.ToString ();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public sealed record JavaPeerInfo
/// </summary>
public IReadOnlyList<string> ImplementedInterfaceJavaNames { get; init; } = Array.Empty<string> ();

/// <summary>
/// Java annotations forwarded from managed custom attributes decorated with
/// <c>Android.Runtime.AnnotationAttribute</c>.
/// </summary>
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];

public bool IsInterface { get; init; }
public bool IsAbstract { get; init; }

Expand Down Expand Up @@ -296,6 +302,20 @@ public sealed record MarshalMethodInfo
/// <c>new virtual</c> while reusing the same JNI name and signature.
/// </summary>
public bool CallManagedMethodDirectly { get; init; }

/// <summary>
/// Java annotations forwarded from the managed method or constructor.
/// </summary>
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
}

/// <summary>
/// Describes a Java annotation forwarded from a managed custom attribute.
/// </summary>
public sealed record JavaAnnotationInfo
{
public required string Name { get; init; }
public IReadOnlyList<KeyValuePair<string, string>> Properties { get; init; } = [];
}

/// <summary>
Expand Down Expand Up @@ -344,6 +364,11 @@ public sealed record JavaConstructorInfo
/// True when this Java constructor has a matching public managed constructor on the target type.
/// </summary>
public bool HasMatchingManagedCtor { get; init; }

/// <summary>
/// Java annotations forwarded from the managed constructor.
/// </summary>
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
}

/// <summary>
Expand Down Expand Up @@ -376,6 +401,11 @@ public sealed record JavaFieldInfo
/// Whether the field is static.
/// </summary>
public bool IsStatic { get; init; }

/// <summary>
/// Java annotations forwarded from the managed field initializer method.
/// </summary>
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
}

/// <summary>
Expand Down
Loading
Loading