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 @@ -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
72 changes: 72 additions & 0 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 IntPtr s_classGetInterfacesMethod;

internal static TrimmableTypeMap Instance =>
s_instance ?? throw new InvalidOperationException (
Expand Down Expand Up @@ -255,6 +256,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 }) {
var result = TryMatchInterfaces (self, jniClass, targetType);
if (result != null) {
return result;
}
}

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

// getInterfaces() returns only directly declared interfaces (not transitive),
// so we recurse into super-interfaces to find the matching TypeMap entry.
// Use raw JNI refs so temporary class lookups don't dispose shared managed peers.
static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType)
{
var interfaces = JNIEnv.CallObjectMethod (jniClass.Handle, GetClassGetInterfacesMethod ());
try {
if (interfaces == IntPtr.Zero) {
return null;
}

int count = JNIEnv.GetArrayLength (interfaces);
for (int i = 0; i < count; i++) {
var iface = JNIEnv.GetObjectArrayElement (interfaces, i);
try {
var ifaceRef = new JniObjectReference (iface);
var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef);
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, ifaceRef, targetType);
if (result != null) {
return result;
}
} finally {
JNIEnv.DeleteLocalRef (iface);
}
}
} finally {
JNIEnv.DeleteLocalRef (interfaces);
}

return null;
}

static IntPtr GetClassGetInterfacesMethod ()
{
var method = s_classGetInterfacesMethod;
if (method != IntPtr.Zero) {
return method;
}

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

Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, IntPtr.Zero);
return s_classGetInterfacesMethod;
}

static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType)
{
if (targetType is null) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,45 @@ public void TryGetArrayType_PrimitiveLeaf_DoesNotRequireRankMapEntry ()
Assert.AreEqual (typeof (byte[][]), jaggedByteArrayType);
}

[Test]
public void InterfaceProxyLookup_DoesNotLeakGlobalRefs ()
{
AssumeTrimmableTypeMapEnabled ();

using var map = new JavaDictionary ();
map.Add ("key", "value");

EnumerateKeys (map);
CollectGarbage (times: 3);
int grefBefore = Java.Interop.Runtime.GlobalReferenceCount;

const int iterationCount = 100;
for (int i = 0; i < iterationCount; i++) {
EnumerateKeys (map);
}

CollectGarbage (times: 3);
int grefAfter = Java.Interop.Runtime.GlobalReferenceCount;

Assert.AreEqual (grefBefore, grefAfter,
$"Interface proxy lookup leaked global references after {iterationCount} iterations. Before={grefBefore}, After={grefAfter}");

static void EnumerateKeys (JavaDictionary map)
{
var keys = map.Keys;
try {
var enumerator = keys.GetEnumerator ();
try {
Assert.IsTrue (enumerator.MoveNext ());
} finally {
(enumerator as IDisposable)?.Dispose ();
}
} finally {
(keys as IDisposable)?.Dispose ();
}
}
}

static ConcurrentDictionary<Type, JavaPeerProxy> GetProxyCache (TrimmableTypeMap instance)
{
var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic);
Expand Down Expand Up @@ -312,6 +351,15 @@ static async Task WaitForGC (Func<bool> predicate, string message, int timeoutMi
Assert.IsTrue (predicate (), message);
}

static void CollectGarbage (int times)
{
for (int i = 0; i < times; i++) {
GC.Collect (generation: 2, mode: GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers ();
JniEnvironment.Runtime.ValueManager.CollectPeers ();
}
}

static IntPtr noPinActionPointer;

static unsafe void NoPinActionHelper (int depth, Action action)
Expand Down
Loading