Skip to content

Commit 3d0cd67

Browse files
[TrimmableTypeMap] Emit array proxies for boxed nullable value types
GetTypes ("[Ljava/lang/Boolean;") and the other boxed value-type array signatures dropped the Nullable<T> array family (bool?[], JavaObjectArray<bool?>) on NativeAOT. Like System.String, Nullable<T> is a built-in reference mapping injected at runtime by GetBuiltInTypeForSimpleReference, so it is neither a scanned Java peer nor a primitive and had no pre-generated array proxy. Unlike String, the proxy map key is the tricky part: at runtime TryGetManagedTypeKey (typeof (bool?)) keyed on Type.FullName, whose type argument carries the full versioned assembly-qualified name (System.Nullable`1[[System.Boolean, System.Private.CoreLib, Version=...]]), which the generator can't stably reproduce. Normalize the generic key on both sides: * Runtime (BuildManagedTypeKey): build closed-generic keys from the open definition's FullName plus each type argument's normalized key (simple assembly name, no Version/Culture/PublicKeyToken), and treat Nullable<T> as a System.Runtime type. typeof (bool?) -> "System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime". * Generator (ModelBuilder): emit reference-array proxies for the eight Nullable<primitive> types that have a boxed java/lang mapping, keyed with the same normalized string. Normalizing (rather than emitting version-qualified duplicates) keeps a single canonical proxy per logical type. Fixes the boxed-nullable portion of JniTypeManagerTests.GetType on trimmable NativeAOT (added in the preceding test-coverage commit). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 243f5c4 commit 3d0cd67

3 files changed

Lines changed: 103 additions & 12 deletions

File tree

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ static class ModelBuilder
3131
new ("D", "Double", "System.Double", ["Java.Interop.JavaDoubleArray"]),
3232
];
3333

34+
// Nullable value types map to the boxed java/lang/<Boxed> references (java/lang/Boolean etc.) via
35+
// TrimmableTypeMapTypeManager.GetBuiltInTypeForSimpleReference. Like System.String they are neither
36+
// scanned Java peers nor primitives, so their array proxies are emitted explicitly. Only the eight
37+
// types with a boxed java/lang mapping are included (java/lang/Byte -> sbyte?, not byte?, etc.).
38+
static readonly (string Name, string ManagedTypeName) [] NullableArrayProxies = [
39+
("Boolean", "System.Boolean"),
40+
("SByte", "System.SByte"),
41+
("Char", "System.Char"),
42+
("Int16", "System.Int16"),
43+
("Int32", "System.Int32"),
44+
("Int64", "System.Int64"),
45+
("Single", "System.Single"),
46+
("Double", "System.Double"),
47+
];
48+
3449
static readonly HashSet<string> EssentialRuntimeTypes = new (StringComparer.Ordinal) {
3550
"java/lang/Object",
3651
"java/lang/Class",
@@ -732,6 +747,35 @@ static void EmitPrimitiveArrayEntries (TypeMapAssemblyData model, int maxArrayRa
732747
});
733748
AddArrayProxyAssociations (model, proxy, proxyReference);
734749
}
750+
751+
// Nullable counterparts of the primitive value types map to the boxed java/lang/<Boxed>
752+
// references and, like System.String, are built-in reference mappings (no scanned peer, no
753+
// primitive proxy). Emit reference-array proxies (Primitive is null) so GetTypes
754+
// ("[Ljava/lang/Boolean;") yields bool?[] / JavaObjectArray<bool?> on NativeAOT. The element
755+
// key uses the normalized generic form (simple assembly names) that
756+
// TrimmableTypeMap.BuildManagedTypeKey produces at runtime for Nullable<T>.
757+
foreach (var nullablePrimitive in NullableArrayProxies) {
758+
var elementTypeName = $"System.Nullable`1[[{nullablePrimitive.ManagedTypeName}, System.Runtime]]";
759+
for (int rank = 1; rank <= maxArrayRank; rank++) {
760+
var proxy = new ArrayProxyData {
761+
TypeName = $"Nullable_{nullablePrimitive.Name}_ArrayProxy{rank}",
762+
ElementType = new TypeRefData {
763+
ManagedTypeName = elementTypeName,
764+
AssemblyName = "System.Runtime",
765+
},
766+
Rank = rank,
767+
};
768+
model.ArrayProxyTypes.Add (proxy);
769+
var proxyReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", model.AssemblyName);
770+
model.Entries.Add (new TypeMapAttributeData {
771+
MapKey = GetArrayProxyMapKey (proxy.ElementType),
772+
ProxyTypeReference = proxyReference,
773+
TargetTypeReference = proxyReference,
774+
AnchorRank = rank,
775+
});
776+
AddArrayProxyAssociations (model, proxy, proxyReference);
777+
}
778+
}
735779
}
736780

737781
static string Brackets (int rank) => rank switch {

src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Reflection;
88
using System.Runtime.InteropServices;
9+
using System.Text;
910
using System.Threading;
1011
using Android.Runtime;
1112
using Java.Interop;
@@ -543,25 +544,58 @@ internal bool TryGetArrayProxy (Type elementType, int additionalRank, [NotNullWh
543544

544545
static bool TryGetManagedTypeKey (Type type, [NotNullWhen (true)] out string? key)
545546
{
546-
var fullName = type.FullName;
547-
if (fullName is null) {
548-
key = null;
549-
return false;
550-
}
547+
key = BuildManagedTypeKey (type);
548+
return key is not null;
549+
}
551550

551+
// Builds the array-proxy map key for a managed type. Closed generic types use a normalized,
552+
// version-independent form so the key matches the one emitted by the trimmable typemap generator,
553+
// which references types by simple assembly name (no Version/Culture/PublicKeyToken). Without
554+
// normalization a closed generic like Nullable<bool> would key on Type.FullName, whose type
555+
// arguments carry the full versioned assembly-qualified name and would never match.
556+
static string? BuildManagedTypeKey (Type type)
557+
{
552558
var assemblyName = GetAssemblyNameForManagedTypeKey (type);
553559
if (assemblyName is null) {
554-
key = null;
555-
return false;
560+
return null;
556561
}
557562

558-
key = $"{fullName}, {assemblyName}";
559-
return true;
563+
if (type.IsGenericType && !type.IsGenericTypeDefinition) {
564+
var definitionName = type.GetGenericTypeDefinition ().FullName;
565+
if (definitionName is null) {
566+
return null;
567+
}
568+
var arguments = type.GetGenericArguments ();
569+
var builder = new StringBuilder (definitionName);
570+
builder.Append ("[[");
571+
for (int i = 0; i < arguments.Length; i++) {
572+
if (i > 0) {
573+
builder.Append ("],[");
574+
}
575+
var argumentKey = BuildManagedTypeKey (arguments [i]);
576+
if (argumentKey is null) {
577+
return null;
578+
}
579+
builder.Append (argumentKey);
580+
}
581+
builder.Append ("]], ");
582+
builder.Append (assemblyName);
583+
return builder.ToString ();
584+
}
585+
586+
var fullName = type.FullName;
587+
if (fullName is null) {
588+
return null;
589+
}
590+
return $"{fullName}, {assemblyName}";
560591
}
561592

562593
static string? GetAssemblyNameForManagedTypeKey (Type type)
563594
{
564-
if (type.IsPrimitive || type == typeof (string)) {
595+
// Primitives, string, and Nullable<T> are surfaced through the System.Runtime reference
596+
// assembly; the trimmable typemap generator emits their keys with "System.Runtime", so
597+
// normalize to it here (the runtime implementation assembly is System.Private.CoreLib).
598+
if (type.IsPrimitive || type == typeof (string) || Nullable.GetUnderlyingType (type) is not null) {
565599
return "System.Runtime";
566600
}
567601

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,8 +1203,8 @@ public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAss
12031203
var primitiveEntries = model.Entries
12041204
.Where (e => e.MapKey.StartsWith ("System.", StringComparison.Ordinal) && e.AnchorRank is not null)
12051205
.ToList ();
1206-
// 12 primitives × 3 ranks + System.String × 3 ranks.
1207-
Assert.Equal (39, primitiveEntries.Count);
1206+
// 12 primitives × 3 ranks + System.String × 3 ranks + 8 Nullable<primitive> × 3 ranks.
1207+
Assert.Equal (63, primitiveEntries.Count);
12081208

12091209
var sbyteRank1 = primitiveEntries.Single (e => e.MapKey == "System.SByte, System.Runtime" && e.AnchorRank == 1);
12101210
Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy1, _Java.Interop.TypeMap", sbyteRank1.ProxyTypeReference);
@@ -1259,6 +1259,19 @@ public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAss
12591259
Assert.DoesNotContain (model.Associations, a =>
12601260
a.SourceTypeReference == "Java.Interop.JavaPrimitiveArray`1[[System.String, System.Runtime]], Java.Interop" &&
12611261
a.AliasProxyTypeReference == stringRank1.ProxyTypeReference);
1262+
1263+
// Nullable<T> boxed mappings (java/lang/Boolean etc.) use the normalized generic key
1264+
// (simple assembly names) that TrimmableTypeMap.BuildManagedTypeKey produces at runtime, and
1265+
// get the reference-array family.
1266+
var nullableBoolRank1 = primitiveEntries.Single (e =>
1267+
e.MapKey == "System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime" && e.AnchorRank == 1);
1268+
Assert.Equal ("_TypeMap.ArrayProxies.Nullable_Boolean_ArrayProxy1, _Java.Interop.TypeMap", nullableBoolRank1.ProxyTypeReference);
1269+
Assert.Contains (model.Associations, a =>
1270+
a.SourceTypeReference == "System.Nullable`1[[System.Boolean, System.Runtime]][], System.Runtime" &&
1271+
a.AliasProxyTypeReference == nullableBoolRank1.ProxyTypeReference);
1272+
Assert.Contains (model.Associations, a =>
1273+
a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime]], Java.Interop" &&
1274+
a.AliasProxyTypeReference == nullableBoolRank1.ProxyTypeReference);
12621275
}
12631276

12641277
[Fact]

0 commit comments

Comments
 (0)