Skip to content
Closed
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 @@ -16,6 +16,13 @@ static class ModelBuilder
{
const string ProxyTypeSuffix = "_Proxy";

// Workaround for https://github.com/dotnet/runtime/issues/127004
// When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the
// trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute
// references the same type. Set to false once the runtime bug is fixed to re-enable
// 3-arg conditional entries that allow unused framework bindings to be trimmed away.
const bool ForceUnconditionalEntries = true;

static readonly HashSet<string> EssentialRuntimeTypes = new (StringComparer.Ordinal) {
"java/lang/Object",
"java/lang/Class",
Expand Down Expand Up @@ -122,8 +129,15 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
model.ProxyTypes.Add (proxy);
}

model.Entries.Add (BuildEntry (peer, proxy, assemblyName, jniName));
if (proxy != null && peer.IsGenericDefinition) {
var entry = BuildEntry (peer, proxy, assemblyName, jniName);
model.Entries.Add (entry);

// Emit a TypeMapAssociation for every entry that has a proxy.
// The runtime's _proxyTypeMap (GetOrCreateProxyTypeMapping) is populated from
// TypeMapAssociationAttribute — NOT from TypeMapAttribute's 3rd arg.
// Without this, the proxy type map is empty and CreatePeer fails for
// interface types like IIterator where targetType-based lookup is needed.
if (proxy != null) {
model.Associations.Add (new TypeMapAssociationData {
SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName),
AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName),
Expand Down Expand Up @@ -353,7 +367,9 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr
proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
}

bool isUnconditional = IsUnconditionalEntry (peer);
// When ForceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap
// attributes to work around https://github.com/dotnet/runtime/issues/127004.
bool isUnconditional = ForceUnconditionalEntries || IsUnconditionalEntry (peer);
string? targetRef = null;
if (!isUnconditional) {
targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ sealed class TypeMapAssemblyEmitter
TypeReferenceHandle _iJavaPeerableRef;
TypeReferenceHandle _jniHandleOwnershipRef;
TypeReferenceHandle _jniObjectReferenceRef;
TypeReferenceHandle _jniObjectReferenceTypeRef;
TypeReferenceHandle _jniObjectReferenceOptionsRef;
TypeReferenceHandle _iAndroidCallableWrapperRef;
TypeReferenceHandle _jniEnvRef;
Expand Down Expand Up @@ -106,6 +107,7 @@ sealed class TypeMapAssemblyEmitter
MemberReferenceHandle _jniTypePeerReferenceRef;
MemberReferenceHandle _jniEnvTypesRegisterNativesRef;
MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef;
MemberReferenceHandle _shouldSkipActivationRef;

/// <summary>
/// Creates a new emitter.
Expand Down Expand Up @@ -182,6 +184,8 @@ void EmitTypeReferences ()
metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv"));
_jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference"));
_jniObjectReferenceTypeRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceType"));
_jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceOptions"));
_iAndroidCallableWrapperRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
Expand Down Expand Up @@ -229,10 +233,16 @@ void EmitMemberReferences ()
rt => rt.Void (),
p => p.AddParameter ().Type ().String ()));

// JniObjectReference..ctor(IntPtr handle, JniObjectReferenceType type)
// Note: The C# constructor has a default parameter (type = Invalid), but in IL there is only
// the 2-parameter overload. We must emit both parameters explicitly.
_jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => p.AddParameter ().Type ().IntPtr ()));
p => {
p.AddParameter ().Type ().IntPtr ();
p.AddParameter ().Type ().Type (_jniObjectReferenceTypeRef, true);
}));

// JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal
// Used by JI-style activation to clean up the original handle after constructing the peer.
Expand All @@ -251,6 +261,12 @@ void EmitMemberReferences ()
rt => rt.Type ().Boolean (),
p => { }));

// JavaPeerProxy.ShouldSkipActivation(IntPtr) -> bool (static method)
_shouldSkipActivationRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "ShouldSkipActivation",
sig => sig.MethodSignature ().Parameters (1,
rt => rt.Type ().Boolean (),
p => { p.AddParameter ().Type ().IntPtr (); }));

// JniNativeMethod..ctor(byte*, byte*, IntPtr)
_jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3,
Expand Down Expand Up @@ -620,9 +636,10 @@ void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef)
EmitCreateInstanceBodyWithLocals (
EncodeJniObjectReferenceAndObjectLocals,
encoder => {
// var jniRef = new JniObjectReference(handle);
// var jniRef = new JniObjectReference(handle, JniObjectReferenceType.Invalid);
encoder.LoadLocalAddress (0);
encoder.OpCode (ILOpCode.Ldarg_1); // handle
encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid
encoder.Call (_jniObjectReferenceCtorRef);

// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy);
Expand Down Expand Up @@ -667,9 +684,10 @@ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, Act
// dup obj (one copy for the call, one for the return)
encoder.OpCode (ILOpCode.Dup);

// var jniRef = new JniObjectReference(handle);
// var jniRef = new JniObjectReference(handle, JniObjectReferenceType.Invalid);
encoder.LoadLocalAddress (0);
encoder.OpCode (ILOpCode.Ldarg_1); // handle
encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid
encoder.Call (_jniObjectReferenceCtorRef);

// obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy);
Expand Down Expand Up @@ -851,6 +869,12 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy
encoder.Call (_withinNewObjectScopeRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

// Skip activation if a managed peer already exists for this Java handle
// (e.g., FinishCreateInstance after StartCreateInstance already registered the peer).
encoder.LoadArgument (1); // self (JNI handle)
encoder.Call (_shouldSkipActivationRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

if (!activationCtor.IsOnLeafType) {
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (targetTypeRef);
Expand All @@ -862,6 +886,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy

encoder.LoadLocalAddress (0);
encoder.LoadArgument (1); // self
encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid
encoder.Call (_jniObjectReferenceCtorRef);

if (activationCtor.IsOnLeafType) {
Expand Down Expand Up @@ -894,6 +919,11 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy
encoder.Call (_withinNewObjectScopeRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

// Skip activation if a managed peer already exists for this Java handle
encoder.LoadArgument (1); // self (JNI handle)
encoder.Call (_shouldSkipActivationRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

if (activationCtor.IsOnLeafType) {
encoder.LoadArgument (1); // self
encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ void Build ()
if (attrName == "RegisterAttribute") {
registerInfo = ParseRegisterAttribute (ca);
registerInfo = registerInfo with { JniName = registerInfo.JniName.Replace ('.', '/') };
} else if (attrName == "JniTypeSignatureAttribute") {
registerInfo = ParseJniTypeSignatureAttribute (ca);
} else if (attrName == "ExportAttribute") {
// [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner
} else if (IsKnownComponentAttribute (attrName)) {
Expand Down Expand Up @@ -218,6 +220,28 @@ internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca)
return ParseRegisterInfo (DecodeAttribute (ca));
}

internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca)
{
var value = DecodeAttribute (ca);

string jniName = "";
bool doNotGenerateAcw = false;

if (value.FixedArguments.Length > 0) {
jniName = (string?)value.FixedArguments [0].Value ?? "";
}

if (TryGetNamedArgument<bool> (value, "GenerateJavaPeer", out var generateJavaPeer)) {
doNotGenerateAcw = !generateJavaPeer;
}

return new RegisterInfo {
JniName = jniName.Replace ('.', '/'),
DoNotGenerateAcw = doNotGenerateAcw,
IsFromJniTypeSignature = true,
};
}

internal CustomAttributeValue<string> DecodeAttribute (CustomAttribute ca)
{
return ca.DecodeValue (customAttributeTypeProvider);
Expand Down Expand Up @@ -504,6 +528,7 @@ sealed record RegisterInfo
public string? Signature { get; init; }
public string? Connector { get; init; }
public bool DoNotGenerateAcw { get; init; }
public bool IsFromJniTypeSignature { get; init; }
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ public sealed record JavaPeerInfo
/// </summary>
public bool DoNotGenerateAcw { get; init; }

/// <summary>
/// True when the type was discovered via <c>[JniTypeSignatureAttribute]</c>
/// rather than <c>[RegisterAttribute]</c>. Used to resolve cross-assembly
/// alias ownership: <c>[Register]</c> types take precedence.
/// </summary>
public bool IsFromJniTypeSignature { get; init; }

/// <summary>
/// Types with component attributes ([Activity], [Service], etc.),
/// custom views from layout XML, or manifest-declared components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<string, JavaPeerInfo> results
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false,
IsUnconditional = isUnconditional,
CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor,
MarshalMethods = marshalMethods,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,25 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes

List<GeneratedAssembly> GenerateTypeMapAssemblies (List<JavaPeerInfo> allPeers, Version systemRuntimeVersion)
{
var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal);
// Move cross-assembly aliases into the first assembly that claims each JNI name.
// The ModelBuilder handles alias groups (multiple peers with the same JNI name)
// but only within a single assembly's peer list. When the same JNI name appears
// in different assemblies (e.g. Java.Lang.Object in Mono.Android and JavaObject in
// Java.Interop both map to java/lang/Object), we must merge them so the runtime
// doesn't crash on duplicate keys.
var peersByAssembly = MergeCrossAssemblyAliases (allPeers);

var generatedAssemblies = new List<GeneratedAssembly> ();
var perAssemblyNames = new List<string> ();
var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
foreach (var group in peersByAssembly) {
string assemblyName = $"_{group.Key}.TypeMap";
perAssemblyNames.Add (assemblyName);
var peers = group.ToList ();
foreach (var (assemblyName, peers) in peersByAssembly) {
string typeMapAssemblyName = $"_{assemblyName}.TypeMap";
perAssemblyNames.Add (typeMapAssemblyName);
var stream = new MemoryStream ();
generator.Generate (peers, stream, assemblyName);
generator.Generate (peers, stream, typeMapAssemblyName);
stream.Position = 0;
generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream));
logger.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count);
generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream));
logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count);
}
var rootStream = new MemoryStream ();
var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
Expand All @@ -164,6 +170,74 @@ List<GeneratedAssembly> GenerateTypeMapAssemblies (List<JavaPeerInfo> allPeers,
return generatedAssemblies;
}

/// <summary>
/// Groups peers by assembly, merging cross-assembly aliases into a single group.
/// When the same JNI name appears in multiple assemblies (e.g. <c>Java.Lang.Object</c>
/// in <c>Mono.Android</c> and <c>JavaObject</c> in <c>Java.Interop</c> both mapping
/// to <c>java/lang/Object</c>), peers from later assemblies are moved into the owner
/// assembly's group so the <see cref="ModelBuilder"/> can handle them as an alias group.
/// </summary>
/// <remarks>
/// Ownership is determined by <c>[Register]</c> over <c>[JniTypeSignature]</c> — the
/// canonical MCW binding type takes precedence. Among peers with the same attribute
/// kind, the first assembly in sorted order wins.
/// </remarks>
internal static List<(string AssemblyName, List<JavaPeerInfo> Peers)> MergeCrossAssemblyAliases (List<JavaPeerInfo> allPeers)
{
var groups = new SortedDictionary<string, List<JavaPeerInfo>> (StringComparer.Ordinal);

// Group by assembly (sorted order)
foreach (var peer in allPeers) {
if (!groups.TryGetValue (peer.AssemblyName, out var list)) {
list = [];
groups [peer.AssemblyName] = list;
}
list.Add (peer);
}

// Build JNI name → owner assembly map.
// [Register] types take precedence over [JniTypeSignature] types.
// Among peers of the same kind, the first assembly (sorted order) wins.
var jniNameOwner = new Dictionary<string, (string AssemblyName, bool IsFromJniTypeSignature)> (StringComparer.Ordinal);
foreach (var kvp in groups) {
string assemblyName = kvp.Key;
foreach (var peer in kvp.Value) {
if (!jniNameOwner.TryGetValue (peer.JavaName, out var current)) {
jniNameOwner [peer.JavaName] = (assemblyName, peer.IsFromJniTypeSignature);
} else if (current.IsFromJniTypeSignature && !peer.IsFromJniTypeSignature) {
// [Register] type takes ownership from [JniTypeSignature] type
jniNameOwner [peer.JavaName] = (assemblyName, false);
}
}
}

// Move colliding peers to the owner assembly
var movedPeers = new List<(JavaPeerInfo Peer, string TargetAssembly)> ();
foreach (var kvp in groups) {
string assemblyName = kvp.Key;
foreach (var peer in kvp.Value) {
var owner = jniNameOwner [peer.JavaName];
if (!string.Equals (owner.AssemblyName, assemblyName, StringComparison.Ordinal)) {
movedPeers.Add ((peer, owner.AssemblyName));
}
}
}

foreach (var moved in movedPeers) {
groups [moved.Peer.AssemblyName].Remove (moved.Peer);
groups [moved.TargetAssembly].Add (moved.Peer);
}

// Return non-empty groups
var result = new List<(string, List<JavaPeerInfo>)> ();
foreach (var kvp in groups) {
if (kvp.Value.Count > 0) {
result.Add ((kvp.Key, kvp.Value));
}
}
return result;
}

List<GeneratedJavaSource> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers)
{
var jcwGenerator = new JcwJavaSourceGenerator ();
Expand Down
18 changes: 18 additions & 0 deletions src/Mono.Android/Java.Interop/JavaPeerProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ protected JavaPeerProxy (
/// </summary>
/// <returns>A factory for creating containers of the target type, or null if not supported.</returns>
public virtual JavaPeerContainerFactory? GetContainerFactory () => null;

/// <summary>
/// Returns <see langword="true"/> when the UCO constructor callback should skip
/// activation because a managed peer already exists for the given JNI handle
/// (e.g., when called from <c>FinishCreateInstance</c> after <c>StartCreateInstance</c>
/// already registered the peer).
/// </summary>
public static bool ShouldSkipActivation (IntPtr jniSelf)
{
var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid);
var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference);
if (peer == null) {
return false;
}
var state = peer.JniManagedPeerState;
return (state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable
&& (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
try {
objClass = JniEnvironment.Types.GetObjectClass (selfRef);
targetClass = JniEnvironment.Types.FindClass (targetJniName);
return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null;
var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass);
return isAssignable ? proxy : null;
} finally {
JniObjectReference.Dispose (ref objClass);
JniObjectReference.Dispose (ref targetClass);
Expand Down
Loading