Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5c2760d
Remove ForceUnconditionalEntries workaround
simonrozsival May 14, 2026
cb4f4ed
TMP: test
simonrozsival May 15, 2026
b30dd88
Replace [DynamicDependency] with NoInlining hack, fix AppFunctionStat…
simonrozsival May 16, 2026
a9bd0c1
Revert unnecessary changes to ServerCertificateCustomValidator
simonrozsival May 18, 2026
1ccae1f
Add Java interface walk fallback to trimmable typemap resolution
simonrozsival May 18, 2026
cf64a40
Optimize: merge interface walk into hierarchy loop, cache getInterfac…
simonrozsival May 18, 2026
55706ee
Use pattern matching for interface guard, add comment about getInterf…
simonrozsival May 18, 2026
dbc8983
Keep [DynamicDependency] attributes, remove obsolete API 21-23 workar…
simonrozsival May 18, 2026
218e295
Add tests for alias group base entry conditionality
simonrozsival May 18, 2026
901b283
Use JniEnvironment primitives instead of raw IntPtr JNI calls
simonrozsival May 18, 2026
8a868e1
Simplify: use Java.Lang.Class.GetInterfaces() instead of raw JNI
simonrozsival May 18, 2026
f6b1e7b
Fix review findings: DoNotRegister, remove extra blank line, clean up…
simonrozsival May 18, 2026
f546bce
Dispose interface Class[] elements to avoid gref leaks
simonrozsival May 18, 2026
fb52640
Move TrustManager test to own file in Xamarin.Android.Net directory
simonrozsival May 18, 2026
612ed3a
Fix trimmable typemap interface GREF handling
simonrozsival May 19, 2026
f9c3dd9
Fix trimmable typemap interface lookup JNI refs
simonrozsival May 19, 2026
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,13 +16,6 @@ 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 @@ -189,13 +182,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
}

// Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations)
// When ForceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just
// like BuildEntry does: dotnet/runtime#127004 strips the TypeMapAssociation that keeps the
// holder alive when a TypeMap entry references the same type, leaving the dictionary key
// missing at runtime and breaking hierarchy lookups for essential types like
// java/lang/String and java/lang/Object.
bool aliasBaseUnconditional = ForceUnconditionalEntries
|| EssentialRuntimeTypes.Contains (jniName)
bool aliasBaseUnconditional = EssentialRuntimeTypes.Contains (jniName)
|| peersForName.Any (IsUnconditionalEntry);
Comment thread
simonrozsival marked this conversation as resolved.
model.Entries.Add (new TypeMapAttributeData {
JniName = jniName,
Expand Down Expand Up @@ -406,9 +393,7 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr
proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
}

// 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);
bool isUnconditional = IsUnconditionalEntry (peer);
string? targetRef = null;
if (!isUnconditional) {
targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
Expand Down
92 changes: 83 additions & 9 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class TrimmableTypeMap
static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy ();
static TrimmableTypeMap? s_instance;
static bool s_nativeMethodsRegistered;
static JniMethodInfo? s_classGetInterfacesMethod;

internal static TrimmableTypeMap Instance =>
s_instance ?? throw new InvalidOperationException (
Expand All @@ -31,6 +32,7 @@ public class TrimmableTypeMap
readonly ITypeMap _typeMap;
readonly ConcurrentDictionary<Type, JavaPeerProxy> _proxyCache = new ();
readonly ConcurrentDictionary<string, JavaPeerProxy[]> _jniProxyCache = new (StringComparer.Ordinal);
readonly ConcurrentDictionary<(string ClassName, Type TargetType), JavaPeerProxy> _interfaceProxyCache = new ();

TrimmableTypeMap (ITypeMap typeMap)
{
Expand Down Expand Up @@ -255,6 +257,19 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
}
}

// When targetType is an interface, also check the Java interfaces
// at each level. getInterfaces() only returns directly declared
// interfaces so we must call it at each class in the hierarchy.
// This handles the case where an intermediate class entry (e.g.,
// X509ExtendedTrustManager) was trimmed but the Java interface
// entry (e.g., X509TrustManager) survives.
if (targetType is { IsInterface: true } && className != null) {
var result = GetProxyForJavaInterfaces (self, jniClass, className, targetType);
if (result != null) {
return result;
}
}

var super = JniEnvironment.Types.GetSuperclass (jniClass);
JniObjectReference.Dispose (ref jniClass);
jniClass = super;
Expand All @@ -266,6 +281,70 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
return null;
}

static JavaPeerProxy? GetProxyForJavaInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, string className, Type targetType)
{
var proxy = self._interfaceProxyCache.GetOrAdd (
(className, targetType),
_ => TryMatchInterfaces (self, jniClass, targetType) ?? s_noPeerSentinel);
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
}

// getInterfaces() returns only directly declared interfaces (not transitive),
// so we recurse into super-interfaces to find the matching TypeMap entry.
static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType)
{
var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (jniClass, GetClassGetInterfacesMethod ());
try {
if (!interfaces.IsValid) {
return null;
}

int count = JniEnvironment.Arrays.GetArrayLength (interfaces);
for (int i = 0; i < count; i++) {
var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i);
try {
var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface);
if (ifaceName != null) {
var proxy = self.GetProxyForJniClass (ifaceName, targetType);
if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) {
return proxy;
}
}

// Recurse into super-interfaces
var result = TryMatchInterfaces (self, iface, targetType);
if (result != null) {
return result;
}
} finally {
JniObjectReference.Dispose (ref iface);
}
}
} finally {
JniObjectReference.Dispose (ref interfaces);
}

return null;
}

static JniMethodInfo GetClassGetInterfacesMethod ()
{
var method = s_classGetInterfacesMethod;
if (method != null) {
return method;
}

var classClass = JniEnvironment.Types.FindClass ("java/lang/Class");
try {
method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;");
} finally {
JniObjectReference.Dispose (ref classClass);
}

var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null);
return previous ?? method;
}

static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType)
{
if (targetType is null) {
Expand Down Expand Up @@ -316,16 +395,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
/// closed subclasses of an open generic class peer.
/// </summary>
/// <remarks>
/// Implementers of an open generic <em>interface</em> peer are intentionally
/// not matched here: <see cref="TryGetProxyFromHierarchy"/> walks only the
/// JNI class chain (<c>getSuperclass</c>), never JNI interfaces, so the
/// proxy returned from that walk is always a class peer. Matching on
/// <c>Type.GetInterfaces()</c> would also force a trimmer
/// Open generic <em>interface</em> peers are intentionally not matched here:
/// matching on <c>Type.GetInterfaces()</c> would force a trimmer
/// <c>DynamicallyAccessedMembers(Interfaces)</c> annotation up the chain
/// (ultimately into Java.Interop's <c>CreatePeer</c> API). If we ever need
/// to discover interface peers, the generator should emit an explicit
/// implementer→interface map so runtime can avoid reflection over
/// interface lists.
/// (ultimately into Java.Interop's <c>CreatePeer</c> API). Interface peer
/// discovery is handled from the Java class metadata instead.
/// </remarks>
internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,25 +180,9 @@ private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManag
index = i;
return x509TrustManager;
}

// On API 21-23, the default Java trust manager is TrustManagerImpl from Conscrypt. The class implements X509TrustManager
// but the .NET pattern matching will fail in this case and we need to cast it explicitly.
int apiLevel = (int)Build.VERSION.SdkInt;
if (apiLevel <= 23) {
if (IsTrustManagerImpl (trustManager)) {
index = i;
return trustManager.JavaCast<IX509TrustManager> ();
}
}
}

throw new InvalidOperationException($"Could not find {nameof(IX509TrustManager)} in {nameof(ITrustManager)} array.");

static bool IsTrustManagerImpl (ITrustManager trustManager)
{
var javaClassName = JNIEnv.GetClassNameFromInstance (trustManager.Handle);
return javaClassName.Equals ("com/android/org/conscrypt/TrustManagerImpl", StringComparison.Ordinal);
}
}

private static ITrustManager[] ModifyTrustManagersArray (ITrustManager[] trustManagers, int originalTrustManagerIndex, IX509TrustManager replacement)
Expand Down
6 changes: 3 additions & 3 deletions src/Mono.Android/map.csv
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,9 @@ E,36,android/app/appfunctions/AppFunctionException.ERROR_ENTERPRISE_POLICY_DISAL
E,36,android/app/appfunctions/AppFunctionException.ERROR_FUNCTION_NOT_FOUND,1003,Android.App.AppFunctions.AppFunctionError,FunctionNotFound,remove,
E,36,android/app/appfunctions/AppFunctionException.ERROR_INVALID_ARGUMENT,1001,Android.App.AppFunctions.AppFunctionError,InvalidArgument,remove,
E,36,android/app/appfunctions/AppFunctionException.ERROR_SYSTEM_ERROR,2000,Android.App.AppFunctions.AppFunctionError,SystemError,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DEFAULT,0,Android.App.AppFunctions.AppFunctionState,Default,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DISABLED,2,Android.App.AppFunctions.AppFunctionState,Disabled,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_ENABLED,1,Android.App.AppFunctions.AppFunctionState,Enabled,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DEFAULT,0,Android.App.AppFunctions.AppFunctionEnabledState,Default,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DISABLED,2,Android.App.AppFunctions.AppFunctionEnabledState,Disabled,remove,
E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_ENABLED,1,Android.App.AppFunctions.AppFunctionEnabledState,Enabled,remove,
E,37,android/app/appfunctions/AppFunctionMetadata.SCOPE_ACTIVITY,1,Android.App.AppFunctions.AppFunctionMetadataScope,Activity,remove,
E,37,android/app/appfunctions/AppFunctionMetadata.SCOPE_GLOBAL,0,Android.App.AppFunctions.AppFunctionMetadataScope,Global,remove,
E,37,android/app/AppInteractionAttribution.INTERACTION_TYPE_OTHER,0,Android.App.AppInteractionAttributionInteractionType,Other,remove,
Expand Down
2 changes: 1 addition & 1 deletion src/Mono.Android/methodmap.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4100,7 +4100,7 @@
36,android.app.appfunctions,AppFunctionException,getErrorCategory,return,Android.App.AppFunctions.AppFunctionErrorCategory
36,android.app.appfunctions,AppFunctionException,getErrorCode,return,Android.App.AppFunctions.AppFunctionError
36,android.app.appfunctions,AppFunctionException,writeToParcel,flags,Android.OS.ParcelableWriteFlags
36,android.app.appfunctions,AppFunctionManager,setAppFunctionEnabled,newEnabledState,Android.App.AppFunctions.AppFunctionState
36,android.app.appfunctions,AppFunctionManager,setAppFunctionEnabled,newEnabledState,Android.App.AppFunctions.AppFunctionEnabledState
36,android.app.appfunctions,ExecuteAppFunctionRequest,writeToParcel,flags,Android.OS.ParcelableWriteFlags
36,android.app.appfunctions,ExecuteAppFunctionResponse,writeToParcel,flags,Android.OS.ParcelableWriteFlags
36,android.app,ApplicationStartInfo,getStartComponent,return,Android.App.ApplicationStartInfoStartComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,54 @@ public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivatio
// Both peers get associations to alias holder
Assert.Equal (2, model.Associations.Count);
}

[Fact]
public void Build_AllMcwAliasGroup_BaseEntryIsConditional ()
{
// When all peers in an alias group are MCW bindings (trimmable),
// the base alias-holder entry should be conditional (3-arg).
var peers = new List<JavaPeerInfo> {
MakeMcwPeer ("test/AllMcw", "Test.First", "A") with { DoNotGenerateAcw = true },
MakeMcwPeer ("test/AllMcw", "Test.Second", "A") with { DoNotGenerateAcw = true },
};

var model = BuildModel (peers);
var baseEntry = model.Entries.Single (e => e.JniName == "test/AllMcw");
Assert.False (baseEntry.IsUnconditional, "All-MCW alias group base entry should be conditional");
Assert.NotNull (baseEntry.TargetTypeReference);
}

[Fact]
public void Build_MixedAcwMcwAliasGroup_BaseEntryIsUnconditional ()
{
// When at least one peer in an alias group is an ACW (unconditional),
// the base alias-holder entry should be unconditional (2-arg).
var peers = new List<JavaPeerInfo> {
MakeMcwPeer ("test/Mixed", "Test.Mcw", "A") with { DoNotGenerateAcw = true },
MakeAcwPeer ("test/Mixed", "Test.Acw", "A"),
};

var model = BuildModel (peers);
var baseEntry = model.Entries.Single (e => e.JniName == "test/Mixed");
Assert.True (baseEntry.IsUnconditional, "Mixed alias group with ACW should have unconditional base entry");
Assert.Null (baseEntry.TargetTypeReference);
}

[Fact]
public void Build_EssentialTypeAliasGroup_BaseEntryIsUnconditional ()
{
// Essential runtime types (java/lang/Object etc.) should always be unconditional,
// even when all peers are MCW bindings.
var peers = new List<JavaPeerInfo> {
MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
MakeMcwPeer ("java/lang/Object", "Java.Lang.Another", "Mono.Android"),
};

var model = BuildModel (peers);
var baseEntry = model.Entries.Single (e => e.JniName == "java/lang/Object");
Assert.True (baseEntry.IsUnconditional, "Essential type alias group should have unconditional base entry");
Assert.Null (baseEntry.TargetTypeReference);
}
}

public class ConditionalAttributes
Expand Down Expand Up @@ -172,14 +220,12 @@ public void Build_UserAcwType_IsUnconditional ()
public void Build_McwBinding_IsTrimmable ()
{
// MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential.
// When ForceUnconditionalEntries is enabled (workaround for dotnet/runtime#127004),
// all entries become unconditional.
var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true };
var model = BuildModel (new [] { peer });

Assert.Single (model.Entries);
Assert.True (model.Entries [0].IsUnconditional);
Assert.Null (model.Entries [0].TargetTypeReference);
Assert.False (model.Entries [0].IsUnconditional);
Assert.Equal ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference);
}

[Fact]
Expand Down Expand Up @@ -248,8 +294,8 @@ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string m
[Fact]
public void Build_SinglePeer_HasAssociation ()
{
// When ForceUnconditionalEntries is enabled, single peers emit associations
// so the runtime proxy type map is populated.
// Single peers with generated proxies emit associations so the runtime proxy
// type map is populated.
var peer = MakePeerWithActivation ("my/app/MainActivity", "MyApp.MainActivity", "App");
var model = BuildModel (new [] { peer }, "MyTypeMap");

Expand Down Expand Up @@ -338,8 +384,8 @@ public void Fixture_McwBinding_IsTrimmable (string javaName)
var peer = FindFixtureByJavaName (javaName);
Assert.True (peer.DoNotGenerateAcw);
var model = BuildModel (new [] { peer });
// ForceUnconditionalEntries workaround makes all entries unconditional
Assert.True (model.Entries [0].IsUnconditional);
Assert.False (model.Entries [0].IsUnconditional);
Assert.NotNull (model.Entries [0].TargetTypeReference);
}
}

Expand Down Expand Up @@ -776,7 +822,6 @@ public class PeBlobValidation
[Fact]
public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip ()
{
// With ForceUnconditionalEntries, both are emitted as 2-arg unconditional
var objectPeer = FindFixtureByJavaName ("java/lang/Object");
var activityPeer = FindFixtureByJavaName ("android/app/Activity");

Expand All @@ -793,7 +838,7 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip ()

var activityEntry = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity");
Assert.NotNull (activityEntry.jniName);
Assert.Null (activityEntry.targetRef); // unconditional due to ForceUnconditionalEntries
Assert.Equal ("Android.App.Activity, TestFixtures", activityEntry.targetRef);
});
}

Expand All @@ -818,22 +863,20 @@ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName,
}

[Fact]
public void FullPipeline_McwBinding_Emits2ArgAttribute_WithWorkaround ()
public void FullPipeline_McwBinding_Emits3ArgAttribute ()
{
// With ForceUnconditionalEntries workaround for dotnet/runtime#127004,
// MCW bindings are emitted as 2-arg unconditional.
var peer = FindFixtureByJavaName ("android/app/Activity");
var model = BuildModel (new [] { peer }, "Blob2ArgWorkaround");
var model = BuildModel (new [] { peer }, "Blob3ArgConditional");
Assert.Single (model.Entries);
Assert.True (model.Entries [0].IsUnconditional);
Assert.False (model.Entries [0].IsUnconditional);

EmitAndVerify (model, "Blob2ArgWorkaround", (pe, reader) => {
EmitAndVerify (model, "Blob3ArgConditional", (pe, reader) => {
var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader);

Assert.Equal ("android/app/Activity", jniName);
Assert.NotNull (proxyRef);
Assert.Contains ("Android_App_Activity_Proxy", proxyRef!);
Assert.Null (targetRef); // unconditional due to ForceUnconditionalEntries
Assert.Equal ("Android.App.Activity, TestFixtures", targetRef);
});
}
}
Expand Down
Loading