Skip to content

AndroidTypeMapImplementation trimmable removes types that are used in type casts #13286

Open

Description

@Cheesebaron

Android framework version

Other, net11.0-android (Preview)

Affected platform version

11.0 RC2

Description

When you have <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation> in your csproj and you use a method that retuns Java.Lang.Object and you use a regular type cast. Like in my repro sample I use this.GetSystemService(Context.StorageStatsService) and cast it to StorageStatsManager? then the StorageStatsManager type is trimmed away and the code ends up throwing a InvalidCastException.

var service = (StorageStatsManager?)this.GetSystemService(Context.StorageStatsService);

If I use .JavaCast<StorageStatsManager>() the type is retained and all is good.

var service = this.GetSystemService(Context.StorageStatsService).JavaCast<StorageStatsManager>();

Steps to Reproduce

I have a small repro here: https://github.com/Cheesebaron/JavaLangObjectCastIssueNet11
Run it with dotnet run --project net11cast/net11cast.csproj /p:Configuration=Release

  1. Create new Android project targeting net11.0-android
  2. Add <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation> in your release config
  3. Use a method like GetSystemService() which returns Java.Lang.Object
  4. Type cast the Java.Lang.Object to something concrete like StorageStatsManager
  5. Run in release mode

Did you find any workaround?

No response

Relevant log output

10-27 15:38:04.500  1508  1508 D AndroidRuntime: Shutting down VM
10-27 15:38:04.500  1508  1508 E AndroidRuntime: FATAL EXCEPTION: main
10-27 15:38:04.500  1508  1508 E AndroidRuntime: Process: com.companyname.net11cast, PID: 1508
10-27 15:38:04.500  1508  1508 E AndroidRuntime: android.runtime.JavaProxyThrowable: [System.InvalidCastException]: Unable to cast object of type 'Java.Lang.Object' to type 'Android.App.Usage.StorageStatsManager'.
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.<IsInstanceOf_NoCacheLookup>g____PInvoke|4_0(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.<IsInstanceOf_NoCacheLookup>g____PInvoke|4_0(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.IsInstanceOf_NoCacheLookup + 0x5(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.ChkCastAny_NoCacheLookup + 0x0(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.ChkCast_Helper + 0x16(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.ChkCastClassSpecial + 0x4b(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at System.Runtime.CompilerServices.CastHelpers.ChkCastClass + 0xe(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at net11cast.MainActivity+<GetStorageInformation>d__1.MoveNext + 0x26(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at net11cast.MainActivity.OnCreate + 0x3f(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at Android.App.Activity.__n_OnCreate_Landroid_os_Bundle_ + 0x0(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at Java.Interop.JniMarshal.SafeInvokeAction + 0x10(Unknown Source)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at scrc648536b707fa8b2047.MainActivity.n_OnCreate_Landroid_os_Bundle_(Native Method)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at scrc648536b707fa8b2047.MainActivity.onCreate(r8-map-id-36836d1d3470e82bd783ca3e552fdc39854a9895817d0fb5a9b359a5c4c62e10:1)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:7825)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:7814)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1307)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3246)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3410)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2017)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:107)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:214)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:7397)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
10-27 15:38:04.500  1508  1508 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
10-27 15:38:04.508  1508  1508 I MonoDroid: UNHANDLED EXCEPTION:
10-27 15:38:04.519  1508  1508 I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception_WasThrown, Android.Runtime.JavaProxyThrowable
10-27 15:38:04.519  1508  1508 I MonoDroid:
10-27 15:38:04.519  1508  1508 I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
10-27 15:38:04.519  1508  1508 I MonoDroid: android.runtime.JavaProxyThrowable
10-27 15:38:04.519  1508  1508 I MonoDroid:
10-27 15:38:04.525  3252  4030 W ActivityTaskManager:   Force finishing activity com.companyname.net11cast/scrc648536b707fa8b2047.MainActivity
10-27 15:38:04.525  3252  1539 I DropBoxManagerService: add tag=data_app_crash isTagEnabled=true flags=0x2
10-27 15:38:04.558  3252  4030 D ActivityTrigger: ActivityTrigger activityPauseTrigger
10-27 15:38:04.566  1508  1508 I Process : Sending signal. PID: 1508 SIG: 9
10-27 15:38:04.570  3252 23572 I AdrenoGLES: QUALCOMM build                   : 99d190e401, I5e6feab613
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Build Date                       : 12/06/21
10-27 15:38:04.570  3252 23572 I AdrenoGLES: OpenGL ES Shader Compiler Version: EV031.27.05.11_01
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Local Branch                     :
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Remote Branch                    : quic/LA.UM.8.11.1.c25
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Remote Branch                    : NONE
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Reconstruct Branch               : NOTHING
10-27 15:38:04.570  3252 23572 I AdrenoGLES: Build Config                     : S P 8.0.12 AArch64
10-27 15:38:04.576  3252 23572 I AdrenoGLES: PFP: 0x016ee187, ME: 0x00000000
10-27 15:38:04.589  3252  6425 I ActivityManager: Process com.companyname.net11cast (pid 1508) has died: fore TOP

Activity

  1. added this to the .NET 12 milestone on Oct 27, 2026
  2. simonrozsival commented on Oct 27, 2026

    @simonrozsival
    Member

    Thanks for the bugreport, @Cheesebaron. I'll take a look. It seems that for some reason we're either trimming out the StorageStatsManager type in the first place or we incorrectly create the wrapper in this.GetSystemService(Context.StorageStatsService) as Java.Lang.Object instead of StorageStatsManager.

  3. simonrozsival commented on Oct 28, 2026

    @simonrozsival
    Member

    I reproduced this locally and reduced it to a two-file application. The failure is caused by an ILLink TypeMap retention bug; the JNI reference/global-reference wrapping itself is working correctly.

    Minimal reproduction

    The only required non-default project property is:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net11.0-android</TargetFramework>
        <OutputType>Exe</OutputType>
        <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation>
      </PropertyGroup>
    </Project>

    The framework type can be reduced from StorageStatsManager to the universally available PowerManager:

    using Android.App;
    using Android.OS;
    using Android.Util;
    
    [Activity (MainLauncher = true)]
    public class MainActivity : Activity
    {
        protected override void OnCreate (Bundle savedInstanceState)
        {
            base.OnCreate (savedInstanceState);
            var powerManager = (PowerManager) GetSystemService ("power");
            Log.Info ("Issue13286", $"IsInteractive={powerManager.IsInteractive}");
        }
    }

    The cast, property access, and log call all remain in the final linked application, so this is not dead-code elimination.

    Results

    Runtime/configuration Result
    CoreCLR Release + trimmable TypeMap PowerManager proxy removed; cast fails
    CoreCLR Release + default TypeMap Succeeds
    NativeAOT Release + trimmable TypeMap Proxy retained; logs IsInteractive=True
    JavaCast<PowerManager>() Succeeds

    Inspection of the linked CoreCLR output shows:

    • The application still contains castclass Android.OS.PowerManager.
    • Mono.Android.dll still contains the PowerManager wrapper and the called IsInteractive member.
    • _Mono.Android.TypeMap.dll no longer contains Android_OS_PowerManager_Proxy.
    • The Java object reference is valid. Since the proxy is absent, runtime hierarchy lookup falls back to the java/lang/Object proxy and creates Java.Lang.Object; the surviving CLR castclass then throws.

    JavaCast<T>() works because its generic parameter has DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors), providing stronger rooting than an ordinary CLR cast.

    ILLink root cause

    The final TypeMap specification from dotnet/runtime#113362 explicitly requires an External Type Map entry to be retained when its trim target is referenced by isinst or castclass (and also by several other operations):

    https://github.com/dotnet/runtime/blob/523349107aca82ae8fa2feaaca5a1767619f1111/docs/design/features/typemap.md#external-type-map

    ILLink initially records the Android attribute as a deferred entry keyed by PowerManager:

    [assembly: TypeMap<Object>(
        "android/os/PowerManager",
        typeof(Android_OS_PowerManager_Proxy),
        typeof(PowerManager))]

    However, MarkStep.MarkInstruction only notifies TypeMapHandler for Code.Isinst:

    https://github.com/dotnet/runtime/blob/523349107aca82ae8fa2feaaca5a1767619f1111/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs#L4097-L4135

    case Code.Isinst:
        ...
        _typeMapHandler.ProcessType(type);

    There is no Code.Castclass case. The instruction falls through to ordinary MarkType(...), which preserves the PowerManager type but does not call TypeMapHandler.ProcessType. The deferred external entry therefore remains unmarked and its target, Android_OS_PowerManager_Proxy, is removed.

    NativeAOT handles both opcodes through the same ImportCasting dependency and marks the necessary type symbol:

    https://github.com/dotnet/runtime/blob/523349107aca82ae8fa2feaaca5a1767619f1111/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs#L319-L330

    This accounts for the CoreCLR/NativeAOT difference.

    The ILLink implementation also currently makes isinst TypeMap processing conditional on the UnusedTypeChecks optimization and does not appear to implement the specification's retention rule for the owning type of instance call/callvirt instructions. The latter is relevant here because the surviving PowerManager.IsInteractive call should independently retain the entry.

    Disabling unusedtypechecks, as done by commit b90c559 for an earlier trimming issue, does not fix this case: the cast already survives; the missing operation is the TypeMap dependency notification.

    I did not find an existing dotnet/runtime issue covering this exact castclass omission. dotnet/runtime#127004 and dotnet/runtime#127504 are related TypeMap retention/order bugs, fixed by dotnet/runtime#127005, but their conditions do not match this failure.

    The likely runtime fix is to process direct castclass and isinst targets before the isinst-specific removal optimization, plus add a linker test where a reference-type castclass is the sole reason an External Type Map entry is retained. The current TypeMap tests cover isinst and value-type unboxing, but not this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions