Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ sealed class TypeMapAssemblyData
/// </summary>
public List<TypeMapAssociationData> Associations { get; } = new ();

/// <summary>
/// Alias holder types to emit — one per alias group (≥2 types sharing a JNI name).
/// </summary>
public List<AliasHolderData> AliasHolders { get; } = new ();

/// <summary>
/// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls.
/// </summary>
Expand Down Expand Up @@ -252,7 +257,7 @@ sealed record ActivationCtorData

/// <summary>
/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry.
/// Links a managed type to the proxy that holds its alias TypeMap entry.
/// Links a managed type to the alias holder that owns the alias group.
/// </summary>
sealed record TypeMapAssociationData
{
Expand All @@ -262,7 +267,30 @@ sealed record TypeMapAssociationData
public required string SourceTypeReference { get; init; }

/// <summary>
/// Assembly-qualified proxy type reference (the alias holder proxy).
/// Assembly-qualified proxy type reference (the alias holder).
/// </summary>
public required string AliasProxyTypeReference { get; init; }
}

/// <summary>
/// An alias holder class to generate in the TypeMap assembly.
/// Extends JavaPeerProxy and implements IJavaPeerAliases.
/// Emitted when multiple .NET types map to the same JNI name.
/// </summary>
sealed class AliasHolderData
{
/// <summary>
/// Simple type name, e.g., "Test_AliasTarget_Aliases".
/// </summary>
public required string TypeName { get; init; }

/// <summary>
/// Namespace for alias holder types.
/// </summary>
public string Namespace { get; init; } = "_TypeMap.Aliases";

/// <summary>
/// Indexed TypeMap keys, e.g., ["test/AliasTarget[0]", "test/AliasTarget[1]"].
/// </summary>
public required List<string> AliasKeys { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,11 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
static void EmitPeers (TypeMapAssemblyData model, string jniName,
List<JavaPeerInfo> peersForName, string assemblyName, HashSet<string> usedProxyNames)
{
// First peer is the "primary" — it gets the base JNI name entry.
// Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ...
JavaPeerProxyData? primaryProxy = null;
for (int i = 0; i < peersForName.Count; i++) {
var peer = peersForName [i];
string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]";
bool isAliasGroup = peersForName.Count > 1;

if (!isAliasGroup) {
// Single peer — no aliases needed, emit directly with the base JNI name
var peer = peersForName [0];
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;

Expand All @@ -124,25 +122,58 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
model.ProxyTypes.Add (proxy);
}

if (i == 0) {
primaryProxy = proxy;
}

model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));

// Emit TypeMapAssociation for all proxy-backed types so managed → proxy
// lookup works even when the final JNI name differs from the type's attributes.
// Generic definitions are included — their proxy types derive from the
// non-generic `JavaPeerProxy` base so the CLR can load them without
// resolving an open generic argument.
var assocProxy = (i > 0 && primaryProxy != null) ? primaryProxy : proxy;
if (assocProxy != null) {
model.Entries.Add (BuildEntry (peer, proxy, assemblyName, jniName));
if (proxy != null && peer.IsGenericDefinition) {
model.Associations.Add (new TypeMapAssociationData {
SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName),
AliasProxyTypeReference = AssemblyQualify ($"{assocProxy.Namespace}.{assocProxy.TypeName}", assemblyName),
AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName),
});
}
return;
}

// Alias group: generate an alias holder and indexed entries for each peer.
// The base JNI name maps to the alias holder; each peer gets "[0]", "[1]", etc.
var aliasKeys = new List<string> ();
string holderTypeName = jniName.Replace ('/', '_').Replace ('$', '_') + "_Aliases";
var holderNamespace = "_TypeMap.Aliases";
string holderRef = AssemblyQualify ($"{holderNamespace}.{holderTypeName}", assemblyName);

for (int i = 0; i < peersForName.Count; i++) {
var peer = peersForName [i];
string entryJniName = $"{jniName}[{i}]";
aliasKeys.Add (entryJniName);

bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;

JavaPeerProxyData? proxy = null;
if (hasProxy) {
proxy = BuildProxyType (peer, jniName, usedProxyNames, isAcw);
model.ProxyTypes.Add (proxy);
}

model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));

// Link each alias type to the alias holder for trimming
model.Associations.Add (new TypeMapAssociationData {
SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName),
AliasProxyTypeReference = holderRef,
});
}

// Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations)
model.Entries.Add (new TypeMapAttributeData {
JniName = jniName,
ProxyTypeReference = holderRef,
TargetTypeReference = holderRef,
});

model.AliasHolders.Add (new AliasHolderData {
TypeName = holderTypeName,
Namespace = holderNamespace,
AliasKeys = aliasKeys,
});
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ sealed class TypeMapAssemblyEmitter
TypeReferenceHandle _jniTypeRef;
TypeReferenceHandle _notSupportedExceptionRef;
TypeReferenceHandle _runtimeHelpersRef;
TypeReferenceHandle _javaPeerAliasesAttrRef;
MemberReferenceHandle _javaPeerAliasesAttrCtorRef;

MemberReferenceHandle _getTypeFromHandleRef;
MemberReferenceHandle _getUninitializedObjectRef;
Expand Down Expand Up @@ -150,6 +152,10 @@ void EmitCore (TypeMapAssemblyData model)
EmitProxyType (proxy, wrapperHandles);
}

foreach (var holder in model.AliasHolders) {
EmitAliasHolderType (holder);
}

foreach (var entry in model.Entries) {
EmitTypeMapAttribute (entry);
}
Expand Down Expand Up @@ -190,6 +196,8 @@ void EmitTypeReferences ()
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException"));
_runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers"));
_javaPeerAliasesAttrRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerAliasesAttribute"));

_jniNativeMethodRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniNativeMethod"));
Expand Down Expand Up @@ -289,6 +297,7 @@ void EmitMemberReferences ()

EmitTypeMapAttributeCtorRef ();
EmitTypeMapAssociationAttributeCtorRef ();
EmitJavaPeerAliasesAttributeCtorRef ();
}

void EmitTypeMapAttributeCtorRef ()
Expand Down Expand Up @@ -398,17 +407,9 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition
metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef);
}

// Self-apply: the proxy type is its own [JavaPeerProxy] attribute.
// This enables type.GetCustomAttribute<JavaPeerProxy>() to instantiate the proxy
// at runtime for AOT-safe type resolution.
var selfAttrCtorRef = _pe.AddMemberRef (typeDefHandle, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
var selfAttrBlob = _pe.BuildAttributeBlob (b => { });
metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorRef, selfAttrBlob);

// .ctor — pass the resolved JNI name, (for generic-definition base) target type, and
// optional invoker type to the base proxy constructor.
_pe.EmitBody (".ctor",
var selfAttrCtorDef = _pe.EmitBody (".ctor",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }),
encoder => {
Expand All @@ -432,6 +433,12 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition
encoder.OpCode (ILOpCode.Ret);
});

// Self-apply: the proxy type is its own [JavaPeerProxy] attribute.
// This enables type.GetCustomAttribute<JavaPeerProxy>() to instantiate the proxy
// at runtime for AOT-safe type resolution.
var selfAttrBlob = _pe.BuildAttributeBlob (b => { });
metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorDef, selfAttrBlob);

// CreateInstance
EmitCreateInstance (proxy);

Expand All @@ -452,6 +459,59 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition
}
}

void EmitAliasHolderType (AliasHolderData holder)
{
var metadata = _pe.Metadata;

// Alias holders are plain classes (NOT JavaPeerProxy subclasses).
// GetCustomAttribute<JavaPeerProxy>() returns null for these — the fast path
// stays clean. Aliases are discovered via [JavaPeerAliases] attribute only when needed.
var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object"));

var typeDefHandle = metadata.AddTypeDefinition (
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
metadata.GetOrAddString (holder.Namespace),
metadata.GetOrAddString (holder.TypeName),
objectRef,
MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1),
MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1));

// Apply [JavaPeerAliases("key[0]", "key[1]", ...)] to the type
EmitJavaPeerAliasesAttribute (typeDefHandle, holder.AliasKeys);
}

void EmitJavaPeerAliasesAttributeCtorRef ()
{
// JavaPeerAliasesAttribute(params string[] aliases) — in Mono.Android, Java.Interop namespace
_javaPeerAliasesAttrCtorRef = _pe.AddMemberRef (_javaPeerAliasesAttrRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
rt => rt.Void (),
p => p.AddParameter ().Type ().SZArray ().String ()));
}

void EmitJavaPeerAliasesAttribute (TypeDefinitionHandle typeDefHandle, List<string> aliasKeys)
{
// Encode the attribute blob: prolog (0x0001), then packed string array, then NumNamed (0x0000).
// The params string[] is encoded as: element count (uint32), then each string as SerializedString.
var blobBuilder = new BlobBuilder ();
blobBuilder.WriteUInt16 (1); // prolog
blobBuilder.WriteInt32 (aliasKeys.Count); // array length
foreach (var key in aliasKeys) {
WriteSerializedString (blobBuilder, key);
}
blobBuilder.WriteUInt16 (0); // NumNamed

_pe.Metadata.AddCustomAttribute (typeDefHandle, _javaPeerAliasesAttrCtorRef, _pe.Metadata.GetOrAddBlob (blobBuilder));
}

static void WriteSerializedString (BlobBuilder builder, string value)
{
var bytes = System.Text.Encoding.UTF8.GetBytes (value);
builder.WriteCompressedInteger (bytes.Length);
builder.WriteBytes (bytes);
}

void EmitCreateInstance (JavaPeerProxyData proxy)
{
if (!proxy.HasActivation) {
Expand Down
24 changes: 24 additions & 0 deletions src/Mono.Android/Java.Interop/JavaPeerProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@

namespace Java.Interop
{
/// <summary>
/// Attribute applied to generated alias holder types. When multiple .NET types
/// map to the same JNI name (e.g., <c>JavaCollection</c> and <c>JavaCollection&lt;T&gt;</c>
/// both map to <c>"java/util/Collection"</c>), the base JNI name entry points to
/// a plain holder class annotated with this attribute, which lists the indexed
/// TypeMap keys for each alias type.
/// </summary>
/// <remarks>
/// The alias holder is NOT a <see cref="JavaPeerProxy"/> subclass — this ensures
/// <c>GetCustomAttribute&lt;JavaPeerProxy&gt;()</c> returns null for alias entries,
/// keeping the fast path (non-alias types) free of alias checks.
/// </remarks>
[AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class JavaPeerAliasesAttribute : Attribute
{
/// <summary>
/// Gets the indexed TypeMap keys for this alias group (e.g., <c>"java/util/Collection[0]"</c>,
/// <c>"java/util/Collection[1]"</c>).
/// </summary>
public string[] Aliases { get; }

public JavaPeerAliasesAttribute (params string[] aliases) => Aliases = aliases;
}

/// <summary>
/// Base attribute class for generated proxy types that enable AOT-safe type mapping
/// between Java and .NET types.
Expand Down
Loading