Skip to content

Commit c88edf2

Browse files
[NativeAOT] Forward JCW Java annotations (#12549)
Fixes #12542 ## Summary - forward Java annotations from managed custom attributes through the trimmable NativeAOT JCW pipeline - preserve annotations on types, methods, registered constructors, property overrides, and exported fields - cache annotation metadata and support cross-assembly attribute definitions such as `Android.Webkit.JavascriptInterfaceAttribute` - add regression coverage matching the app-to-`Mono.Android` assembly boundary ## Testing - `dotnet test tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal` (773 passed)
1 parent cbf06ef commit c88edf2

16 files changed

Lines changed: 513 additions & 45 deletions

File tree

Xamarin.Android.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj" />
6363
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj" />
6464
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj" />
65+
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj" />
6566
<Project Path="tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj" />
6667
<Project Path="tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj" />
6768
<Project Path="tests/Xamarin.Android.Tools.Aidl-Tests/Xamarin.Android.Tools.Aidl-Tests.csproj" />

src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
using System;
33
using System.Diagnostics;
44
using System.IO;
5+
using System.Text;
56

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

69+
/// <summary>
70+
/// Append <paramref name="value"/> to <paramref name="builder"/> as exactly four
71+
/// hexadecimal digits, without allocating.
72+
/// </summary>
73+
public static void WriteHex (StringBuilder builder, ushort value, bool upperCase = true)
74+
{
75+
if (builder == null)
76+
throw new ArgumentNullException (nameof (builder));
77+
78+
builder.Append (GetHexValue (value >> 12, upperCase));
79+
builder.Append (GetHexValue ((value >> 8) & 0x0f, upperCase));
80+
builder.Append (GetHexValue ((value >> 4) & 0x0f, upperCase));
81+
builder.Append (GetHexValue (value & 0x0f, upperCase));
82+
}
83+
6884
/// <summary>
6985
/// Convert <paramref name="bytes"/> to a hexadecimal string, without allocating
7086
/// intermediate strings.

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public void Generate (JavaPeerInfo type, TextWriter writer, string? applicationJ
6565
{
6666
writer.NewLine = "\n";
6767
WritePackageDeclaration (type, writer);
68+
WriteAnnotations (type.Annotations, writer);
6869
WriteClassDeclaration (type, writer, applicationJavaClass);
6970
WriteStaticInitializer (type, writer);
7071
WriteConstructors (type, writer);
@@ -176,6 +177,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
176177
string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams);
177178
string args = FormatArgumentList (ctorParams);
178179

180+
WriteAnnotations (ctor.Annotations, writer);
179181
writer.Write ($$"""
180182
public {{simpleClassName}} ({{parameters}})
181183
{
@@ -212,6 +214,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
212214
static void WriteFields (JavaPeerInfo type, TextWriter writer)
213215
{
214216
foreach (var field in type.JavaFields) {
217+
WriteAnnotations (field.Annotations, writer);
215218
writer.Write ('\t');
216219
writer.Write (field.Visibility);
217220
writer.Write (' ');
@@ -257,8 +260,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
257260
}
258261

259262
if (method.Connector != null && !method.IsExport) {
263+
writer.WriteLine ();
264+
WriteAnnotations (method.Annotations, writer);
260265
writer.Write ($$"""
261-
262266
@Override
263267
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
264268
{
@@ -270,8 +274,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
270274
} else {
271275
string access = method.IsExport && method.JavaAccess != null ? method.JavaAccess : "public";
272276
string staticKeyword = method.IsStatic ? "static " : "";
277+
writer.WriteLine ();
278+
WriteAnnotations (method.Annotations, writer);
273279
writer.Write ($$"""
274-
275280
{{access}} {{staticKeyword}}{{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
276281
{
277282
{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
@@ -283,6 +288,29 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
283288
}
284289
}
285290

291+
static void WriteAnnotations (IReadOnlyList<JavaAnnotationInfo> annotations, TextWriter writer)
292+
{
293+
foreach (var annotation in annotations) {
294+
writer.Write ('@');
295+
writer.Write (annotation.Name);
296+
if (annotation.Properties.Count > 0) {
297+
writer.Write (" (");
298+
bool first = true;
299+
foreach (var property in annotation.Properties) {
300+
if (!first) {
301+
writer.Write (", ");
302+
}
303+
writer.Write (property.Key);
304+
writer.Write (" = ");
305+
writer.Write (property.Value);
306+
first = false;
307+
}
308+
writer.Write (')');
309+
}
310+
writer.WriteLine ();
311+
}
312+
}
313+
286314
static void WriteGCUserPeerMethods (TextWriter writer)
287315
{
288316
writer.Write ("""

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ sealed record ExportInfo
678678
{
679679
public IReadOnlyList<string>? ThrownNames { get; init; }
680680
public string? SuperArgumentsString { get; init; }
681+
public bool IsField { get; init; }
681682
public IReadOnlyList<ExportParameterKindInfo> ParameterKinds { get; init; } = [];
682683
public ExportParameterKindInfo ReturnKind { get; init; }
683684
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Reflection.Metadata;
5+
using System.Text;
6+
using Microsoft.Android.Build.Tasks;
7+
8+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
9+
10+
sealed class JavaAnnotationParser
11+
{
12+
sealed record AnnotationTypeInfo (string JavaName, IReadOnlyDictionary<string, string> PropertyNames);
13+
14+
static readonly IReadOnlyList<JavaAnnotationInfo> noAnnotations = [];
15+
16+
readonly IReadOnlyDictionary<string, AssemblyIndex> assemblies;
17+
readonly Func<string, string?> resolveTypeName;
18+
readonly Dictionary<(AssemblyIndex Index, EntityHandle Type), AnnotationTypeInfo?> annotationTypes = new ();
19+
20+
public JavaAnnotationParser (IReadOnlyDictionary<string, AssemblyIndex> assemblies, Func<string, string?> resolveTypeName)
21+
{
22+
this.assemblies = assemblies;
23+
this.resolveTypeName = resolveTypeName;
24+
}
25+
26+
public IReadOnlyList<JavaAnnotationInfo> Parse (CustomAttributeHandleCollection attributes, AssemblyIndex index)
27+
{
28+
List<JavaAnnotationInfo>? annotations = null;
29+
foreach (var attributeHandle in attributes) {
30+
var attribute = index.Reader.GetCustomAttribute (attributeHandle);
31+
var annotationType = GetAnnotationType (attribute, index);
32+
if (annotationType is null) {
33+
continue;
34+
}
35+
36+
annotations ??= [];
37+
annotations.Add (new JavaAnnotationInfo {
38+
Name = annotationType.JavaName,
39+
Properties = GetProperties (attribute, index, annotationType),
40+
});
41+
}
42+
return annotations ?? noAnnotations;
43+
}
44+
45+
static string? GetJavaName (TypeDefinition attributeType, AssemblyIndex index)
46+
{
47+
foreach (var markerHandle in attributeType.GetCustomAttributes ()) {
48+
var marker = index.Reader.GetCustomAttribute (markerHandle);
49+
if (!AssemblyIndex.IsCustomAttributeMatch (marker, index.Reader, "Android.Runtime", "AnnotationAttribute")) {
50+
continue;
51+
}
52+
53+
var value = index.DecodeAttribute (marker);
54+
return value.FixedArguments.Length > 0 ? value.FixedArguments [0].Value as string : null;
55+
}
56+
return null;
57+
}
58+
59+
IReadOnlyList<KeyValuePair<string, string>> GetProperties (
60+
CustomAttribute attribute,
61+
AssemblyIndex index,
62+
AnnotationTypeInfo annotationType)
63+
{
64+
var properties = new List<KeyValuePair<string, string>> ();
65+
foreach (var property in index.DecodeAttribute (attribute).NamedArguments) {
66+
if (property.Kind != CustomAttributeNamedArgumentKind.Property || property.Name is null) {
67+
continue;
68+
}
69+
var propertyName = annotationType.PropertyNames.TryGetValue (property.Name, out var javaName)
70+
? javaName
71+
: property.Name;
72+
properties.Add (new KeyValuePair<string, string> (
73+
propertyName,
74+
ManagedValueToJavaSource (property.Type, property.Value)
75+
));
76+
}
77+
return properties;
78+
}
79+
80+
static IReadOnlyDictionary<string, string> GetJavaPropertyNames (TypeDefinition attributeType, AssemblyIndex index)
81+
{
82+
var names = new Dictionary<string, string> (StringComparer.Ordinal);
83+
foreach (var propertyHandle in attributeType.GetProperties ()) {
84+
var property = index.Reader.GetPropertyDefinition (propertyHandle);
85+
var managedName = index.Reader.GetString (property.Name);
86+
foreach (var attributeHandle in property.GetCustomAttributes ()) {
87+
var attribute = index.Reader.GetCustomAttribute (attributeHandle);
88+
if (!AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Android.Runtime", "RegisterAttribute")) {
89+
continue;
90+
}
91+
var value = index.DecodeAttribute (attribute);
92+
if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string javaName) {
93+
names [managedName] = javaName;
94+
}
95+
break;
96+
}
97+
}
98+
return names;
99+
}
100+
101+
AnnotationTypeInfo? GetAnnotationType (CustomAttribute attribute, AssemblyIndex index)
102+
{
103+
EntityHandle typeHandle = default;
104+
if (attribute.Constructor.Kind == HandleKind.MethodDefinition) {
105+
typeHandle = index.Reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType ();
106+
} else if (attribute.Constructor.Kind == HandleKind.MemberReference) {
107+
typeHandle = index.Reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor).Parent;
108+
}
109+
110+
var key = (index, typeHandle);
111+
if (typeHandle.IsNil || annotationTypes.TryGetValue (key, out var cached) && cached is null) {
112+
return null;
113+
}
114+
if (cached is not null) {
115+
return cached;
116+
}
117+
118+
TypeDefinition attributeType;
119+
AssemblyIndex attributeIndex;
120+
if (typeHandle.Kind == HandleKind.TypeDefinition) {
121+
attributeType = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)typeHandle);
122+
attributeIndex = index;
123+
} else if (typeHandle.Kind == HandleKind.TypeReference) {
124+
var typeReference = MetadataTypeNameResolver.GetTypeRefFromReference (
125+
index.Reader,
126+
(TypeReferenceHandle)typeHandle,
127+
index.AssemblyName,
128+
rawTypeKind: 0
129+
);
130+
if (!assemblies.TryGetValue (typeReference.AssemblyName, out attributeIndex) ||
131+
!attributeIndex.TypesByFullName.TryGetValue (typeReference.ManagedTypeName, out var resolvedHandle)) {
132+
annotationTypes [key] = null;
133+
return null;
134+
}
135+
attributeType = attributeIndex.Reader.GetTypeDefinition (resolvedHandle);
136+
} else {
137+
annotationTypes [key] = null;
138+
return null;
139+
}
140+
141+
var javaName = GetJavaName (attributeType, attributeIndex);
142+
var result = javaName.IsNullOrEmpty ()
143+
? null
144+
: new AnnotationTypeInfo (javaName, GetJavaPropertyNames (attributeType, attributeIndex));
145+
annotationTypes [key] = result;
146+
return result;
147+
}
148+
149+
string ManagedValueToJavaSource (string managedType, object? value)
150+
{
151+
if (value is null) {
152+
return "null";
153+
}
154+
if (managedType == "String" || managedType == "System.String") {
155+
return ToJavaStringLiteral (value.ToString () ?? "");
156+
}
157+
if (managedType == "System.Type" && value is string typeName) {
158+
var javaName = resolveTypeName (typeName);
159+
if (javaName is not null) {
160+
return JniSignatureHelper.JniNameToJavaName (javaName) + ".class";
161+
}
162+
throw new InvalidOperationException ($"Java annotation type value '{typeName}' does not resolve to a Java peer.");
163+
}
164+
if (value is bool boolean) {
165+
return boolean ? "true" : "false";
166+
}
167+
if (value is IFormattable formattable) {
168+
return formattable.ToString (null, CultureInfo.InvariantCulture) ?? "";
169+
}
170+
return value.ToString () ?? "";
171+
}
172+
173+
static string ToJavaStringLiteral (string value)
174+
{
175+
var builder = new StringBuilder (value.Length + 2);
176+
builder.Append ('"');
177+
foreach (char c in value) {
178+
switch (c) {
179+
case '"':
180+
builder.Append ("\\\"");
181+
break;
182+
case '\\':
183+
builder.Append ("\\\\");
184+
break;
185+
case '\b':
186+
builder.Append ("\\b");
187+
break;
188+
case '\t':
189+
builder.Append ("\\t");
190+
break;
191+
case '\n':
192+
builder.Append ("\\n");
193+
break;
194+
case '\f':
195+
builder.Append ("\\f");
196+
break;
197+
case '\r':
198+
builder.Append ("\\r");
199+
break;
200+
default:
201+
if (char.IsControl (c)) {
202+
builder.Append ("\\u");
203+
HexUtilities.WriteHex (builder, c, upperCase: false);
204+
} else {
205+
builder.Append (c);
206+
}
207+
break;
208+
}
209+
}
210+
builder.Append ('"');
211+
return builder.ToString ();
212+
}
213+
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public sealed record JavaPeerInfo
6363
/// </summary>
6464
public IReadOnlyList<string> ImplementedInterfaceJavaNames { get; init; } = Array.Empty<string> ();
6565

66+
/// <summary>
67+
/// Java annotations forwarded from managed custom attributes decorated with
68+
/// <c>Android.Runtime.AnnotationAttribute</c>.
69+
/// </summary>
70+
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
71+
6672
public bool IsInterface { get; init; }
6773
public bool IsAbstract { get; init; }
6874

@@ -296,6 +302,20 @@ public sealed record MarshalMethodInfo
296302
/// <c>new virtual</c> while reusing the same JNI name and signature.
297303
/// </summary>
298304
public bool CallManagedMethodDirectly { get; init; }
305+
306+
/// <summary>
307+
/// Java annotations forwarded from the managed method or constructor.
308+
/// </summary>
309+
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
310+
}
311+
312+
/// <summary>
313+
/// Describes a Java annotation forwarded from a managed custom attribute.
314+
/// </summary>
315+
public sealed record JavaAnnotationInfo
316+
{
317+
public required string Name { get; init; }
318+
public IReadOnlyList<KeyValuePair<string, string>> Properties { get; init; } = [];
299319
}
300320

301321
/// <summary>
@@ -344,6 +364,11 @@ public sealed record JavaConstructorInfo
344364
/// True when this Java constructor has a matching public managed constructor on the target type.
345365
/// </summary>
346366
public bool HasMatchingManagedCtor { get; init; }
367+
368+
/// <summary>
369+
/// Java annotations forwarded from the managed constructor.
370+
/// </summary>
371+
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
347372
}
348373

349374
/// <summary>
@@ -376,6 +401,11 @@ public sealed record JavaFieldInfo
376401
/// Whether the field is static.
377402
/// </summary>
378403
public bool IsStatic { get; init; }
404+
405+
/// <summary>
406+
/// Java annotations forwarded from the managed field initializer method.
407+
/// </summary>
408+
public IReadOnlyList<JavaAnnotationInfo> Annotations { get; init; } = [];
379409
}
380410

381411
/// <summary>

0 commit comments

Comments
 (0)