diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index d86c78d932e..8946d5e7b6d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -359,7 +359,8 @@ protected static bool MonitorAdbLogcat (Func action, string logcat { string ext = Environment.OSVersion.Platform != PlatformID.Unix ? ".exe" : ""; string adb = Path.Combine (AndroidSdkPath, "platform-tools", "adb" + ext); - var info = new ProcessStartInfo (adb, "logcat") { + string adbTarget = Environment.GetEnvironmentVariable ("ADB_TARGET"); + var info = new ProcessStartInfo (adb, $"{adbTarget} logcat") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs new file mode 100644 index 00000000000..8ab50ae951b --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs @@ -0,0 +1,35 @@ +using System; + +using Android.App; +using Android.OS; +using Android.Runtime; +using Android.Util; + +using Java.InteropTests; + +namespace ${ROOT_NAMESPACE}; + +[Register ("${JAVA_PACKAGENAME}.MainActivity"), Activity (Label = "${PROJECT_NAME}", MainLauncher = true)] +public class MainActivity : Activity +{ + const string ResultPrefix = "INTERFACE_COLLECTION_ROOTING_RESULT"; + const string ResultToken = "${RESULT_TOKEN}"; + const string Tag = "InterfaceCollections"; + + protected override void OnCreate (Bundle savedInstanceState) + { + base.OnCreate (savedInstanceState); + + try { + using var holder = new RawInterfaceCollectionHolder (); + using var list = (IDisposable) holder.CreateList (); + using var collection = (IDisposable) holder.CreateCollection (); + using var dictionary = (IDisposable) holder.CreateInterfaceDictionary (); + Log.Info (Tag, $"{ResultPrefix} PASS {ResultToken}"); + } catch (Exception e) { + Log.Error (Tag, $"{ResultPrefix} FAIL {ResultToken}: {e}"); + } finally { + Finish (); + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml new file mode 100644 index 00000000000..336f4cd924f --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/rooting.dgml.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs new file mode 100644 index 00000000000..f94e19bbec4 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionRootingTests.cs @@ -0,0 +1,120 @@ +using System.IO; +using System.Linq; +using System.Xml.Linq; + +using NUnit.Framework; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +public class InterfaceCollectionRootingTests : BaseTest +{ + static readonly XNamespace DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; + + static string GraphPath => Path.Combine ( + XABuildPaths.TopDirectory, "tests", "MSBuildDeviceIntegration", "Resources", "InterfaceCollectionApp", "rooting.dgml.xml"); + + [Test] + public void CanonicalWrapperRootingGraph () + { + var path = TestContext.Parameters.Get ("InterfaceCollectionRootingGraph", GraphPath); + InterfaceCollectionTests.AssertCanonicalWrapperRooting (path); + } + + [TestCase ("list")] + [TestCase ("collection")] + [TestCase ("dictionary")] + public void DirectFactoryRooting (string wrapper) + { + var graph = XDocument.Load (GraphPath); + UseDirectFactoryRoot (graph, wrapper); + AssertGraph (graph); + } + + [TestCase ("dictionary-factory", "conditional factory primary dependency")] + [TestCase ("peer-metadata", "conditional factory metadata dependency")] + public void RejectsMissingConditionalInput (string source, string message) + { + var graph = XDocument.Load (GraphPath); + FindLink (graph, source, "dictionary-conditional").Remove (); + Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains (message)); + } + + [TestCase ("dictionary-factory", "conditional factory primary dependency")] + [TestCase ("peer-metadata", "conditional factory metadata dependency")] + public void RejectsWrongConditionalInput (string source, string message) + { + var graph = XDocument.Load (GraphPath); + FindLink (graph, source, "dictionary-conditional").SetAttributeValue ("Source", "collection-factory"); + Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains (message)); + } + + [TestCase (false)] + [TestCase (true)] + public void RejectsMissingAllocation (bool conditional) + { + var graph = XDocument.Load (GraphPath); + if (!conditional) { + UseDirectFactoryRoot (graph, "dictionary"); + } + FindLink (graph, conditional ? "dictionary-conditional" : "dictionary-factory", "dictionary-type").Remove (); + Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("newobj dependency was not found")); + } + + [TestCase (false)] + [TestCase (true)] + public void RejectsUnexpectedRoot (bool conditional) + { + var graph = XDocument.Load (GraphPath); + if (!conditional) { + UseDirectFactoryRoot (graph, "dictionary"); + } + var unexpectedLink = new XElement (FindLink ( + graph, conditional ? "dictionary-conditional" : "dictionary-factory", "dictionary-type")); + unexpectedLink.SetAttributeValue ("Source", "collection-factory"); + graph.Descendants (DgmlNamespace + "Links").Single ().Add (unexpectedLink); + Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("unexpected incoming dependency")); + } + + [TestCase ("dictionary-factory")] + [TestCase ("dictionary-conditional")] + public void RejectsAmbiguousFactoryNodes (string nodeId) + { + var graph = XDocument.Load (GraphPath); + var duplicate = new XElement (FindNode (graph, nodeId)); + duplicate.SetAttributeValue ("Id", "duplicate"); + graph.Descendants (DgmlNamespace + "Nodes").Single ().Add (duplicate); + Assert.That (() => AssertGraph (graph), Throws.TypeOf ().With.Message.Contains ("ambiguous node matches")); + } + + static void UseDirectFactoryRoot (XDocument graph, string wrapper) + { + FindLink (graph, $"{wrapper}-conditional", $"{wrapper}-type").SetAttributeValue ("Source", $"{wrapper}-factory"); + FindLink (graph, $"{wrapper}-factory", $"{wrapper}-conditional").Remove (); + FindLink (graph, "peer-metadata", $"{wrapper}-conditional").Remove (); + FindNode (graph, $"{wrapper}-conditional").Remove (); + } + + static XElement FindNode (XDocument graph, string id) + { + return graph.Descendants (DgmlNamespace + "Node").Single (node => node.Attribute ("Id")?.Value == id); + } + + static XElement FindLink (XDocument graph, string source, string target) + { + return graph.Descendants (DgmlNamespace + "Link").Single ( + link => link.Attribute ("Source")?.Value == source && link.Attribute ("Target")?.Value == target); + } + + static void AssertGraph (XDocument graph) + { + var path = Path.GetTempFileName (); + try { + graph.Save (path); + InterfaceCollectionTests.AssertCanonicalWrapperRooting (path); + } finally { + File.Delete (path); + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs new file mode 100644 index 00000000000..f609348f10e --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -0,0 +1,500 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; + +using NUnit.Framework; + +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Category ("UsesDevice")] + public class InterfaceCollectionTests : DeviceTest + { + const string DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; + const string ResultPrefix = "INTERFACE_COLLECTION_ROOTING_RESULT"; + + [Test] + public void InterfaceCollectionFactoryRootsCanonicalWrappers () + { + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.NativeAOT, "interfacecollectionrooting")) { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + proj.SetRuntimeIdentifiers ([DeviceAbi]); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("AndroidSdkDirectory", AndroidSdkResolver.GetAndroidSdkPath ()); + var javaSdkDirectory = AndroidSdkResolver.GetJavaSdkPath (); + proj.SetProperty ("JavaSdkDirectory", javaSdkDirectory); + proj.SetProperty ("JavaCPath", Path.Combine (javaSdkDirectory, "bin", "javac")); + proj.SetProperty ("JarPath", Path.Combine (javaSdkDirectory, "bin", "jar")); + proj.SetDefaultTargetDevice (); + var resultToken = Guid.NewGuid ().ToString ("N"); + proj.MainActivity = proj.ProcessSourceTemplate ( + ReadFixture ("MainActivity.cs").Replace ("${RESULT_TOKEN}", resultToken, StringComparison.Ordinal)); + proj.Sources.Add (new BuildItem.Source ("RawInterfaceCollectionHolder.cs") { + TextContent = () => ReadRuntimeFixture (Path.Combine ("Java.Interop", "RawInterfaceCollectionHolder.cs")), + }); + proj.AndroidJavaSources.Add (CreateJavaSource ("ValueProvider.java", bind: true)); + proj.AndroidJavaSources.Add (CreateJavaSource ("ExtendedValueProvider.java", bind: true)); + proj.AndroidJavaSources.Add (CreateJavaSource ("InterfaceCollectionFixture.java", bind: false)); + proj.OtherBuildItems.Add (new AndroidItem.ProguardConfiguration ("proguard.cfg") { + TextContent = () => ReadRuntimeFixture ("InterfaceCollection.proguard.cfg"), + }); + + var testDirectory = Path.Combine ("temp", nameof (InterfaceCollectionFactoryRootsCanonicalWrappers)); + using var builder = CreateApkBuilder (testDirectory); + try { + Assert.IsTrue (builder.Install (proj), "The focused interface-collection app should install."); + AssertGeneratedBindingsAreIsolated (builder, proj); + + ClearAdbLogcat (); + var logcatPath = Path.Combine (Root, builder.ProjectDirectory, "interface-collections-logcat.log"); + string resultLine = ""; + bool resultFound = MonitorAdbLogcat (line => { + if (!line.Contains (ResultPrefix, StringComparison.Ordinal) || + !line.Contains (resultToken, StringComparison.Ordinal)) { + return false; + } + resultLine = line; + return true; + }, logcatPath, ActivityStartTimeoutInSeconds, onMonitoringStarted: () => StartActivityAndAssert (proj)); + Assert.IsTrue (resultFound, $"The focused app did not report a result. See '{logcatPath}'."); + StringAssert.Contains ($"{ResultPrefix} PASS {resultToken}", resultLine); + + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + var dgmlFiles = Directory.GetFiles (projectDirectory, $"{proj.ProjectName}.scan.dgml.xml", SearchOption.AllDirectories); + Assert.AreEqual (1, dgmlFiles.Length, "The focused NativeAOT app should produce one scan dependency graph."); + AssertCanonicalWrapperRooting (dgmlFiles [0]); + TestContext.Out.WriteLine ($"Focused NativeAOT dependency graph: {dgmlFiles [0]}"); + } finally { + RunAdbCommand ($"uninstall {proj.PackageName}"); + } + } + + static AndroidItem.AndroidJavaSource CreateJavaSource (string fileName, bool bind) + { + var path = Path.Combine ("java", "net", "dot", "android", "test", fileName); + return new AndroidItem.AndroidJavaSource (path) { + Encoding = Encoding.ASCII, + TextContent = () => ReadRuntimeFixture (path), + Metadata = { + { "Bind", bind.ToString () }, + }, + }; + } + + void AssertGeneratedBindingsAreIsolated (ProjectBuilder builder, XamarinAndroidApplicationProject proj) + { + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + var generatedSourceDirectory = Path.Combine (projectDirectory, proj.IntermediateOutputPath, "generated", "src"); + FileAssert.Exists (Path.Combine (generatedSourceDirectory, "Net.Dot.Android.Test.IValueProvider.cs")); + FileAssert.Exists (Path.Combine (generatedSourceDirectory, "Net.Dot.Android.Test.IExtendedValueProvider.cs")); + Assert.IsEmpty ( + Directory.GetFiles (generatedSourceDirectory, "*InterfaceCollection*.cs", SearchOption.TopDirectoryOnly), + "The raw JNI holder and concrete peers must not produce managed bindings that can root closed collection wrappers."); + } + + internal static void AssertCanonicalWrapperRooting (string dgmlFile) + { + var chains = new [] { + new RootingChain ( + "JavaList", + "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceListFromJniHandle", + "Mono_Android_Android_Runtime_JavaList_1 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaList_1", + "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1, " + + "Mono_Android_Android_Runtime_JavaList_1___ctor_0)", + "Mono_Android_Android_Runtime_JavaList_1___ctor_0", + "JavaList`1..ctor(native int,JniHandleOwnership)"), + new RootingChain ( + "JavaCollection", + "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceCollectionFromJniHandle", + "Mono_Android_Android_Runtime_JavaCollection_1 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1", + "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1, " + + "Mono_Android_Android_Runtime_JavaCollection_1___ctor)", + "Mono_Android_Android_Runtime_JavaCollection_1___ctor", + "JavaCollection`1..ctor(native int,JniHandleOwnership)"), + new RootingChain ( + "JavaDictionary", + "Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle", + "Mono_Android_Android_Runtime_JavaDictionary_2 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2", + "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2, " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0)", + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0", + "JavaDictionary`2..ctor(native int,JniHandleOwnership)"), + }; + var duplicateNodeIds = new List (); + var missingNodeIds = new List (); + var nodeIds = new HashSet (StringComparer.Ordinal); + var unexpectedCanonicalRoots = new List (); + + using (var reader = CreateDgmlReader (dgmlFile)) { + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element || + reader.LocalName != "Node" || + reader.NamespaceURI != DgmlNamespace) { + continue; + } + var id = reader.GetAttribute ("Id") ?? ""; + var label = reader.GetAttribute ("Label") ?? ""; + if (id.Length == 0) { + missingNodeIds.Add (label); + } else if (!nodeIds.Add (id)) { + duplicateNodeIds.Add ($"Id=\"{id}\" Label=\"{label}\""); + } + foreach (var chain in chains) { + chain.ObserveNode (id, label); + } + if (IsUnexpectedCanonicalReferenceConstructor (label)) { + unexpectedCanonicalRoots.Add (label); + } + } + } + + using (var reader = CreateDgmlReader (dgmlFile)) { + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element || + reader.LocalName != "Link" || + reader.NamespaceURI != DgmlNamespace) { + continue; + } + var source = reader.GetAttribute ("Source") ?? ""; + var target = reader.GetAttribute ("Target") ?? ""; + var reason = reader.GetAttribute ("Reason") ?? ""; + foreach (var chain in chains) { + chain.ObserveLink (source, target, reason); + } + } + } + + Assert.IsEmpty (missingNodeIds, "The NativeAOT dependency graph contained nodes without IDs."); + Assert.IsEmpty (duplicateNodeIds, "The NativeAOT dependency graph contained duplicate node IDs."); + Assert.IsEmpty ( + unexpectedCanonicalRoots, + "Only SafeJavaCollectionFactory's IJavaPeerable instantiations should root the reference-wrapper canonical constructors."); + foreach (var chain in chains) { + chain.AssertComplete (); + TestContext.Out.WriteLine ($"{chain.Name} canonical constructor rooted through SafeJavaCollectionFactory."); + } + } + + static XmlReader CreateDgmlReader (string dgmlFile) + { + return XmlReader.Create (dgmlFile, new XmlReaderSettings { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }); + } + + static bool IsUnexpectedCanonicalReferenceConstructor (string label) + { + if (!label.Contains ("..ctor(native int,JniHandleOwnership) backed by ", StringComparison.Ordinal)) { + return false; + } + bool usesReferenceCanonicalCode = + label.Contains ("JavaList_1___ctor_0", StringComparison.Ordinal) || + label.Contains ("JavaCollection_1___ctor", StringComparison.Ordinal) || + label.Contains ("JavaDictionary_2___ctor_0", StringComparison.Ordinal); + if (!usesReferenceCanonicalCode) { + return false; + } + bool isExpectedRoot = + label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || + label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || + label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + + "..ctor(native int,JniHandleOwnership) backed by " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + + "..ctor(native int,JniHandleOwnership) backed by " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0"; + return !isExpectedRoot; + } + + static string ReadFixture (string fileName) + { + return File.ReadAllText ( + Path.Combine ( + XABuildPaths.TopDirectory, + "tests", + "MSBuildDeviceIntegration", + "Resources", + "InterfaceCollectionApp", + fileName)); + } + + static string ReadRuntimeFixture (string fileName) + { + return File.ReadAllText ( + Path.Combine (XABuildPaths.TopDirectory, "tests", "Mono.Android-Tests", "Mono.Android-Tests", fileName)); + } + + sealed class RootingChain + { + const string ReferenceTypeMetadata = "Type metadata: [Java.Interop]Java.Interop.IJavaPeerable"; + + readonly string constructorPattern; + readonly string canonicalConstructorPattern; + readonly string constructedTypePattern; + readonly string genericDictionaryPattern; + readonly string genericDictionaryDependencyPattern; + readonly string sourcePattern; + readonly string conditionalSourcePattern; + readonly List ambiguousNodeMatches = new (); + readonly HashSet observedNodeRoles = new (StringComparer.Ordinal); + readonly List unexpectedIncomingLinks = new (); + + string canonicalConstructorId = ""; + string constructedTypeId = ""; + string constructorId = ""; + string genericDictionaryId = ""; + string genericDictionaryDependencyId = ""; + string sourceId = ""; + string conditionalSourceId = ""; + string referenceTypeMetadataId = ""; + bool canonicalConstructorToDependency; + bool constructedTypeToGenericDictionary; + bool genericDictionaryToDependency; + bool genericDictionaryToConstructor; + bool sourceToConstructedType; + bool conditionalSourceToConstructedType; + bool sourceToConditionalSource; + bool metadataToConditionalSource; + + public RootingChain ( + string name, + string sourcePattern, + string constructedTypePattern, + string genericDictionaryPattern, + string genericDictionaryDependencyPattern, + string canonicalConstructorPattern, + string constructorPattern) + { + Name = name; + this.sourcePattern = sourcePattern; + conditionalSourcePattern = $"({sourcePattern}, {ReferenceTypeMetadata})"; + this.constructedTypePattern = constructedTypePattern; + this.genericDictionaryPattern = genericDictionaryPattern; + this.genericDictionaryDependencyPattern = genericDictionaryDependencyPattern; + this.canonicalConstructorPattern = canonicalConstructorPattern; + this.constructorPattern = constructorPattern; + } + + public string Name { get; } + + public void ObserveNode (string id, string label) + { + int matchedRoles = 0; + matchedRoles += ObserveNode ( + label == sourcePattern, + id, + label, + "SafeJavaCollectionFactory source", + ref sourceId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == conditionalSourcePattern, + id, + label, + "conditional factory dependency", + ref conditionalSourceId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == ReferenceTypeMetadata, + id, + label, + "IJavaPeerable type metadata", + ref referenceTypeMetadataId) ? 1 : 0; + matchedRoles += ObserveNode ( + IsConstructedTypeLabel (label, constructedTypePattern), + id, + label, + "IJavaPeerable constructed type", + ref constructedTypeId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == genericDictionaryPattern, + id, + label, + "IJavaPeerable generic dictionary", + ref genericDictionaryId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == genericDictionaryDependencyPattern, + id, + label, + "IJavaPeerable constructor dictionary dependency", + ref genericDictionaryDependencyId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == canonicalConstructorPattern, + id, + label, + "canonical compiled constructor", + ref canonicalConstructorId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == $"[Mono.Android]Android.Runtime.{constructorPattern} backed by {canonicalConstructorPattern}", + id, + label, + "IJavaPeerable activation constructor", + ref constructorId) ? 1 : 0; + if (matchedRoles > 1) { + ambiguousNodeMatches.Add ($"multiple roles: Id=\"{id}\" Label=\"{label}\""); + } + } + + public void ObserveLink (string source, string target, string reason) + { + sourceToConstructedType |= IsLink (source, target, reason, sourceId, constructedTypeId, "newobj"); + // ILC may represent the type guard as a conditional dependency instead of a direct newobj edge. + conditionalSourceToConstructedType |= IsLink (source, target, reason, conditionalSourceId, constructedTypeId, "newobj"); + sourceToConditionalSource |= IsLink (source, target, reason, sourceId, conditionalSourceId, "Primary"); + metadataToConditionalSource |= IsLink (source, target, reason, referenceTypeMetadataId, conditionalSourceId, "Secondary"); + constructedTypeToGenericDictionary |= IsLink (source, target, reason, constructedTypeId, genericDictionaryId, "reloc"); + genericDictionaryToDependency |= IsLink ( + source, + target, + reason, + genericDictionaryId, + genericDictionaryDependencyId, + "Primary"); + canonicalConstructorToDependency |= IsLink ( + source, + target, + reason, + canonicalConstructorId, + genericDictionaryDependencyId, + "Secondary"); + genericDictionaryToConstructor |= IsLink ( + source, + target, + reason, + genericDictionaryDependencyId, + constructorId, + "Generic dictionary dependency"); + + RejectUnexpectedIncoming (source, target, reason, constructedTypeId, sourceId, "newobj", conditionalSourceId, "newobj"); + RejectUnexpectedIncoming ( + source, target, reason, conditionalSourceId, sourceId, "Primary", referenceTypeMetadataId, "Secondary"); + RejectUnexpectedIncoming (source, target, reason, genericDictionaryId, constructedTypeId, "reloc"); + RejectUnexpectedIncoming ( + source, target, reason, genericDictionaryDependencyId, genericDictionaryId, "Primary", canonicalConstructorId, "Secondary"); + RejectUnexpectedIncoming ( + source, + target, + reason, + constructorId, + genericDictionaryDependencyId, + "Generic dictionary dependency"); + } + + public void AssertComplete () + { + Assert.IsEmpty (ambiguousNodeMatches, $"{Name} canonical constructor path had ambiguous node matches."); + Assert.IsNotEmpty (sourceId, $"{Name} SafeJavaCollectionFactory source node was not found."); + Assert.IsNotEmpty (constructedTypeId, $"{Name} IJavaPeerable constructed-type node was not found."); + Assert.IsNotEmpty (genericDictionaryId, $"{Name} IJavaPeerable generic dictionary node was not found."); + Assert.IsNotEmpty (genericDictionaryDependencyId, $"{Name} IJavaPeerable constructor dictionary dependency was not found."); + Assert.IsNotEmpty (canonicalConstructorId, $"{Name} canonical compiled constructor node was not found."); + Assert.IsNotEmpty (constructorId, $"{Name} IJavaPeerable activation constructor node was not found."); + Assert.IsTrue ( + sourceToConstructedType || conditionalSourceToConstructedType, + $"{Name} SafeJavaCollectionFactory newobj dependency was not found."); + if (conditionalSourceToConstructedType) { + Assert.IsTrue (sourceToConditionalSource, $"{Name} conditional factory primary dependency was not found."); + Assert.IsTrue (metadataToConditionalSource, $"{Name} conditional factory metadata dependency was not found."); + } + Assert.IsTrue (constructedTypeToGenericDictionary, $"{Name} constructed-type relocation dependency was not found."); + Assert.IsTrue (genericDictionaryToDependency, $"{Name} generic dictionary primary dependency was not found."); + Assert.IsTrue (canonicalConstructorToDependency, $"{Name} canonical constructor secondary dependency was not found."); + Assert.IsTrue (genericDictionaryToConstructor, $"{Name} generic dictionary constructor dependency was not found."); + Assert.IsEmpty (unexpectedIncomingLinks, $"{Name} canonical constructor path had an unexpected incoming dependency."); + } + + bool ObserveNode (bool matches, string id, string label, string role, ref string observedId) + { + if (!matches) { + return false; + } + if (!observedNodeRoles.Add (role)) { + ambiguousNodeMatches.Add ($"{role}: Id=\"{id}\" Label=\"{label}\""); + return true; + } + observedId = id; + return true; + } + + void RejectUnexpectedIncoming ( + string source, + string target, + string reason, + string expectedTarget, + string expectedSource, + string expectedReason, + string alternativeSource = "", + string alternativeReason = "") + { + if (IsIncomingLink (target, expectedTarget) && + !IsLink (source, target, reason, expectedSource, expectedTarget, expectedReason) && + !IsLink (source, target, reason, alternativeSource, expectedTarget, alternativeReason)) { + unexpectedIncomingLinks.Add (FormatLink (source, target, reason)); + } + } + + static bool IsIncomingLink (string actualTarget, string expectedTarget) + { + return expectedTarget.Length > 0 && actualTarget == expectedTarget; + } + + static bool IsLink ( + string actualSource, + string actualTarget, + string actualReason, + string expectedSource, + string expectedTarget, + string expectedReason) + { + return expectedSource.Length > 0 && + expectedTarget.Length > 0 && + actualSource == expectedSource && + actualTarget == expectedTarget && + actualReason == expectedReason; + } + + static string FormatLink (string source, string target, string reason) + { + return $"Source=\"{source}\" Target=\"{target}\" Reason=\"{reason}\""; + } + + static bool IsConstructedTypeLabel (string label, string constructedTypePattern) + { + if (!label.EndsWith (constructedTypePattern, StringComparison.Ordinal)) { + return false; + } + + int prefixLength = label.Length - constructedTypePattern.Length; + if (prefixLength <= "_ZTV".Length || + !label.StartsWith ("_ZTV", StringComparison.Ordinal)) { + return false; + } + for (int i = "_ZTV".Length; i < prefixLength; i++) { + if (label [i] < '0' || label [i] > '9') { + return false; + } + } + return true; + } + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/InterfaceCollection.proguard.cfg b/tests/Mono.Android-Tests/Mono.Android-Tests/InterfaceCollection.proguard.cfg new file mode 100644 index 00000000000..7ad86bae874 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/InterfaceCollection.proguard.cfg @@ -0,0 +1,3 @@ +-keep class net.dot.android.test.InterfaceCollectionBasePeer { *; } +-keep class net.dot.android.test.InterfaceCollectionExtendedPeer { *; } +-keep class net.dot.android.test.InterfaceCollectionHolder { *; } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs new file mode 100644 index 00000000000..b4e8de3a474 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Android.Runtime; + +using Net.Dot.Android.Test; +using NUnit.Framework; + +namespace Java.InteropTests; + +[TestFixture] +[Category ("InterfaceCollections")] +public class InterfaceCollectionMarshallingTests +{ + [Test] + public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var list = holder.CreateList (); + try { + AssertWrapperType (list, typeof (JavaList<>), typeof (IValueProvider)); + Assert.AreEqual (4, list.Count, "list count"); + + var first = list [0]; + var duplicate = list [1]; + var second = list [2]; + + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + Assert.AreSame (first, duplicate, "duplicate list reference"); + Assert.AreSame (first, list [0], "repeated list lookup"); + AssertSameJavaObject (first, duplicate); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (list [3], "null list element"); + Assert.IsTrue (list.Contains (first), "list contains first"); + Assert.IsTrue (list.Contains (null), "list contains null"); + + list.Add (second); + Assert.AreEqual (5, list.Count, "list count after add"); + Assert.AreSame (second, list [4], "added list reference"); + CollectionAssert.AreEqual ( + new [] { 11, 11, 22, 22 }, + list.Where (value => value != null).Select (value => value.Value), + "list enumeration"); + + var roundTrip = holder.RoundTripList (list); + try { + AssertWrapperType (roundTrip, typeof (JavaList<>), typeof (IValueProvider)); + AssertSameJavaObject (list, roundTrip); + Assert.AreSame (first, roundTrip [0], "round-tripped list element"); + } finally { + DisposeIfDistinct (list, roundTrip); + } + + Assert.IsTrue (list.Remove (first), "remove first list reference"); + Assert.IsTrue (list.Contains (first), "list retains duplicate"); + Assert.IsTrue (list.Remove (first), "remove duplicate list reference"); + Assert.IsFalse (list.Contains (first), "list no longer contains first"); + } finally { + DisposeJavaObject (list); + } + } + + [Test] + public void JavaList_InheritedInterfaceUsesExplicitInvoker () + { + using var holder = new RawInterfaceCollectionHolder (); + var list = holder.CreateInheritedList (); + try { + AssertWrapperType (list, typeof (JavaList<>), typeof (IExtendedValueProvider)); + Assert.AreEqual (2, list.Count, "inherited list count"); + AssertExtendedInterfacePeer (list [0], 33, 333); + AssertExtendedInterfacePeer (list [1], 44, 444); + } finally { + DisposeJavaObject (list); + } + } + + [Test] + public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var collection = holder.CreateCollection (); + try { + AssertWrapperType (collection, typeof (JavaCollection<>), typeof (IValueProvider)); + Assert.AreEqual (3, collection.Count, "collection count"); + + var values = new IValueProvider [3]; + collection.CopyTo (values, 0); + var first = values [0]; + var second = values [1]; + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (values [2], "null collection element"); + + collection.Add (first); + Assert.AreEqual (4, collection.Count, "collection count after add"); + Assert.IsTrue (collection.Contains (first), "collection contains first"); + Assert.IsTrue (collection.Contains (null), "collection contains null"); + Assert.AreSame (first, collection.ElementAt (3), "collection enumeration"); + + var roundTrip = holder.RoundTripCollection (collection); + try { + AssertWrapperType (roundTrip, typeof (JavaCollection<>), typeof (IValueProvider)); + AssertSameJavaObject (collection, roundTrip); + Assert.AreSame (first, roundTrip.ElementAt (0), "round-tripped collection element"); + } finally { + DisposeIfDistinct (collection, roundTrip); + } + + collection.Clear (); + Assert.AreEqual (0, collection.Count, "collection count after clear"); + } finally { + DisposeJavaObject (collection); + } + } + + [Test] + public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateKeyDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (string)); + Assert.AreEqual (3, dictionary.Count, "key dictionary count"); + + var first = holder.GetFirst (); + var second = holder.GetSecond (); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + Assert.IsTrue (dictionary.ContainsKey (first), "dictionary contains first key"); + Assert.IsTrue (dictionary.ContainsKey (null), "dictionary contains null key"); + Assert.AreEqual ("first", dictionary [first], "first key value"); + Assert.AreEqual ("null", dictionary [null], "null key value"); + Assert.AreSame (first, holder.GetFirst (), "repeated key lookup"); + AssertKeyDictionaryEnumeration (dictionary, first, second); + + var roundTrip = holder.RoundTripKeyDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (string)); + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreEqual ("second", roundTrip [second], "round-tripped key value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove (first), "remove interface key"); + Assert.IsFalse (dictionary.ContainsKey (first), "removed interface key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + [Test] + public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateValueDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (string), typeof (IValueProvider)); + Assert.AreEqual (4, dictionary.Count, "value dictionary count"); + + var first = dictionary ["first"]; + var duplicate = dictionary ["duplicate"]; + var second = dictionary ["second"]; + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + Assert.AreSame (first, duplicate, "duplicate dictionary value"); + Assert.AreSame (first, dictionary ["first"], "repeated value lookup"); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (dictionary ["null"], "null dictionary value"); + CollectionAssert.AreEqual ( + new IValueProvider [] { first, duplicate, second, null }, + dictionary.Select (entry => entry.Value), + "dictionary value enumeration"); + + dictionary.Add ("added", second); + Assert.AreSame (second, dictionary ["added"], "added dictionary value"); + + var roundTrip = holder.RoundTripValueDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (string), typeof (IValueProvider)); + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreSame (first, roundTrip ["duplicate"], "round-tripped dictionary value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove ("first"), "remove string key"); + Assert.IsFalse (dictionary.ContainsKey ("first"), "removed string key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + [Test] + public void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateInterfaceDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (IValueProvider)); + Assert.AreEqual (3, dictionary.Count, "interface dictionary count"); + + var first = holder.GetFirst (); + var second = holder.GetSecond (); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + Assert.AreSame (second, dictionary [first], "interface dictionary first value"); + Assert.AreSame (first, dictionary [second], "interface dictionary second value"); + Assert.IsNull (dictionary [null], "interface dictionary null value"); + Assert.IsTrue (dictionary.ContainsKey (first), "interface dictionary contains first"); + var entries = dictionary.ToArray (); + Assert.AreEqual (3, entries.Length, "interface dictionary enumeration count"); + Assert.IsTrue ( + entries.Any (entry => ReferenceEquals (entry.Key, first) && ReferenceEquals (entry.Value, second)), + "interface dictionary enumeration"); + Assert.IsTrue (entries.Any (entry => entry.Key == null && entry.Value == null), "null dictionary entry"); + + var roundTrip = holder.RoundTripInterfaceDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (IValueProvider)); + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreSame (second, roundTrip [first], "round-tripped interface dictionary value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove (second), "remove interface dictionary key"); + Assert.IsFalse (dictionary.ContainsKey (second), "removed interface dictionary key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + static void AssertBaseInterfacePeer (IValueProvider peer, int expectedValue) + { + Assert.IsNotNull (peer, "base interface peer"); + Assert.AreEqual (expectedValue, peer.Value, "base interface value"); + Assert.AreEqual (typeof (IValueProviderInvoker), peer.GetType (), "base interface invoker"); + Assert.IsFalse (peer is IExtendedValueProvider, "base peer must not implement the derived interface"); + } + + static void AssertExtendedInterfacePeer (IExtendedValueProvider peer, int expectedValue, int expectedOtherValue) + { + Assert.IsNotNull (peer, "extended interface peer"); + Assert.AreEqual (expectedValue, peer.Value, "extended interface value"); + Assert.AreEqual (expectedOtherValue, peer.OtherValue, "extended interface other value"); + Assert.AreEqual (typeof (IExtendedValueProviderInvoker), peer.GetType (), "extended interface invoker"); + } + + static void AssertWrapperType (object wrapper, Type expectedGenericDefinition, params Type [] expectedArguments) + { + var wrapperType = wrapper.GetType (); + Assert.IsTrue (wrapperType.IsGenericType, "wrapper must be generic"); + Assert.AreEqual (expectedGenericDefinition, wrapperType.GetGenericTypeDefinition (), "wrapper generic definition"); + CollectionAssert.AreEqual (expectedArguments, wrapperType.GenericTypeArguments, "wrapper generic arguments"); + } + + static void AssertKeyDictionaryEnumeration ( + IDictionary dictionary, + IValueProvider first, + IValueProvider second) + { + int count = 0; + bool foundFirst = false; + bool foundSecond = false; + bool foundNull = false; + foreach (var entry in dictionary) { + count++; + if (entry.Key == null) { + Assert.AreEqual ("null", entry.Value, "null key value"); + foundNull = true; + } else if (ReferenceEquals (entry.Key, first)) { + Assert.AreEqual ("first", entry.Value, "first key value"); + foundFirst = true; + } else if (ReferenceEquals (entry.Key, second)) { + Assert.AreEqual ("second", entry.Value, "second key value"); + foundSecond = true; + } else { + Assert.Fail ($"Unexpected dictionary key value '{entry.Key.Value}'."); + } + } + Assert.AreEqual (3, count, "key dictionary entry count"); + Assert.IsTrue (foundFirst, "first key entry"); + Assert.IsTrue (foundSecond, "second key entry"); + Assert.IsTrue (foundNull, "null key entry"); + } + + static void AssertSameJavaObject (object expected, object actual) + { + var expectedPeer = (IJavaObject) expected; + var actualPeer = (IJavaObject) actual; + Assert.IsTrue ( + JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle), + $"expected identical Java peers; expected '{expected.GetType ()}' at '{expectedPeer.Handle}', " + + $"actual '{actual.GetType ()}' at '{actualPeer.Handle}'"); + } + + static void AssertDistinctJavaObjects (object first, object second) + { + var firstPeer = (IJavaObject) first; + var secondPeer = (IJavaObject) second; + Assert.IsFalse ( + JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle), + $"expected distinct Java peers; first '{first.GetType ()}' at '{firstPeer.Handle}', " + + $"second '{second.GetType ()}' at '{secondPeer.Handle}'"); + } + + static void DisposeIfDistinct (object owner, object value) + { + if (!ReferenceEquals (owner, value)) { + DisposeJavaObject (value); + } + } + + static void DisposeJavaObject (object value) + { + if (value is IDisposable disposable) { + disposable.Dispose (); + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/RawInterfaceCollectionHolder.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/RawInterfaceCollectionHolder.cs new file mode 100644 index 00000000000..ed4a42871d1 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/RawInterfaceCollectionHolder.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +using Android.Runtime; + +using Java.Interop; +using Net.Dot.Android.Test; + +namespace Java.InteropTests; + +// Shared with the isolated NativeAOT probe: do not reference closed Java collection wrapper types here. +sealed class RawInterfaceCollectionHolder : IDisposable +{ + const string CollectionSignature = "()Ljava/util/Collection;"; + const string DictionarySignature = "()Ljava/util/Map;"; + const string JniName = "net/dot/android/test/InterfaceCollectionHolder"; + const string ListSignature = "()Ljava/util/List;"; + const string RoundTripCollectionSignature = "(Ljava/util/Collection;)Ljava/util/Collection;"; + const string RoundTripDictionarySignature = "(Ljava/util/Map;)Ljava/util/Map;"; + const string RoundTripListSignature = "(Ljava/util/List;)Ljava/util/List;"; + + readonly Java.Lang.Object holder; + + public RawInterfaceCollectionHolder () + { + var holderClass = JniEnvironment.Types.FindClass (JniName); + try { + var constructor = JNIEnv.GetMethodID (holderClass.Handle, "", "()V"); + var handle = JNIEnv.NewObject (holderClass.Handle, constructor); + holder = new Java.Lang.Object (handle, JniHandleOwnership.TransferLocalRef); + } finally { + JniObjectReference.Dispose (ref holderClass); + } + } + + public IList CreateList () + { + return ConvertJavaValue> (Call ("createList", ListSignature)); + } + + public IList CreateInheritedList () + { + return ConvertJavaValue> (Call ("createInheritedList", ListSignature)); + } + + public ICollection CreateCollection () + { + return ConvertJavaValue> (Call ("createCollection", CollectionSignature)); + } + + public IValueProvider GetFirst () + { + return ConvertJavaValue (Call ("getFirst", "()Lnet/dot/android/test/ValueProvider;")); + } + + public IValueProvider GetSecond () + { + return ConvertJavaValue (Call ("getSecond", "()Lnet/dot/android/test/ValueProvider;")); + } + + public IDictionary CreateKeyDictionary () + { + return ConvertJavaValue> (Call ("createKeyDictionary", DictionarySignature)); + } + + public IDictionary CreateValueDictionary () + { + return ConvertJavaValue> (Call ("createValueDictionary", DictionarySignature)); + } + + public IDictionary CreateInterfaceDictionary () + { + return ConvertJavaValue> (Call ("createInterfaceDictionary", DictionarySignature)); + } + + public IList RoundTripList (IList value) + { + return ConvertJavaValue> (Call ("roundTripList", RoundTripListSignature, value)); + } + + public ICollection RoundTripCollection (ICollection value) + { + return ConvertJavaValue> (Call ("roundTripCollection", RoundTripCollectionSignature, value)); + } + + public IDictionary RoundTripKeyDictionary (IDictionary value) + { + return ConvertJavaValue> ( + Call ("roundTripKeyDictionary", RoundTripDictionarySignature, value)); + } + + public IDictionary RoundTripValueDictionary (IDictionary value) + { + return ConvertJavaValue> ( + Call ("roundTripValueDictionary", RoundTripDictionarySignature, value)); + } + + public IDictionary RoundTripInterfaceDictionary (IDictionary value) + { + return ConvertJavaValue> ( + Call ("roundTripInterfaceDictionary", RoundTripDictionarySignature, value)); + } + + public void Dispose () + { + holder.Dispose (); + } + + IntPtr Call (string methodName, string signature, object value = null) + { + var holderClass = JniEnvironment.Types.GetObjectClass (holder.PeerReference); + try { + var method = JNIEnv.GetMethodID (holderClass.Handle, methodName, signature); + IntPtr handle; + if (value == null) { + handle = JNIEnv.CallObjectMethod (holder.Handle, method); + } else { + var peer = (IJavaObject) value; + handle = JNIEnv.CallObjectMethod (holder.Handle, method, new JValue (peer.Handle)); + GC.KeepAlive (value); + } + return handle; + } finally { + JniObjectReference.Dispose (ref holderClass); + } + } + + [DynamicDependency ("FromJniHandle", "Java.Interop.JavaConvert", "Mono.Android")] + static T ConvertJavaValue (IntPtr handle) + { + var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); + if (javaConvert == null) { + throw new InvalidOperationException ("JavaConvert type was not found."); + } + + var method = javaConvert.GetMethod ( + "FromJniHandle", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: [typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type)], + modifiers: null); + if (method == null) { + throw new InvalidOperationException ("JavaConvert.FromJniHandle method was not found."); + } + + var value = method.Invoke (null, [handle, JniHandleOwnership.TransferLocalRef, typeof (T)]); + if (value == null) { + throw new InvalidOperationException ($"JavaConvert returned null for target type '{typeof (T)}'."); + } + return (T) value; + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index c95fa6b6df9..8c817a19434 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -98,6 +98,7 @@ <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> +