Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] fix detection of "Android libraries" (#…
Browse files Browse the repository at this point in the history
…8904)

Fixes: dotnet/maui#18819

Context: https://github.com/kernshen/NoJavaPeer.git
Context: #4225 (comment)

An assembly's assembly references do not include transitive
dependencies.  Given:

	// Mono.Android.dll
	namespace Java.Lang {
	    public partial class Object {}
	}

	// MauiLib1.dll
	namespace MauiLib1 {
	    public class BaseClass : Java.Lang.Object {}
	}

	// MauiLib2.dll
	namespace MauiLib2 {
	    public class DerivedClass3 : MauiLib1.BaseClass {}
	}

then the assembly references for `MauiLib1.dll` will include
`Mono.Android.dll` (it directly references a type from it),
while the assembly references for `MauiLib2.dll` will include
`MauiLib1.dll` (it directly references a type from it) *but*
`MauiLib2.dll` *will not* have an assembly reference to
`Mono.Android.dll`.

This is how things have worked since .NET Framework 1.0.  This should
not be surprising.

[As part of the .NET for Android][0] [`SignAndroidPackage`][1] target,
Java Callable Wrappers (JCWs) need to be emitted for all
`Java.Lang.Object` subclasses.  This in turn requires
*loading all assemblies* to *find* the `Java.Lang.Object` subclasses.
As a performance optimization, we only load assemblies which we
believed could contain `Java.Lang.Object` subclasses:

 1. Assemblies with `'%(TargetFrameworkIdentifier)' == 'MonoAndroid'`,
    which is "carry over" from how Xamarin.Android did things,
    and works if a .NET for Android project references a
    Xamarin.Android project.

 2. Assemblies with an assembly reference to `Mono.Android.dll`.

Assemblies with transitive dependencies were caught by (1)…
in Xamarin.Android.

With .NET for Android, that is no longer the case:
`%(TargetFrameworkIdentifier)` is now always `.NETCoreApp`.  This in
turn meant that the only assemblies that could be used to generate
JCWs were those which directly referenced `Mono.Android.dll`!

Enter dotnet/maui#18819 and kernshen/NoJavaPeer, which contains
MAUI and .NET for Android solutions with the above transitive reference
structure:

 1. `MauiLib1.dll` / `AndroidLib1.dll` references `Mono.Android.dll`,
    exports `BaseClass`

 2. `MauiLib2.dll` / `AndroidLib2.dll` references `*Lib1.dll` *and not*
    `Mono.Android.dll`; exports `DerivedClass3` which inherits `BaseClass`.

 3. App project attempts to instantiate `DerivedClass3`.

The result: a runtime exception:

	Only System.NotSupportedException Message=Cannot create instance of type 'MauiLib2.DerivedClass3': no Java peer type found.
	   at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 22
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 77
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 139
	   at Java.Lang.Object..ctor() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Java.Lang.Object.cs:line 37
	   at MauiLib1.BaseClass..ctor()
	   at MauiLib2.DerivedClass..ctor() in C:\Project\Maui\MauiApp1\MauiLib2\Platforms\Android\DerivedClass.cs:line 7
	   at MauiApp1.App..ctor(IServiceProvider serviceProvider) in C:\Project\Maui\MauiApp1\MauiApp1\App.xaml.cs:line 18

The exception occurs because there is no JCW for `DerivedClass3`, and
there isn't a JCW for `DerivedClass3` because `MauiLib2.dll` was not
processed at all, because it had no assembly reference to
`Mono.Android.dll`.

As a workaround, update `MauiLib2.dll` to contain an assembly reference
to `Mono.Android.dll`.

*Fix* this scenario by updating
`MonoAndroidHelper.IsMonoAndroidAssembly()` to consider these to be
.NET for Android assemblies:

 1. Assemblies with `%(TargetFrameworkIdentifier)` *containing*
   `Android`.  (This doesn't actually change anything; it's a
    simplification.)

 2. Assemblies with `%(TargetPlatformIdentifier)` *containing*
    `Android`.

    *This* causes `MauiLib2.dll` to be treated as a .NET for Android
    assembly, fixing the bug.

 3. Assemblies with an assembly reference to `Mono.Android.dll`.

The addition of check (2) allows assemblies with only transitive
(non-) references to `Mono.Android.dll` to be properly considered,
allowing JCWs to be emitted for types within them.

Update the `BuildWithLibraryTests.ProjectDependencies()` unit test to
better check for this scenario.

[0]: https://github.com/xamarin/xamarin-android/wiki/Blueprint#after-build
[1]: https://learn.microsoft.com/en-us/dotnet/android/building-apps/build-targets#signandroidpackage
  • Loading branch information
jonathanpeppers committed Apr 26, 2024
1 parent 183b7f7 commit 528934c
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,7 @@ public void ProjectDependencies ([Values(true, false)] bool projectReference)
};
libB.Sources.Clear ();
libB.Sources.Add (new BuildItem.Source ("Foo.cs") {
TextContent = () => @"public class Foo {
public Foo () {
var bar = new Bar();
}
}",
TextContent = () => "public class Foo : Bar { }",
});

var libC = new XamarinAndroidLibraryProject () {
Expand All @@ -283,7 +279,7 @@ public Foo () {
};
libC.Sources.Clear ();
libC.Sources.Add (new BuildItem.Source ("Bar.cs") {
TextContent = () => "public class Bar { }",
TextContent = () => "public class Bar : Java.Lang.Object { }",
});
libC.Sources.Add (new BuildItem ("EmbeddedResource", "Foo.resx") {
TextContent = () => InlineData.ResxWithContents ("<data name=\"CancelButton\"><value>Cancel</value></data>")
Expand All @@ -309,8 +305,8 @@ public Foo () {
ProjectName = "AppA",
IsRelease = true,
Sources = {
new BuildItem.Source ("Bar.cs") {
TextContent = () => "public class Bar : Foo { }",
new BuildItem.Source ("Baz.cs") {
TextContent = () => "public class Baz : Foo { }",
},
new BuildItem ("EmbeddedResource", "Foo.resx") {
TextContent = () => InlineData.ResxWithContents ("<data name=\"CancelButton\"><value>Cancel</value></data>")
Expand All @@ -321,6 +317,10 @@ public Foo () {
}
};
appA.AddReference (libB);
if (!projectReference) {
// @(ProjectReference) implicits adds this reference. For `class Baz : Foo : Bar`:
appA.OtherBuildItems.Add (new BuildItem.Reference ($@"..\{libC.ProjectName}\bin\Release\{libC.TargetFramework}\{libC.ProjectName}.dll"));
}
var appBuilder = CreateApkBuilder (Path.Combine (path, appA.ProjectName));
Assert.IsTrue (appBuilder.Build (appA), $"{appA.ProjectName} should succeed");

Expand All @@ -332,6 +332,17 @@ public Foo () {
helper.AssertContainsEntry ($"assemblies/{libC.ProjectName}.dll");
helper.AssertContainsEntry ($"assemblies/es/{appA.ProjectName}.resources.dll");
helper.AssertContainsEntry ($"assemblies/es/{libC.ProjectName}.resources.dll");

var intermediate = Path.Combine (Root, appBuilder.ProjectDirectory, appA.IntermediateOutputPath);
var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
FileAssert.Exists (dexFile);

// NOTE: the crc hashes here might change one day, but if we used [Android.Runtime.Register("")]
// LibraryB.dll would have a reference to Mono.Android.dll, which invalidates the test.
string className = "Lcrc6414a4b78410c343a2/Bar;";
Assert.IsTrue (DexUtils.ContainsClass (className, dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
className = "Lcrc646d2d82b4d8b39bd8/Foo;";
Assert.IsTrue (DexUtils.ContainsClass (className, dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,13 @@ public static string MapAndroidAbiToClang (string androidAbi)

public static bool IsMonoAndroidAssembly (ITaskItem assembly)
{
// NOTE: look for both MonoAndroid and Android
var tfi = assembly.GetMetadata ("TargetFrameworkIdentifier");
if (string.Compare (tfi, "MonoAndroid", StringComparison.OrdinalIgnoreCase) == 0)
if (tfi.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1)
return true;

var tpi = assembly.GetMetadata ("TargetPlatformIdentifier");
if (tpi.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1)
return true;

var hasReference = assembly.GetMetadata ("HasMonoAndroidReference");
Expand Down

0 comments on commit 528934c

Please sign in to comment.