Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeywo
AssertArrayTypesFromSignature<double[]> (manager, "[[D", typeof (JavaObjectArray<JavaPrimitiveArray<double>>));
AssertArrayTypesFromSignature<string[]> (manager, "[[Ljava/lang/String;", typeof (JavaObjectArray<JavaObjectArray<string>>));

// Nullable counterparts of the primitive value types: the boxed reference signatures
// java/lang/<Boxed> map to Nullable<T> (see JniBuiltinSimpleReferenceToType /
// GetBuiltInTypeForSimpleReference). Unlike the primitive keyword arrays above, these are
// non-keyword reference arrays, so the element type is the boxed Nullable<T> and the array
// family is T?[] and JavaObjectArray<T?> (no JavaArray<T>/JavaPrimitiveArray<T>).
AssertArrayTypesFromSignature<bool?> (manager, "[Ljava/lang/Boolean;", typeof (JavaObjectArray<bool?>));
AssertArrayTypesFromSignature<sbyte?> (manager, "[Ljava/lang/Byte;", typeof (JavaObjectArray<sbyte?>));
AssertArrayTypesFromSignature<char?> (manager, "[Ljava/lang/Character;", typeof (JavaObjectArray<char?>));
AssertArrayTypesFromSignature<short?> (manager, "[Ljava/lang/Short;", typeof (JavaObjectArray<short?>));
AssertArrayTypesFromSignature<int?> (manager, "[Ljava/lang/Integer;", typeof (JavaObjectArray<int?>));
AssertArrayTypesFromSignature<long?> (manager, "[Ljava/lang/Long;", typeof (JavaObjectArray<long?>));
AssertArrayTypesFromSignature<float?> (manager, "[Ljava/lang/Float;", typeof (JavaObjectArray<float?>));
AssertArrayTypesFromSignature<double?> (manager, "[Ljava/lang/Double;", typeof (JavaObjectArray<double?>));

// Yes, these look weird...
// Assume: class II {}
Assert.AreEqual (null, GetType ("II"));
Expand Down
134 changes: 126 additions & 8 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ static class ModelBuilder
new ("D", "Double", "System.Double", ["Java.Interop.JavaDoubleArray"]),
];

// Nullable value types map to the boxed java/lang/<Boxed> references (java/lang/Boolean etc.) via
// TrimmableTypeMapTypeManager.GetBuiltInTypeForSimpleReference. Like System.String they are neither
// scanned Java peers nor primitives, so their array proxies are emitted explicitly. Only the eight
// types with a boxed java/lang mapping are included (java/lang/Byte -> sbyte?, not byte?, etc.).
static readonly (string Name, string ManagedTypeName) [] NullableArrayProxies = [
("Boolean", "System.Boolean"),
("SByte", "System.SByte"),
("Char", "System.Char"),
("Int16", "System.Int16"),
("Int32", "System.Int32"),
("Int64", "System.Int64"),
("Single", "System.Single"),
("Double", "System.Double"),
];

static readonly HashSet<string> EssentialRuntimeTypes = new (StringComparer.Ordinal) {
"java/lang/Object",
"java/lang/Class",
Expand All @@ -55,7 +70,7 @@ static class ModelBuilder
/// Emit per-rank array <c>TypeMap</c> entries + <c>__ArrayMapRank{N}</c> sentinels
/// for ranks 1..<paramref name="maxArrayRank"/>. 0 disables array entry emission.
/// </param>
public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0)
public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0, int maxReferenceArrayRank = 0)
{
if (peers is null) {
throw new ArgumentNullException (nameof (peers));
Expand All @@ -66,13 +81,20 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
if (maxArrayRank < 0) {
throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0.");
}
if (maxReferenceArrayRank < 0) {
throw new ArgumentOutOfRangeException (nameof (maxReferenceArrayRank), maxReferenceArrayRank, "Must be >= 0.");
}

assemblyName ??= Path.GetFileNameWithoutExtension (outputPath);

var model = new TypeMapAssemblyData {
AssemblyName = assemblyName,
ModuleName = Path.GetFileName (outputPath),
MaxArrayRank = maxArrayRank,
// The per-assembly __ArrayMapRank{N} anchor count must be uniform across every typemap
// assembly (the root generator builds a rectangular [assembly][rank] matrix), so the model
// always carries the overall maximum rank. Primitive and reference element types may
// populate different rank ranges within that shared anchor set.
MaxArrayRank = Math.Max (maxArrayRank, maxReferenceArrayRank),
};

// Invoker types are NOT emitted as separate proxies or TypeMap entries.
Expand Down Expand Up @@ -109,12 +131,13 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri

EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);

if (maxArrayRank > 0) {
EmitArrayEntries (model, jniName, peersForName, maxArrayRank);
// Java peer types are reference types: emit array proxies only up to the reference rank.
if (maxReferenceArrayRank > 0) {
EmitArrayEntries (model, jniName, peersForName, maxReferenceArrayRank);
}
}

if (maxArrayRank > 0 && string.Equals (assemblyName, "_Java.Interop.TypeMap", StringComparison.Ordinal)) {
if (string.Equals (assemblyName, "_Java.Interop.TypeMap", StringComparison.Ordinal)) {
EmitPrimitiveArrayEntries (model, maxArrayRank);
}
Comment thread
Copilot marked this conversation as resolved.

Expand Down Expand Up @@ -541,6 +564,26 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr
static string AssemblyQualify (string typeName, string assemblyName)
=> $"{typeName}, {assemblyName}";

static string AssemblyQualify (TypeRefData type)
{
if (type.GenericArguments.Count == 0) {
return AssemblyQualify (type.ManagedTypeName, type.AssemblyName);
}

var builder = new StringBuilder ();
builder.Append (type.ManagedTypeName);
builder.Append ("[[");
for (int i = 0; i < type.GenericArguments.Count; i++) {
if (i > 0) {
builder.Append ("],[");
}
builder.Append (AssemblyQualify (type.GenericArguments [i]));
}
builder.Append ("]], ");
builder.Append (type.AssemblyName);
return builder.ToString ();
}

static string AddArrayRank (string typeReference, int rank)
{
if (rank == 0) {
Expand Down Expand Up @@ -569,7 +612,7 @@ static string MakeNestedJavaObjectArrayTypeReference (string elementTypeReferenc

static IReadOnlyList<string> GetArrayTypeReferences (ArrayProxyData proxy)
{
var elementType = AssemblyQualify (proxy.ElementType.ManagedTypeName, proxy.ElementType.AssemblyName);
var elementType = AssemblyQualify (proxy.ElementType);
if (proxy.Primitive is null) {
var rankOneTypes = new [] {
MakeGenericTypeReference ("Java.Interop.JavaObjectArray`1", "Java.Interop", elementType),
Expand All @@ -585,7 +628,7 @@ static IReadOnlyList<string> GetArrayTypeReferences (ArrayProxyData proxy)
MakeGenericTypeReference ("Java.Interop.JavaPrimitiveArray`1", "Java.Interop", elementType),
];
foreach (var concreteArrayType in proxy.Primitive.ConcreteArrayTypes) {
rankOnePrimitiveTypes.Add (AssemblyQualify (concreteArrayType.ManagedTypeName, concreteArrayType.AssemblyName));
rankOnePrimitiveTypes.Add (AssemblyQualify (concreteArrayType));
}
return ExpandRankOneTypes (rankOnePrimitiveTypes, proxy.Rank);
}
Expand Down Expand Up @@ -616,7 +659,7 @@ static void AddArrayProxyAssociations (TypeMapAssemblyData model, ArrayProxyData
}

static string GetArrayProxyMapKey (TypeRefData elementType)
=> AssemblyQualify (elementType.ManagedTypeName, elementType.AssemblyName);
=> AssemblyQualify (elementType);

/// <summary>
/// Emits per-rank array TypeMap entries for one peer, anchored to the per-assembly
Expand Down Expand Up @@ -665,6 +708,13 @@ static void EmitArrayEntriesForPeer (TypeMapAssemblyData model, JavaPeerInfo pee
}
}

// Emits array proxies for the built-in element types that are not scanned Java peers:
// * the keyword primitives (int/bool/...) up to maxArrayRank (jagged/multidim primitive arrays
// like int[][][] are cheap — a small fixed set of element types),
// * System.String up to maxArrayRank (also a built-in element type, so multidimensional string
// arrays like String[][] stay resolvable),
// * the boxed Nullable<T> value types up to maxArrayRank (also built-in element types mapping to
// java/lang/<Boxed>; a small fixed set, so multidim boxed arrays stay resolvable too).
static void EmitPrimitiveArrayEntries (TypeMapAssemblyData model, int maxArrayRank)
{
foreach (var primitive in PrimitiveArrayProxies) {
Expand Down Expand Up @@ -694,6 +744,74 @@ static void EmitPrimitiveArrayEntries (TypeMapAssemblyData model, int maxArrayRa
AddArrayProxyAssociations (model, proxy, proxyReference);
}
}

// System.String maps to java/lang/String but is a built-in element type injected at runtime by
// TrimmableTypeMapTypeManager.GetBuiltInTypeForSimpleReference, so it is neither a scanned Java
// peer (EmitArrayEntriesForPeer) nor a keyword primitive. Emit its array proxies here (Primitive
// is null, so it gets the reference-array family) so GetTypes ("[Ljava/lang/String;") yields
// System.String[] / JavaObjectArray<string> / JavaArray<string> on NativeAOT, matching CoreCLR.
// String is a single, fixed built-in element type (not part of the scanned-peer explosion), so —
// like the keyword primitives — it goes up to maxArrayRank. This keeps multidimensional string
// arrays (String[][], "[[Ljava/lang/String;") resolvable, matching the other type-map backends.
for (int rank = 1; rank <= maxArrayRank; rank++) {
var proxy = new ArrayProxyData {
TypeName = ManagedTypeNameToArrayProxyTypeName ("System.String", rank),
ElementType = new TypeRefData {
ManagedTypeName = "System.String",
AssemblyName = "System.Runtime",
},
Rank = rank,
};
model.ArrayProxyTypes.Add (proxy);
var proxyReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", model.AssemblyName);
model.Entries.Add (new TypeMapAttributeData {
MapKey = GetArrayProxyMapKey (proxy.ElementType),
ProxyTypeReference = proxyReference,
TargetTypeReference = proxyReference,
AnchorRank = rank,
});
AddArrayProxyAssociations (model, proxy, proxyReference);
}

// Nullable counterparts of the primitive value types map to the boxed java/lang/<Boxed>
// references and, like System.String, are built-in element mappings (no scanned peer, no
// primitive proxy). Emit reference-array proxies (Primitive is null) so GetTypes
// ("[Ljava/lang/Boolean;") yields bool?[] / JavaObjectArray<bool?> on NativeAOT. The element
// key uses the normalized generic form (simple assembly names) that
// TrimmableTypeMap.BuildManagedTypeKey produces at runtime for Nullable<T>, while the model
// keeps the generic instantiation structured so the emitter writes a real Nullable<T> type
// signature. Like the primitives and System.String, these are a small fixed set of built-in
// element types (not the scanned-peer explosion), so they go up to maxArrayRank and keep
// multidim boxed arrays resolvable.
foreach (var nullablePrimitive in NullableArrayProxies) {
for (int rank = 1; rank <= maxArrayRank; rank++) {
var proxy = new ArrayProxyData {
TypeName = $"Nullable_{nullablePrimitive.Name}_ArrayProxy{rank}",
ElementType = new TypeRefData {
ManagedTypeName = "System.Nullable`1",
AssemblyName = "System.Runtime",
GenericArguments = [
new TypeRefData {
ManagedTypeName = nullablePrimitive.ManagedTypeName,
AssemblyName = "System.Runtime",
IsValueType = true,
},
],
IsValueType = true,
},
Rank = rank,
};
model.ArrayProxyTypes.Add (proxy);
var proxyReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", model.AssemblyName);
model.Entries.Add (new TypeMapAttributeData {
MapKey = GetArrayProxyMapKey (proxy.ElementType),
ProxyTypeReference = proxyReference,
TargetTypeReference = proxyReference,
AnchorRank = rank,
});
AddArrayProxyAssociations (model, proxy, proxyReference);
}
}
}

static string Brackets (int rank) => rank switch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion)
/// <param name="useSharedTypemapUniverse">
/// When true, uses <c>Java.Lang.Object</c> as the shared anchor type. When false, emits a per-assembly anchor.
/// </param>
/// <param name="maxArrayRank">Max rank for per-rank array <c>TypeMap</c> entries. 0 disables.</param>
public void Generate (IReadOnlyList<JavaPeerInfo> peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0)
/// <param name="maxArrayRank">Max rank for per-rank built-in element array <c>TypeMap</c> entries. 0 disables.</param>
/// <param name="maxReferenceArrayRank">Max rank for per-rank scanned Java peer reference-type array <c>TypeMap</c> entries. 0 disables.</param>
public void Generate (IReadOnlyList<JavaPeerInfo> peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0, int maxReferenceArrayRank = 0)
{
var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank);
var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, maxReferenceArrayRank);
var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
emitter.Emit (model, stream, useSharedTypemapUniverse);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public TrimmableTypeMapResult Execute (
XDocument? manifestTemplate = null,
string? packageNamingPolicy = null,
int maxArrayRank = 0,
int maxReferenceArrayRank = 0,
bool generateTypeMapAssemblies = true)
{
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
Expand All @@ -43,6 +44,9 @@ public TrimmableTypeMapResult Execute (
if (maxArrayRank < 0) {
throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0.");
}
if (maxReferenceArrayRank < 0) {
throw new ArgumentOutOfRangeException (nameof (maxReferenceArrayRank), maxReferenceArrayRank, "Must be >= 0.");
}

var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames);
if (allPeers.Count == 0) {
Expand All @@ -56,7 +60,7 @@ public TrimmableTypeMapResult Execute (
PropagateCannotRegisterToDescendants (allPeers);

var generatedAssemblies = generateTypeMapAssemblies
? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank)
? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, maxReferenceArrayRank)
: [];
var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList ();
logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count);
Expand Down Expand Up @@ -178,7 +182,8 @@ List<GeneratedAssembly> GenerateTypeMapAssemblies (
List<JavaPeerInfo> allPeers,
Version systemRuntimeVersion,
bool useSharedTypemapUniverse,
int maxArrayRank)
int maxArrayRank,
int maxReferenceArrayRank)
{
List<(string AssemblyName, List<JavaPeerInfo> Peers)> peersByAssembly;

Expand Down Expand Up @@ -207,14 +212,16 @@ List<GeneratedAssembly> GenerateTypeMapAssemblies (
string typeMapAssemblyName = $"_{assemblyName}.TypeMap";
perAssemblyNames.Add (typeMapAssemblyName);
var stream = new MemoryStream ();
generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank);
generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank, maxReferenceArrayRank);
stream.Position = 0;
generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream));
logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count);
}
var rootStream = new MemoryStream ();
var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank);
// The root generator builds the [assembly][rank] anchor matrix, so it needs the overall maximum
// rank across primitive and reference element types.
rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: Math.Max (maxArrayRank, maxReferenceArrayRank));
rootStream.Position = 0;
generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream));
logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count);
Expand Down
2 changes: 1 addition & 1 deletion src/Mono.Android/Java.Interop/JavaPeerProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public sealed class JavaPeerAliasesAttribute : Attribute
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
public abstract class JavaPeerProxy : Attribute
{
private protected JavaPeerProxy (string jniName, Type targetType)
protected JavaPeerProxy (string jniName, Type targetType)
{
ArgumentNullException.ThrowIfNull (jniName);
ArgumentNullException.ThrowIfNull (targetType);
Expand Down
54 changes: 44 additions & 10 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Android.Runtime;
using Java.Interop;
Expand Down Expand Up @@ -543,25 +544,58 @@ internal bool TryGetArrayProxy (Type elementType, int additionalRank, [NotNullWh

static bool TryGetManagedTypeKey (Type type, [NotNullWhen (true)] out string? key)
{
var fullName = type.FullName;
if (fullName is null) {
key = null;
return false;
}
key = BuildManagedTypeKey (type);
return key is not null;
}

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

key = $"{fullName}, {assemblyName}";
return true;
if (type.IsGenericType && !type.IsGenericTypeDefinition) {
var definitionName = type.GetGenericTypeDefinition ().FullName;
if (definitionName is null) {
return null;
}
var arguments = type.GetGenericArguments ();
var builder = new StringBuilder (definitionName);
builder.Append ("[[");
for (int i = 0; i < arguments.Length; i++) {
if (i > 0) {
builder.Append ("],[");
}
var argumentKey = BuildManagedTypeKey (arguments [i]);
if (argumentKey is null) {
return null;
}
builder.Append (argumentKey);
}
builder.Append ("]], ");
builder.Append (assemblyName);
return builder.ToString ();
}

var fullName = type.FullName;
if (fullName is null) {
return null;
}
return $"{fullName}, {assemblyName}";
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
TargetFrameworkVersion="$(TargetFrameworkVersion)"
PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)"
MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)"
MaxReferenceArrayRank="$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)"
GenerateTypeMapAssemblies="false"
CleanJavaSourceOutputDirectory="true"
AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"
Expand Down
Loading
Loading