Skip to content

Commit

Permalink
[One .NET] fix [Export] for Release builds (#6235)
Browse files Browse the repository at this point in the history
Imagine you use the `[Export]` attribute:

	partial class Example : Java.Lang.Object {
	    [Java.Interop.Export]
	    public void newJavaMethod()
	    {
	    }
	}

If you then build this type in a .NET 6+ app in Release config,
the build will fail with a `NullReferenceException`:

	Xamarin.Android.Common.targets(1413,3): error XA4209: Failed to generate Java type for class: UnnamedProject.ContainsExportedMethods due to System.NullReferenceException: Object reference not set to an instance of an object.
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator.Signature..ctor(String name, String signature, String connector, String managedParameters, String outerType, String superCall)
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator.Signature..ctor(MethodDefinition method, ExportAttribute export, IMetadataResolver cache)
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator.AddMethod(MethodDefinition registeredMethod, MethodDefinition implementedMethod)
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator..ctor(TypeDefinition type, String outerType, Action`2 log, IMetadataResolver resolver)
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator..ctor(TypeDefinition type, Action`2 log, IMetadataResolver resolver)
	    at Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator..ctor(TypeDefinition type, Action`2 log, TypeDefinitionCache cache)
	    at Xamarin.Android.Tasks.GenerateJavaStubs.CreateJavaSources(IEnumerable`1 javaTypes, TypeDefinitionCache cache)

The `NullReferenceException` happens because the `System.Void` type
in `System.Runtime.dll` is actually a type forwarder to
`System.Private.CoreLib.dll`:

	[assembly: TypeForwardedTo(typeof(void))]

and in Release config, `JavaCallableWrapperGenerator` runs on the
*linked* assembly output, and once `System.Runtime.dll` is linked
it no longer contains type forwarders.  Consequently
`JavaCallableWrapperGenerator` is unable to resolve the type
`System.Void, System.Runtime`, as the type forwarder no longer exists.

I found we could solve this issue by preserving the types in
`System.Runtime.dll`:

	<linker>
	  <assembly fullname="System.Runtime">
	    <type fullname="System.Void" />
	  </assembly>
	</linker>

However, while this works, it meant that *none* of those types could
be linked at all, which increased app sizes.

"Split the difference" by instead preserving the `ToString()` method:

	<linker>
	  <assembly fullname="System.Runtime">
	    <type fullname="System.Void">
	      <method name="ToString" />
	    </type>
	  </assembly>
	</linker>

Additionally, preserve all other built-in types such as `Int32` and
`Boolean`.

The result of this is that a linked `System.Runtime.dll` increases
in size by ~150 bytes.

The test now passes, and I believe most usage of `[Export]` should
work now for `Release` builds.
  • Loading branch information
jonathanpeppers committed Aug 27, 2021
1 parent 5b5bb62 commit 8460867
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 13 deletions.
59 changes: 59 additions & 0 deletions src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.xml
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8" ?>
<linker>
<!-- This preserves type forwards for Java stub generation -->
<assembly fullname="System.Runtime">
<type fullname="System.Boolean">
<method name="ToString" />
</type>
<type fullname="System.Byte">
<method name="ToString" />
</type>
<type fullname="System.Char">
<method name="ToString" />
</type>
<type fullname="System.Decimal">
<method name="ToString" />
</type>
<type fullname="System.Double">
<method name="ToString" />
</type>
<type fullname="System.Int16">
<method name="ToString" />
</type>
<type fullname="System.Int32">
<method name="ToString" />
</type>
<type fullname="System.Int64">
<method name="ToString" />
</type>
<type fullname="System.IntPtr">
<method name="ToString" />
</type>
<type fullname="System.Object">
<method name="ToString" />
</type>
<type fullname="System.SByte">
<method name="ToString" />
</type>
<type fullname="System.Single">
<method name="ToString" />
</type>
<type fullname="System.String">
<method name="ToString" />
</type>
<type fullname="System.UInt16">
<method name="ToString" />
</type>
<type fullname="System.UInt32">
<method name="ToString" />
</type>
<type fullname="System.UInt64">
<method name="ToString" />
</type>
<type fullname="System.UIntPtr">
<method name="ToString" />
</type>
<!-- System.Void has no methods -->
<type fullname="System.Void" />
</assembly>
</linker>
Expand Up @@ -20,7 +20,7 @@
"Size": 533257
},
"assemblies/System.Runtime.dll": {
"Size": 2681
"Size": 2832
},
"assemblies/UnnamedProject.dll": {
"Size": 3535
Expand Down
Expand Up @@ -128,7 +128,7 @@
"Size": 1844
},
"assemblies/System.Runtime.dll": {
"Size": 2877
"Size": 3013
},
"assemblies/System.Runtime.InteropServices.RuntimeInformation.dll": {
"Size": 3945
Expand Down
25 changes: 14 additions & 11 deletions tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs
Expand Up @@ -19,35 +19,45 @@ public class MonoAndroidExportTest : DeviceTest {
new object[] {
/* embedAssemblies */ true,
/* fastDevType */ "Assemblies",
/* isRelease */ false,
},
new object[] {
/* embedAssemblies */ false,
/* fastDevType */ "Assemblies",
/* isRelease */ false,
},
new object[] {
/* embedAssemblies */ true,
/* fastDevType */ "Assemblies:Dexes",
/* isRelease */ false,
},
new object[] {
/* embedAssemblies */ false,
/* fastDevType */ "Assemblies:Dexes",
/* isRelease */ false,
},
new object[] {
/* embedAssemblies */ true,
/* fastDevType */ "",
/* isRelease */ true,
},
};
#pragma warning restore 414

[Test]
[TestCaseSource (nameof (MonoAndroidExportTestCases))]
public void MonoAndroidExportReferencedAppStarts (bool embedAssemblies, string fastDevType)
public void MonoAndroidExportReferencedAppStarts (bool embedAssemblies, string fastDevType, bool isRelease)
{
AssertCommercialBuild ();
AssertHasDevices ();
var proj = new XamarinAndroidApplicationProject () {
IsRelease = false,
AndroidFastDeploymentType = fastDevType,
IsRelease = isRelease,
References = {
new BuildItem.Reference ("Mono.Android.Export"),
},
};
if (!string.IsNullOrEmpty (fastDevType))
proj.AndroidFastDeploymentType = fastDevType;
proj.Sources.Add (new BuildItem.Source ("ContainsExportedMethods.cs") {
TextContent = () => @"using System;
using Java.Interop;
Expand Down Expand Up @@ -94,20 +104,13 @@ protected override void OnCreate (Bundle bundle)
}
}
}";
//TODO: x86_64 is a workaround in .NET 6 for: https://github.com/xamarin/monodroid/issues/1136
proj.SetAndroidSupportedAbis ("armeabi-v7a", "x86", "x86_64");
proj.SetAndroidSupportedAbis ("armeabi-v7a", "x86");
proj.SetProperty ("EmbedAssembliesIntoApk", embedAssemblies.ToString ());
proj.SetDefaultTargetDevice ();
using (var b = CreateApkBuilder (Path.Combine ("temp", TestName))) {
string apiLevel;
proj.TargetFrameworkVersion = b.LatestTargetFrameworkVersion (out apiLevel);

// TODO: We aren't sure how to support preview bindings in .NET6 yet.
if (Builder.UseDotNet && apiLevel == "31") {
apiLevel = "30";
proj.TargetFrameworkVersion = "v11.0";
}

proj.AndroidManifest = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<manifest xmlns:android=""http://schemas.android.com/apk/res/android"" android:versionCode=""1"" android:versionName=""1.0"" package=""${proj.PackageName}"">
<uses-sdk android:minSdkVersion=""24"" android:targetSdkVersion=""{apiLevel}"" />
Expand Down

0 comments on commit 8460867

Please sign in to comment.