Skip to content

IL Linker Intrinsics + Marshal Methods, oh my! #8155

Description

@jonpryor

Android application type

.NET Android (net7.0-android, etc.)

Affected platform version

.NET 8 in main (ff6eb66)

Description

The .NET Linker will apply optimizations that can result in per-ABI assemblies, see e.g. https://github.com/dotnet/runtime/blob/318c0e6708ced35180fd5218170f82246e2f2bac/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.64bit.xml

LLVM Marshal Methods (8bc7a3e) updates assemblies.

What happens if we mix these together?

Steps to Reproduce

See intrinsics+mm.zip, which is:

  1. Create an Android Class Library project: dotnet new androidlib -n androidlib

  2. To (1), add MyRunner.java, which contains a virtual/abstract/etc. method. (There needs to be virtual methods so that the assembly contains marshal methods.)

    package com.example.androidlib;
    public abstract class MyRunner {
        public static void run(MyRunner r) {
            r.run();
        }
    
        public abstract void run();
    }
  3. To (1), update Class1.cs to inherit from the MyRunner type and override the method. Additionally, Class1.cs (or some other type in the same project/assembly) should use IntPtr.Size:

    namespace androidlib;
    
    public class Class1 : Com.Example.Androidlib.MyRunner
    {
        public static readonly bool Is64Bit = IntPtr.Size >= 8;
    
        public override void Run ()
        {
            Console.WriteLine("androidlib.Class1.Run!  IntPtr.Size={0}", IntPtr.Size);
        }
    }
  4. Enable trimming for androidlib.csproj:

     <IsTrimmable>True</IsTrimmable>
  5. Create an Android App project: dotnet new androidlib -n android

  6. Update android.csproj to reference androdilib.csproj:

    <ProjectReference Include="..\androidlib\androidlib.csproj" />
  7. Update MainActivity.cs to use the types from (2), (3):

    Com.Example.Androidlib.MyRunner.Run(new androidlib.Class1())

Build & run the resulting app in Release configuration.

The app builds successfully:

% ../../dotnet-local.sh build -c Release -p:AndroidSdkDirectory=$HOME/android-toolchain/sdk
# no errors

The app runs successfully:

% ../../dotnet-local.sh build -c Release -p:AndroidSdkDirectory=$HOME/android-toolchain/sdk -t:Install
# side-note: why does `Install` *re-optimize* assemblies?
MSBuild version 17.7.0-preview-23316-03+0fdab8fb8 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  androidlib -> …/intrinsics+mm/androidlib/bin/Release/net8.0-android/androidlib.dll
  android -> …/intrinsics+mm/android/bin/Release/net8.0-android/android.dll
  androidlib -> …/intrinsics+mm/androidlib/bin/Release/net8.0-android/androidlib.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
…
  [1/11] _Microsoft.Android.Resource.Designer.dll -> _Microsoft.Android.Resource.Designer.dll.so
  [1/11] android.dll -> android.dll.so
…

% ../../dotnet-local.sh build -c Release -p:AndroidSdkDirectory=$HOME/android-toolchain/sdk -t:StartAndroidActivity

However, the app is "wrong". Because we set $(IsTrimmable)=True on androidlib.dll, the linker will "inline" IntPtr.Size to a constant value based on the architecture, 8 for 64-bit platforms, 4 for 32-bit platforms. But…

% ikdasm obj/Release/net8.0-android/android-arm64/linked/shrunk/androidlib.dll
…
  .method public hidebysig virtual instance void 
          Run() cil managed
  {
    // Code size       21 (0x15)
    .maxstack  8
    IL_0000:  ldstr      "androidlib.Class1.Run!  IntPtr.Size={0}"
    IL_0005:  ldc.i4     0x4
    IL_000a:  box        [System.Private.CoreLib]System.Int32
    IL_000f:  call       void [System.Console]System.Console::WriteLine(string,
                                                                        object)
    IL_0014:  ret
  } // end of method Class1::Run

We can see that IntPtr.Size was inlined to 4 for arm64, where it should be 8!

Additionally, all of the androidlib.dll assemblies are identical!

% find obj -iname androidlib.dll | xargs shasum 
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-arm/linked/shrunk/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-arm/linked/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-x86/linked/shrunk/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-x86/linked/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-arm64/linked/shrunk/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-arm64/linked/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-x64/linked/shrunk/androidlib.dll
0dc7a87d79f4507abf22e258f4c7c5a151ec8321  obj/Release/net8.0-android/android-x64/linked/androidlib.dll

When running the app on a Pixel 6 (64-bit device!), we see the same erroneous behavior:

I DOTNET  : androidlib.Class1.Run!  IntPtr.Size=4

😱

Did you find any workaround?

Don't enable trimming; either don't set $(IsTrimmable) at all, or set $(IsTrimmable)=False.

I DOTNET  : androidlib.Class1.Run!  IntPtr.Size=8

Relevant log output

No response

Activity

  1. jonpryor commented on Jun 28, 2023

    @jonpryor
    ContributorAuthor

    If I update android.csproj to contain:

    <RuntimeIdentifiers>android-x64;android-arm64</RuntimeIdentifiers>

    i.e. only 64-bit platforms, then ikdasm obj/Release/net8.0-android/android-arm64/linked/shrunk/androidlib.dll shows that IntPtr.Size was inlined to 8, as expected.

  2. jonpryor commented on Jun 28, 2023

    @jonpryor
    ContributorAuthor

    If I remove use of marshal methods from androidlib by removing MyRunner.java and updating Class1.cs to just use IntPtr.Size (while keeping trimming enabled):

    namespace androidlib;
    
    public class Class1
    {
        public static readonly bool Is64Bit = IntPtr.Size >= 8;
    }

    Then the resulting app IL is as expected:

    % find obj -iname androidlib.dll | xargs shasum 
    289d3b268c35fc56fe28ee1658845e66666332f4  obj/Release/net8.0-android/android-arm/linked/shrunk/androidlib.dll
    289d3b268c35fc56fe28ee1658845e66666332f4  obj/Release/net8.0-android/android-arm/linked/androidlib.dll
    289d3b268c35fc56fe28ee1658845e66666332f4  obj/Release/net8.0-android/android-x86/linked/shrunk/androidlib.dll
    289d3b268c35fc56fe28ee1658845e66666332f4  obj/Release/net8.0-android/android-x86/linked/androidlib.dll
    a1da91d2d5e9ca6b38d8a5670bb16e9f06a86a9a  obj/Release/net8.0-android/android-arm64/linked/shrunk/androidlib.dll
    a1da91d2d5e9ca6b38d8a5670bb16e9f06a86a9a  obj/Release/net8.0-android/android-arm64/linked/androidlib.dll
    a1da91d2d5e9ca6b38d8a5670bb16e9f06a86a9a  obj/Release/net8.0-android/android-x64/linked/shrunk/androidlib.dll
    a1da91d2d5e9ca6b38d8a5670bb16e9f06a86a9a  obj/Release/net8.0-android/android-x64/linked/androidlib.dll

    32-bit assemblies differ from 64-bit assemblies, and:

      .field public static initonly bool Is64Bit
      .method private hidebysig specialname rtspecialname static 
              void  .cctor() cil managed
      {
        // Code size       17 (0x11)
        .maxstack  8
        IL_0000:  ldc.i4     0x8
        IL_0005:  ldc.i4.8
        IL_0006:  clt
        IL_0008:  ldc.i4.0
        IL_0009:  ceq
        IL_000b:  stsfld     bool androidlib.Class1::Is64Bit
        IL_0010:  ret
      } // end of method Class1::.cctor

    on the 64-bit androidlib.dll, IntPtr.Size is inlined to 8.

  3. jonpryor commented on Jun 28, 2023

    @jonpryor
    ContributorAuthor

    Disabling LLVM marshal methods by setting $(AndroidEnableMarshalMethods)=False is another workaround, and will result in per-arch assemblies.

  4. jonpryor commented on Jun 28, 2023

    @jonpryor
    ContributorAuthor
  5. jonpryor commented on Sep 26, 2024

    @jonpryor
    ContributorAuthor

    I believe this was fixed by #8253.

  6. locked and limited conversation to collaborators on Oct 27, 2024
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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions