diff --git a/.editorconfig b/.editorconfig index 10844a4a473..415070be82a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -37,8 +37,8 @@ indent_size = 4 [*.{cs,csx,java,vb,vbx}] insert_final_newline = true indent_style = tab -tab_width = 8 -indent_size = 8 +tab_width = 4 +indent_size = 4 max_line_length = 180 # CMake files diff --git a/.vscode/launch.json b/.vscode/launch.json index 404a2d6f95b..4fe5a1f2748 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -76,6 +76,24 @@ "clearjdb" ] } + , + { + "name": "Run Java.Interop Tests (.NET 9)", + "type": "coreclr", + "request": "launch", + "program": "C:/Program Files/dotnet/dotnet.exe", + "args": [ + "exec", + "C:/Program Files/dotnet/sdk/10.0.100-rc.1.25419.117/vstest.console.dll", + "--framework:.NETCoreApp,Version=v9.0", + "${workspaceFolder}/external/Java.Interop/bin/Test${input:configuration}-net9.0/Java.Base-Tests.dll", + "--logger:Microsoft.TestPlatform.MSBuildLogger;Verbosity=minimal", + "--nologo", + "--artifactsProcessingMode-collect" + ], + "cwd": "${workspaceFolder}/external/Java.Interop", + "console": "integratedTerminal" + } ], "inputs": [ { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b5273d6a0c8..ed7472fac2e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -100,7 +100,7 @@ { "label": "Build Microsoft.Android.Sdk.Analysis Tests", "type": "shell", - "windows": { "command": "dotnet-local.cmd build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}", }, + "windows": { "command": "./dotnet-local.cmd build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}", }, "linux": { "command": "./dotnet-local.sh build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}",}, "osx": { "command": "./dotnet-local.sh build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}",}, "group": { @@ -126,7 +126,7 @@ { "label": "prepare-sample-under-dotnet", "type": "shell", - "windows": { "command": "dotnet-local.cmd build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples", }, + "windows": { "command": "./dotnet-local.cmd build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples", }, "linux": { "command": "./dotnet-local.sh build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples",}, "osx": { "command": "./dotnet-local.sh build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples",}, "group": { @@ -140,7 +140,7 @@ { "label": "build-sample-under-dotnet", "type": "shell", - "windows": { "command": "dotnet-local.cmd build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog", }, + "windows": { "command": "./dotnet-local.cmd build ${workspaceFolder}/${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog", }, "linux": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog",}, "osx": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog",}, "group": { @@ -154,7 +154,7 @@ { "label": "run-sample-under-dotnet", "type": "shell", - "windows": { "command": "dotnet-local.cmd build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog", }, + "windows": { "command": "./dotnet-local.cmd build ${workspaceFolder}/${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog", }, "linux": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog",}, "osx": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog",}, "group": { diff --git a/src/Microsoft.Android.Sdk.ILLink/GenerateTypeMapAttributesStep.cs b/src/Microsoft.Android.Sdk.ILLink/GenerateTypeMapAttributesStep.cs new file mode 100644 index 00000000000..02491ffee2c --- /dev/null +++ b/src/Microsoft.Android.Sdk.ILLink/GenerateTypeMapAttributesStep.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.TypeNameMappings; +using Mono.Cecil; +using Mono.Cecil.Rocks; +using Mono.Linker; +using Mono.Linker.Steps; + +namespace Microsoft.Android.Sdk.ILLink; + +/// +/// Generates TypeMap attributes using the .NET 10 TypeMapAttribute and TypeMapAssociationAttribute +/// Find the best .NET type that maps to each Java type, and create the following code: +/// +/// [assembly: TypeMapAttribute("java/lang/JavaClas", typeof(Java.Lang.JavaClass), typeof(Java.Lang.JavaClass))] +/// [assembly: TypeMapAssociationAttribute(typeof(Java.Lang.JavaClass), typeof(Java.Lang.JavaClassProxy))] +/// +/// [TypeMapProxy("java/lang/JavaClass")] +/// class JavaClassProxy { +/// Target +/// } +/// +/// +public class GenerateTypeMapAttributesStep : BaseStep +{ + const string TypeMapAttributeTypeName = "System.Runtime.InteropServices.TypeMapAttribute`1"; + MethodReference TypeMapAttributeCtor; + + const string TypeMapAssociationAttributeTypeName = "System.Runtime.InteropServices.TypeMapAssociationAttribute`1"; + MethodReference TypeMapAssociationAttributeCtor; + + const string TypeMapProxyAttributeTypeName = "Java.Interop.TypeMapProxyAttribute"; + //TypeReference TypeMapProxyAttribute; + MethodReference TypeMapProxyAttributeCtor; + + const string TypeMapAssemblyTargetAttributeTypeName = "System.Runtime.InteropServices.TypeMapAssemblyTargetAttribute`1"; + MethodReference TypeMapAssemblyTargetAttributeCtor; + + const string JavaTypeMapUniverseTypeName = "Java.Lang.Object"; + TypeReference JavaTypeMapUniverseType { get; set; } + + TypeReference SystemTypeType { get; set; } + TypeReference SystemStringType { get; set; } + + AssemblyDefinition AssemblyToInjectTypeMap { get; set; } + AssemblyDefinition MonoAndroidAssembly { get; set; } + + /// + /// Generates the MethodReference for the given TypeMap attribute constructor, + /// adding the necessary TypeRefs into the given assembly. + /// + void GetTypeMapAttributeReferences ( + string attributeTypeName, + Func ctorSelector, + AssemblyDefinition addReferencesTo, + TypeReference typeMapUniverse, + out MethodReference ctor) + { + var typeMapAttributeDefinition = Context.GetType (attributeTypeName); + var attributeType = addReferencesTo.MainModule.ImportReference (typeMapAttributeDefinition.MakeGenericInstanceType (typeMapUniverse)); + + var typeMapAttributeCtorDefinition = typeMapAttributeDefinition.Methods + .FirstOrDefault (ctorSelector) ?? throw new InvalidOperationException ($"Couldn't find {attributeTypeName}..ctor()"); + var typeMapAttributeCtor = new MethodReference ( + typeMapAttributeCtorDefinition.Name, + typeMapAttributeCtorDefinition.ReturnType, + attributeType) { + HasThis = typeMapAttributeCtorDefinition.HasThis, + ExplicitThis = typeMapAttributeCtorDefinition.ExplicitThis, + CallingConvention = typeMapAttributeCtorDefinition.CallingConvention, + }; + foreach (var param in typeMapAttributeCtorDefinition.Parameters) { + typeMapAttributeCtor.Parameters.Add (new ParameterDefinition ( + param.Name, + param.Attributes, + addReferencesTo.MainModule.ImportReference (param.ParameterType))); + } + ctor = addReferencesTo.MainModule.ImportReference (typeMapAttributeCtor); + } + + protected override void Process () + { + AssemblyToInjectTypeMap = Context.Annotations.GetType ().GetMethod ("GetEntryPointAssembly")?.Invoke (Context.Annotations, null) as AssemblyDefinition ?? throw new NotImplementedException ("asdfasdf NoEntryPoint"); + var javaTypeMapUniverseTypeDefinition = Context.GetType (JavaTypeMapUniverseTypeName); + JavaTypeMapUniverseType = AssemblyToInjectTypeMap.MainModule.ImportReference (javaTypeMapUniverseTypeDefinition); + + GetTypeMapAttributeReferences (TypeMapAttributeTypeName, + m => m.IsConstructor + && m.Parameters is [ + { ParameterType.FullName: "System.String" }, + { ParameterType.FullName: "System.Type" }, + { ParameterType.FullName: "System.Type" }], + AssemblyToInjectTypeMap, + JavaTypeMapUniverseType, + out TypeMapAttributeCtor); + + GetTypeMapAttributeReferences (TypeMapAssociationAttributeTypeName, + m => m.IsConstructor + && m.Parameters is [ + { ParameterType.FullName: "System.Type" }, + { ParameterType.FullName: "System.Type" }], + AssemblyToInjectTypeMap, + JavaTypeMapUniverseType, + out TypeMapAssociationAttributeCtor); + + MonoAndroidAssembly = javaTypeMapUniverseTypeDefinition.Module.Assembly; + GetTypeMapAttributeReferences (TypeMapAssemblyTargetAttributeTypeName, + m => m.IsConstructor + && m.Parameters is [{ ParameterType.FullName: "System.String" }], + MonoAndroidAssembly, + JavaTypeMapUniverseType, + out TypeMapAssemblyTargetAttributeCtor); + + var typeMapProxyAttrTypeDef = Context.GetType (TypeMapProxyAttributeTypeName); + var typeMapProxyAttribute = AssemblyToInjectTypeMap.MainModule.ImportReference (typeMapProxyAttrTypeDef); + var typeMapProxyAttrCtor = typeMapProxyAttrTypeDef.Methods.Single (m => m.IsConstructor); + TypeMapProxyAttributeCtor = AssemblyToInjectTypeMap.MainModule.ImportReference (typeMapProxyAttrCtor); + + SystemTypeType = AssemblyToInjectTypeMap.MainModule.ImportReference (Context.GetType ("System.Type")); + SystemStringType = AssemblyToInjectTypeMap.MainModule.ImportReference (Context.GetType ("System.String")); + } + + + protected override void ProcessAssembly (AssemblyDefinition assembly) + { + foreach (var type in assembly.MainModule.Types) { + ProcessType (assembly, type); + } + } + + // Need to find all possible mappings and pick the best before emitting + Dictionary externalMappings = new (); + // Need to inject the Proxy type, but cannot modify the assembly while iterating through types + Dictionary proxyMappings = new (); + // list of proxy types to inject into AssemblyToInjectInto in EndProcess + List typesToInject = new (); + + /// + /// Selects the best type that would be mapped to in the AndroidValueManager/AndroidTypeManager + /// + private TypeDefinition PickBestTargetType (TypeDefinition existing, TypeDefinition type) + { + if (type == existing) + return existing; + // Types in Mono.Android assembly should be chosen first + if (existing.Module.Assembly.Name.Name != "Mono.Android" && + type.Module.Assembly.Name.Name == "Mono.Android") { + return type; + } + // We found the `Invoker` type *before* the declared type + // Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate. + if ((type.IsAbstract || type.IsInterface) && + !existing.IsAbstract && + !existing.IsInterface && + type.IsAssignableFrom ((TypeReference) existing, Context)) { + return type; + } + // we found a generic subclass of a non-generic type + if (type.IsGenericInstance && + !existing.IsGenericInstance && + type.IsAssignableFrom ((TypeReference) existing, Context)) { + return type; + } + return existing; + } + + /// + /// Iterates through all types to find types that map to/from java types, and stores + /// them for modifying the assemblies during EndProcess + /// + private void ProcessType (AssemblyDefinition assembly, TypeDefinition type) + { + if (type.HasJavaPeer (Context)) { + string javaName = JavaNativeTypeManager.ToJniName (type, Context); + if (externalMappings.TryGetValue (javaName, out var existing)) { + externalMappings [javaName] = PickBestTargetType (existing, type); + } else { + externalMappings.Add (javaName, type); + } + Context.LogMessage (MessageContainer.CreateInfoMessage ($"Type '{type.FullName}' has peer '{javaName}'")); + var proxyType = GenerateTypeMapProxyType (javaName, type); + typesToInject.Add (proxyType); + proxyMappings.Add (type, proxyType); + } else { + Context.LogMessage (MessageContainer.CreateInfoMessage ($"Type '{type.FullName}' has no peer")); + } + + if (!type.HasNestedTypes) + return; + + foreach (TypeDefinition nested in type.NestedTypes) + ProcessType (assembly, nested); + } + + protected override void EndProcess () + { + // HACK ALERT + // We override the entry_assembly so that the TypeMapHandler in illink can have a starting point for TypeMapTargetAssemblies. + // Mono.Android should be the entrypoint assembly so that we can call Assembly.SetEntryAssembly() during application initialization. + // TODO: + // - Add support for "EntryPointAssembly"s that don't have a .entrypoint or Main() method + // - Use MSBuild logic to set the EntryPointAssembly to Mono.Android + Context.Annotations.GetType ().GetField ("entry_assembly", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue (Context.Annotations, MonoAndroidAssembly); + foreach (var type in typesToInject) { + AssemblyToInjectTypeMap.MainModule.Types.Add (type); + Debug.Assert (type.Module.Assembly is not null); + } + foreach (var mapping in externalMappings) { + var attr = GenerateTypeMapAttribute (mapping.Value, mapping.Key); + Context.LogMessage (MessageContainer.CreateInfoMessage ($"Injecting [{attr.AttributeType.FullName}({string.Join (", ", attr.ConstructorArguments.Select (caa => caa.ToString ()))})] into {AssemblyToInjectTypeMap.Name}")); + AssemblyToInjectTypeMap.CustomAttributes.Add (attr); + } + foreach (var mapping in proxyMappings) { + var attr = GenerateTypeMapAssociationAttribute (mapping.Key, mapping.Value); + Context.LogMessage (MessageContainer.CreateInfoMessage ($"Injecting [{attr.AttributeType.FullName}({string.Join (", ", attr.ConstructorArguments.Select (caa => caa.ToString ()))})] into {AssemblyToInjectTypeMap.Name}")); + AssemblyToInjectTypeMap.CustomAttributes.Add (attr); + } + + AssemblyToInjectTypeMap.Write (Context.GetAssemblyLocation (AssemblyToInjectTypeMap) + Random.Shared.GetHexString (4) + ".injected.dll"); + + // JNIEnvInit sets Mono.Android as the entrypoint assembly. Forward the typemap logic to the user/custom assembly; + CustomAttribute targetAssembly = new (TypeMapAssemblyTargetAttributeCtor); + targetAssembly.ConstructorArguments.Add (new (SystemStringType, AssemblyToInjectTypeMap.Name.FullName)); + MonoAndroidAssembly.CustomAttributes.Add (targetAssembly); + MonoAndroidAssembly.Write (Path.Combine ( + Path.GetDirectoryName (Context.GetAssemblyLocation (AssemblyToInjectTypeMap)), + "Mono.Android." + Random.Shared.GetHexString (4) + ".injected.dll")); + } + + /// + /// Generates [TypeMapAssociation(typeof(type), typeof(proxyType))] + /// + CustomAttribute GenerateTypeMapAssociationAttribute (TypeDefinition type, TypeDefinition proxyType) + { + var ca = new CustomAttribute (TypeMapAssociationAttributeCtor); + ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type))); + ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(proxyType))); + return ca; + } + + /// + /// Generates [TypeMap("javaName", typeof(type), typeof(type))] + /// + CustomAttribute GenerateTypeMapAttribute (TypeDefinition type, string javaName) + { + CustomAttribute ca = new (TypeMapAttributeCtor); + ca.ConstructorArguments.Add (new (SystemStringType, javaName)); + ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type))); + ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type))); + return ca; + } + + /// + /// Generates + /// + /// [TypeMapProxy("javaClassName")] + /// sealed class AssemblyName._.mappedTypeFullName_ + /// { + /// } + /// + /// + TypeDefinition GenerateTypeMapProxyType (string javaClassName, TypeDefinition mappedType) + { + StringBuilder mappedName = new (mappedType.Name); + TypeDefinition? declaringType = mappedType; + while (declaringType is not null) { + mappedName.Insert (0, "_"); + mappedName.Insert (0, declaringType.Name); + if (declaringType.DeclaringType is null) + break; + declaringType = declaringType.DeclaringType; + } + var proxyType = new TypeDefinition ( + mappedType.Module.Assembly.Name.Name + "._." + declaringType.Namespace, + mappedName.ToString () + "_", + TypeAttributes.Class | TypeAttributes.NotPublic | TypeAttributes.Sealed, + AssemblyToInjectTypeMap.MainModule.TypeSystem.Object); + + var ca = new CustomAttribute (TypeMapProxyAttributeCtor); + ca.ConstructorArguments.Add (new CustomAttributeArgument (SystemStringType, javaClassName)); + proxyType.CustomAttributes.Add (ca); + return proxyType; + } +} diff --git a/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs b/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs index dc39f615a08..a57cf8efd92 100644 --- a/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs +++ b/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs @@ -9,6 +9,41 @@ namespace MonoDroid.Tuner { + /// + /// Marks managed types and members that participate in Java <-> .NET interop so + /// they are not removed by the linker/trimmer. + /// + /// Behavior overview: + /// - For non-framework assemblies only, proactively preserves types which implement + /// Java interop contracts and members that are invoked from Java but may have no + /// static managed references. + /// - Preserves a user-specified HttpMessageHandler type if provided via + /// LinkContext custom data key "AndroidHttpClientHandlerType", keeping its + /// public parameterless constructor. + /// - Preserves custom views referenced from Android layout XML using the map loaded + /// from "AndroidCustomViewMapFile" (LinkContext custom data), keeping required + /// constructors (Context/IAttributeSet[/Int32]) and JNI constructors + /// (IntPtr) and (IntPtr, JniHandleOwnership). + /// - For types implementing Java.Interop interfaces (IJavaObject/IJavaPeerable), + /// preserves interface invoker types (e.g., IFooInvoker), their constructors and + /// invoked methods discovered via [Register] metadata, and the Java interfaces + /// themselves unless DoNotGenerateAcw/GenerateJavaPeer=false is specified. + /// - For user types which override Java-exposed members, preserves the overridden + /// methods (they are callable from Java but often unreferenced in managed code). + /// - For generated *Implementor types (event implementors), preserves "*Handler" + /// methods referenced from Java callbacks. + /// - Honors attributes implementing Java.Interop.IJniNameProviderAttribute as a + /// preservation signal. Android.Runtime.RegisterAttribute is ignored for this + /// purpose other than reading its properties. + /// + /// Inputs via LinkContext custom data: + /// - "AndroidHttpClientHandlerType": string assembly-qualified type name to keep. + /// - "AndroidCustomViewMapFile": string path to the custom view map produced at build time. + /// + /// This handler registers assembly/type callbacks and marks items via + /// Linker Annotations, effectively seeding the mark graph so MarkStep can + /// retain the necessary interop surface. + /// public class MarkJavaObjects : BaseMarkHandler { Dictionary> module_types = new Dictionary> (); diff --git a/src/Microsoft.Android.Sdk.ILLink/RemoveAttributesBase.cs b/src/Microsoft.Android.Sdk.ILLink/RemoveAttributesBase.cs deleted file mode 100644 index 7f9cc8c40ef..00000000000 --- a/src/Microsoft.Android.Sdk.ILLink/RemoveAttributesBase.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -using Mono.Linker; -using Mono.Linker.Steps; - -using Mono.Tuner; - -using Mono.Cecil; - -namespace Microsoft.Android.Sdk.ILLink -{ - public abstract class RemoveAttributesBase : BaseSubStep { - - public override SubStepTargets Targets { - get { - return SubStepTargets.Assembly - | SubStepTargets.Type - | SubStepTargets.Field - | SubStepTargets.Method - | SubStepTargets.Property - | SubStepTargets.Event; - } - } - - public override bool IsActiveFor (AssemblyDefinition assembly) - { - return Annotations.GetAction (assembly) == AssemblyAction.Link; - } - - public override void ProcessAssembly (AssemblyDefinition assembly) - { - ProcessAttributeProvider (assembly); - ProcessAttributeProvider (assembly.MainModule); - } - - public override void ProcessType (TypeDefinition type) - { - ProcessAttributeProvider (type); - - if (type.HasGenericParameters) - ProcessAttributeProviderCollection (type.GenericParameters); - } - - void ProcessAttributeProviderCollection (IList list) - { - for (int i = 0; i < list.Count; i++) - ProcessAttributeProvider ((ICustomAttributeProvider) list [i]); - } - - public override void ProcessField (FieldDefinition field) - { - ProcessAttributeProvider (field); - } - - public override void ProcessMethod (MethodDefinition method) - { - ProcessMethodAttributeProvider (method); - } - - void ProcessMethodAttributeProvider (MethodDefinition method) - { - ProcessAttributeProvider (method); - ProcessAttributeProvider (method.MethodReturnType); - - if (method.HasParameters) - ProcessAttributeProviderCollection (method.Parameters); - - if (method.HasGenericParameters) - ProcessAttributeProviderCollection (method.GenericParameters); - } - - public override void ProcessProperty (PropertyDefinition property) - { - ProcessAttributeProvider (property); - } - - public override void ProcessEvent (EventDefinition @event) - { - ProcessAttributeProvider (@event); - } - - void ProcessAttributeProvider (ICustomAttributeProvider provider) - { - if (!provider.HasCustomAttributes) - return; - - for (int i = 0; i < provider.CustomAttributes.Count; i++) { - var attrib = provider.CustomAttributes [i]; - if (!IsRemovedAttribute (attrib)) - continue; - - WillRemoveAttribute (provider, attrib); - provider.CustomAttributes.RemoveAt (i--); - } - } - - protected abstract bool IsRemovedAttribute (CustomAttribute attribute); - protected virtual void WillRemoveAttribute (ICustomAttributeProvider provider, CustomAttribute attribute) { } - } -} diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 6ed13d5e73b..6d163110ffd 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -17,24 +17,25 @@ static internal class JNIEnvInit { #pragma warning disable 0649 // NOTE: Keep this in sync with the native side in src/native/common/include/managed-interface.hh - internal struct JnienvInitializeArgs { - public IntPtr javaVm; - public IntPtr env; - public IntPtr grefLoader; - public IntPtr Loader_loadClass; - public IntPtr grefClass; // TODO: remove, not needed anymore - public uint logCategories; - public int version; // TODO: remove, not needed anymore - public int grefGcThreshold; - public IntPtr grefIGCUserPeer; - public byte brokenExceptionTransitions; - public int packageNamingPolicy; - public byte ioExceptionType; - public int jniAddNativeMethodRegistrationAttributePresent; - public bool jniRemappingInUse; - public bool marshalMethodsEnabled; - public IntPtr grefGCUserPeerable; - public bool managedMarshalMethodsLookupEnabled; + internal struct JnienvInitializeArgs + { + public IntPtr javaVm; + public IntPtr env; + public IntPtr grefLoader; + public IntPtr Loader_loadClass; + public IntPtr grefClass; // TODO: remove, not needed anymore + public uint logCategories; + public int version; // TODO: remove, not needed anymore + public int grefGcThreshold; + public IntPtr grefIGCUserPeer; + public byte brokenExceptionTransitions; + public int packageNamingPolicy; + public byte ioExceptionType; + public int jniAddNativeMethodRegistrationAttributePresent; + public bool jniRemappingInUse; + public bool marshalMethodsEnabled; + public IntPtr grefGCUserPeerable; + public bool managedMarshalMethodsLookupEnabled; } #pragma warning restore 0649 @@ -62,8 +63,8 @@ static Type TypeGetType (string typeName) => var type = TypeGetType (typeName); if (type == null) { RuntimeNativeMethods.monodroid_log (LogLevel.Error, - LogCategories.Default, - $"Could not load type '{typeName}'. Skipping JNI registration of type '{Java.Interop.TypeManager.GetClassName (jniClass)}'."); + LogCategories.Default, + $"Could not load type '{typeName}'. Skipping JNI registration of type '{Java.Interop.TypeManager.GetClassName (jniClass)}'."); return; } @@ -105,7 +106,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) IntPtr total_timing_sequence = IntPtr.Zero; IntPtr partial_timing_sequence = IntPtr.Zero; - Logger.SetLogCategories ((LogCategories)args->logCategories); + Logger.SetLogCategories ((LogCategories) args->logCategories); gref_gc_threshold = args->grefGcThreshold; @@ -113,18 +114,22 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) MarshalMethodsEnabled = args->marshalMethodsEnabled; java_class_loader = args->grefLoader; - BoundExceptionType = (BoundExceptionType)args->ioExceptionType; + BoundExceptionType = (BoundExceptionType) args->ioExceptionType; JniRuntime.JniTypeManager typeManager; JniRuntime.JniValueManager valueManager; - if (RuntimeFeature.ManagedTypeMap) { - typeManager = new ManagedTypeManager (); + if (RuntimeFeature.IsCoreClrRuntime) { + typeManager = new TypeMapAttributeTypeManager (args->jniAddNativeMethodRegistrationAttributePresent != 0); + } else if (RuntimeFeature.ManagedTypeMap) { + typeManager = new ManagedTypeManager (); } else { - typeManager = new AndroidTypeManager (args->jniAddNativeMethodRegistrationAttributePresent != 0); + typeManager = new AndroidTypeManager (args->jniAddNativeMethodRegistrationAttributePresent != 0); } if (RuntimeFeature.IsMonoRuntime) { valueManager = new AndroidValueManager (); } else if (RuntimeFeature.IsCoreClrRuntime) { - valueManager = ManagedValueManager.GetOrCreateInstance (); + // Set the entrypoint assembly to Mono.Android.dll for the TypeMapAttributeTypeManager logic + Assembly.SetEntryAssembly (Assembly.GetExecutingAssembly ()); + valueManager = new TypeMapAttributeValueManager (); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } @@ -142,10 +147,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) PropagateExceptions = args->brokenExceptionTransitions == 0; - JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args->packageNamingPolicy; + JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy) args->packageNamingPolicy; if (args->managedMarshalMethodsLookupEnabled) { - delegate* unmanaged getFunctionPointer = &ManagedMarshalMethodsLookupTable.GetFunctionPointer; + delegate* unmanaged getFunctionPointer = &ManagedMarshalMethodsLookupTable.GetFunctionPointer; xamarin_app_init (args->env, getFunctionPointer); } @@ -153,7 +158,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) } [DllImport (RuntimeConstants.InternalDllName, CallingConvention = CallingConvention.Cdecl)] - static extern unsafe void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); + static extern unsafe void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); static void SetSynchronizationContext () => SynchronizationContext.SetSynchronizationContext (Android.App.Application.SynchronizationContext); diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 379558e3265..8f1d29d9bc4 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -216,7 +216,7 @@ static Exception CreateMissingConstructorException (Type type, Type[] ptypes) return new NotSupportedException (message.ToString (), CreateJavaLocationException ()); } - static Exception CreateJavaLocationException () + internal static Exception CreateJavaLocationException () { using (var loc = new Java.Lang.Error ("Java callstack:")) return new JavaLocationException (loc.ToString ()); @@ -299,6 +299,7 @@ static Type monovm_typemap_java_to_managed (string java_type_name) Type? type = null; IntPtr class_ptr = JNIEnv.GetObjectClass (handle); string? class_name = GetClassName (class_ptr); + System.Diagnostics.Debug.Assert (class_name == JNIEnv.GetClassNameFromInstance (handle)); lock (TypeManagerMapDictionaries.AccessLock) { while (class_ptr != IntPtr.Zero) { type = GetJavaToManagedTypeCore (class_name); @@ -492,7 +493,7 @@ public static void RegisterPackages (string[] packages, Converter } [Register ("mono/android/TypeManager", DoNotGenerateAcw = true)] - internal class JavaTypeManager : Java.Lang.Object + internal class _JavaTypeManager : Java.Lang.Object { [Register ("activate", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V", "")] static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) diff --git a/src/Mono.Android/Java.Interop/TypeMapAttributeTypeManager.cs b/src/Mono.Android/Java.Interop/TypeMapAttributeTypeManager.cs new file mode 100644 index 00000000000..ee26be20a7e --- /dev/null +++ b/src/Mono.Android/Java.Interop/TypeMapAttributeTypeManager.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Android.Runtime; +using Java.Interop.Tools.TypeNameMappings; + +namespace Java.Interop +{ + class TypeMapAttributeTypeManager : JniRuntime.JniTypeManager + { + struct JniRemappingReplacementMethod + { + public string target_type; + public string target_name; + public bool is_static; + }; + + bool jniAddNativeMethodRegistrationAttributePresent; + static IReadOnlyDictionary ProxyTypeMap { get; } = TypeMapping.GetOrCreateProxyTypeMapping (); + static IReadOnlyDictionary ExternalTypeMap { get; } = TypeMapping.GetOrCreateExternalTypeMapping (); + + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + + public TypeMapAttributeTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) + { + this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; + } + + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) + { + if (!ExternalTypeMap.TryGetValue (jniSimpleReference, out Type? value)) { + if (Logger.LogAssembly) { + // Miss message is logged in the native runtime + JNIEnv.LogTypemapTrace (new System.Diagnostics.StackTrace (true)); + } + } else { + yield return value; + } + + foreach (var ti in base.GetTypesForSimpleReference (jniSimpleReference)) + yield return ti; + } + + protected override string? GetSimpleReference (Type type) + { + if (!ProxyTypeMap.TryGetValue (type, out Type? proxyType)) + return null; + if (proxyType.GetCustomAttribute () is { } attr) + return GetReplacementTypeCore(attr.JniName) ?? attr.JniName; + return null; + } + + protected override IEnumerable GetSimpleReferences (Type type) + { + if (GetSimpleReference (type) is { } str) + return [str]; + return []; + } + + protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) + { + ReadOnlySpan name = jniSimpleReference; + int slash = name.LastIndexOf ('/'); + var desugarType = new StringBuilder (jniSimpleReference.Length + "Desugar".Length); + if (slash > 0) { + desugarType.Append (name.Slice (0, slash + 1)) + .Append ("Desugar") + .Append (name.Slice (slash + 1)); + } else { + desugarType.Append ("Desugar").Append (name); + } + + var typeWithPrefix = desugarType.ToString (); + var typeWithSuffix = $"{jniSimpleReference}$-CC"; + + var replacements = new []{ + GetReplacementTypeCore (typeWithPrefix) ?? typeWithPrefix, + GetReplacementTypeCore (typeWithSuffix) ?? typeWithSuffix, + }; + + if (Logger.LogAssembly) { + var message = $"Remapping type `{jniSimpleReference}` to one one of {{ `{replacements [0]}`, `{replacements [1]}` }}"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + return replacements; + } + + protected override string? GetReplacementTypeCore (string jniSimpleReference) + { + if (!JNIEnvInit.jniRemappingInUse) { + return null; + } + + IntPtr ret = RuntimeNativeMethods._monodroid_lookup_replacement_type (jniSimpleReference); + if (ret == IntPtr.Zero) { + return null; + } + + return Marshal.PtrToStringAnsi (ret); + } + + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) + { + if (!JNIEnvInit.jniRemappingInUse) { + return null; + } + + IntPtr retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); + if (retInfo == IntPtr.Zero) { + return null; + } + + var method = new JniRemappingReplacementMethod (); + method = Marshal.PtrToStructure (retInfo); + var newSignature = jniMethodSignature; + + int? paramCount = null; + if (method.is_static) { + paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature) + 1; + newSignature = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length); + } + + if (Logger.LogAssembly) { + var message = $"Remapping method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " + + $"`{method.target_type}.{method.target_name}{newSignature}`; " + + $"param-count: {paramCount}; instance-to-static? {method.is_static}"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + return new JniRuntime.ReplacementMethodInfo { + SourceJniType = jniSourceType, + SourceJniMethodName = jniMethodName, + SourceJniMethodSignature = jniMethodSignature, + TargetJniType = method.target_type, + TargetJniMethodName = method.target_name, + TargetJniMethodSignature = newSignature, + TargetJniMethodParameterCount = paramCount, + TargetJniMethodInstanceToStatic = method.is_static, + }; + } + + [return: DynamicallyAccessedMembers (Constructors)] + protected override Type? GetInvokerTypeCore ( + [DynamicallyAccessedMembers (Constructors)] + Type type) + { + if (type.IsInterface || type.IsAbstract) { + return JavaObjectExtensions.GetInvokerType (type) + ?? base.GetInvokerTypeCore (type); + } + + return null; + } + + delegate Delegate GetCallbackHandler (); + + static MethodInfo? dynamic_callback_gen; + + // See ExportAttribute.cs + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] + [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] + static Delegate CreateDynamicCallback (MethodInfo method) + { + if (dynamic_callback_gen == null) { + var assembly = Assembly.Load ("Mono.Android.Export"); + if (assembly == null) + throw new InvalidOperationException ("To use methods marked with ExportAttribute, Mono.Android.Export.dll needs to be referenced in the application"); + var type = assembly.GetType ("Java.Interop.DynamicCallbackCodeGenerator"); + if (type == null) + throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required type was not found."); + dynamic_callback_gen = type.GetMethod ("Create"); + if (dynamic_callback_gen == null) + throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required method was not found."); + } + return (Delegate) dynamic_callback_gen.Invoke (null, new object [] { method })!; + } + + static List sharedRegistrations = new List (); + + static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) + { + if (!MagicRegistrationMap.Filled) + return false; + + bool lockTaken = false; + bool rv = false; + + try { + Monitor.TryEnter (sharedRegistrations, ref lockTaken); + List registrations; + if (lockTaken) { + sharedRegistrations.Clear (); + registrations = sharedRegistrations; + } else { + registrations = new List (); + } + JniNativeMethodRegistrationArguments arguments = new JniNativeMethodRegistrationArguments (registrations, methods.ToString ()); + rv = MagicRegistrationMap.CallRegisterMethod (arguments, type.FullName!); + + if (registrations.Count > 0) + nativeClass.RegisterNativeMethods (registrations.ToArray ()); + } finally { + if (lockTaken) { + Monitor.Exit (sharedRegistrations); + } + } + + return rv; + } + + class MagicRegistrationMap + { +#pragma warning disable CS0649 // Field is never assigned to; + // assigned to in generated IL: https://github.com/xamarin/xamarin-android/blob/cbfa7e20acebd37b52ec4de9d5c1a4a66ddda799/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/MonoDroidMarkStep.cs#L204 + static Dictionary? typesMap; +#pragma warning restore CS0649 + + static void Prefill () + { + // fill code added by the linker + } + + static MagicRegistrationMap () + { + Prefill (); + } + + static public bool Filled { + get { + return typesMap != null && typesMap.Count > 0; + } + } + + internal static bool CallRegisterMethod (JniNativeMethodRegistrationArguments arguments, string typeName) + { + int idx = 0; + + if (typeName == null || !(typesMap?.TryGetValue (typeName, out idx) == true)) + return false; + + return CallRegisterMethodByIndex (arguments, idx); + } + + static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments arguments, int? typeIdx) + { + // updated by the linker to register known types + return false; + } + } + + [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan) instead.")] + public override void RegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + string? methods) => + RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); + + [UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Type.GetType() can never statically know the string value parsed from parameter 'methods'.")] + [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] + public override void RegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, + ReadOnlySpan methods) + { + try { + if (methods.IsEmpty) { + if (jniAddNativeMethodRegistrationAttributePresent) + base.RegisterNativeMembers (nativeClass, type, methods); + return; + } else if (FastRegisterNativeMembers (nativeClass, type, methods)) { + return; + } + + int methodCount = CountMethods (methods); + if (methodCount < 1) { + if (jniAddNativeMethodRegistrationAttributePresent) { + base.RegisterNativeMembers (nativeClass, type, methods); + } + return; + } + + JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; + int nativesIndex = 0; + MethodInfo []? typeMethods = null; + + ReadOnlySpan methodsSpan = methods; + bool needToRegisterNatives = false; + + while (!methodsSpan.IsEmpty) { + int newLineIndex = methodsSpan.IndexOf ('\n'); + + ReadOnlySpan methodLine = methodsSpan.Slice (0, newLineIndex != -1 ? newLineIndex : methodsSpan.Length); + if (!methodLine.IsEmpty) { + SplitMethodLine (methodLine, + out ReadOnlySpan name, + out ReadOnlySpan signature, + out ReadOnlySpan callbackString, + out ReadOnlySpan callbackDeclaringTypeString); + + Delegate? callback = null; + if (callbackString.SequenceEqual ("__export__")) { + var mname = name.Slice (2); + MethodInfo? minfo = null; + typeMethods ??= type.GetMethods (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + foreach (var mi in typeMethods) + if (mname.SequenceEqual (mi.Name) && signature.SequenceEqual (JavaNativeTypeManager.GetJniSignature (mi))) { + minfo = mi; + break; + } + + if (minfo == null) + throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); + callback = CreateDynamicCallback (minfo); + needToRegisterNatives = true; + } else { + Type callbackDeclaringType = type; + if (!callbackDeclaringTypeString.IsEmpty) { + callbackDeclaringType = Type.GetType (callbackDeclaringTypeString.ToString (), throwOnError: true)!; + } + while (callbackDeclaringType.ContainsGenericParameters) { + callbackDeclaringType = callbackDeclaringType.BaseType!; + } + + GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), + callbackDeclaringType, callbackString.ToString ()); + callback = connector (); + } + + if (callback != null) { + needToRegisterNatives = true; + natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); + } + } + + methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; + } + + if (needToRegisterNatives) { + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, nativesIndex); + } + } catch (Exception e) { + JniEnvironment.Runtime.RaisePendingException (e); + } + } + + static int CountMethods (ReadOnlySpan methodsSpan) + { + int count = 0; + while (!methodsSpan.IsEmpty) { + count++; + + int newLineIndex = methodsSpan.IndexOf ('\n'); + methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; + } + return count; + } + + static void SplitMethodLine ( + ReadOnlySpan methodLine, + out ReadOnlySpan name, + out ReadOnlySpan signature, + out ReadOnlySpan callback, + out ReadOnlySpan callbackDeclaringType) + { + int colonIndex = methodLine.IndexOf (':'); + name = methodLine.Slice (0, colonIndex); + methodLine = methodLine.Slice (colonIndex + 1); + + colonIndex = methodLine.IndexOf (':'); + signature = methodLine.Slice (0, colonIndex); + methodLine = methodLine.Slice (colonIndex + 1); + + colonIndex = methodLine.IndexOf (':'); + callback = methodLine.Slice (0, colonIndex != -1 ? colonIndex : methodLine.Length); + + callbackDeclaringType = colonIndex != -1 ? methodLine.Slice (colonIndex + 1) : default; + } + } +} diff --git a/src/Mono.Android/Java.Interop/TypeMapAttributeValueManager.cs b/src/Mono.Android/Java.Interop/TypeMapAttributeValueManager.cs new file mode 100644 index 00000000000..d6a0c56a29f --- /dev/null +++ b/src/Mono.Android/Java.Interop/TypeMapAttributeValueManager.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using Android.Runtime; + +namespace Java.Interop +{ + class TypeMapAttributeValueManager : JniRuntime.JniValueManager + { + Dictionary instances = new Dictionary (); + + public override void WaitForGCBridgeProcessing () + { + if (!AndroidRuntimeInternal.BridgeProcessing) + return; + RuntimeNativeMethods._monodroid_gc_wait_for_bridge_processing (); + } + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type? targetType) + { + if (!reference.IsValid) + return null; + + var peer = CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType) as IJavaPeerable; + JniObjectReference.Dispose (ref reference, options); + return peer; + } + + internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]Type? targetType) + { + Type? type = null; + IntPtr class_ptr = JNIEnv.GetObjectClass (handle); + string? class_name = TypeManager.GetClassName (class_ptr); + System.Diagnostics.Debug.Assert (class_name == JNIEnv.GetClassNameFromInstance (handle)); + lock (TypeManagerMapDictionaries.AccessLock) { + while (class_ptr != IntPtr.Zero) { + if (class_name != null) { + var class_signature = JniTypeSignature.Parse (class_name); + type = JNIEnvInit.androidRuntime!.TypeManager.GetType (class_signature); + if (type != null) { + break; + } + } + + IntPtr super_class_ptr = JNIEnv.GetSuperclass (class_ptr); + JNIEnv.DeleteLocalRef (class_ptr); + class_name = null; + class_ptr = super_class_ptr; + if (class_ptr != IntPtr.Zero) { + class_name = TypeManager.GetClassName (class_ptr); + } + } + } + + if (class_ptr != IntPtr.Zero) { + JNIEnv.DeleteLocalRef (class_ptr); + class_ptr = IntPtr.Zero; + } + + if (targetType != null && + (type == null || + !targetType.IsAssignableFrom (type))) { + type = targetType; + } + + if (type == null) { + class_name = JNIEnv.GetClassNameFromInstance (handle); + JNIEnv.DeleteRef (handle, transfer); + throw new NotSupportedException ( + FormattableString.Invariant ($"Internal error finding wrapper class for '{class_name}'. (Where is the Java.Lang.Object wrapper?!)"), + TypeManager.CreateJavaLocationException ()); + } + + if (type.IsInterface || type.IsAbstract) { + var invokerType = JavaObjectExtensions.GetInvokerType (type); + if (invokerType == null) + throw new NotSupportedException ("Unable to find Invoker for type '" + type.FullName + "'. Was it linked away?", + TypeManager.CreateJavaLocationException ()); + type = invokerType; + } + + var typeSig = JNIEnvInit.androidRuntime?.TypeManager.GetTypeSignature (type) ?? default; + if (!typeSig.IsValid || typeSig.SimpleReference == null) { + throw new ArgumentException ($"Could not determine Java type corresponding to `{type.AssemblyQualifiedName}`.", nameof (targetType)); + } + + JniObjectReference typeClass = default; + JniObjectReference handleClass = default; + try { + try { + typeClass = JniEnvironment.Types.FindClass (typeSig.SimpleReference); + } catch (Exception e) { + throw new ArgumentException ($"Could not find Java class `{typeSig.SimpleReference}`.", + nameof (targetType), + e); + } + + handleClass = JniEnvironment.Types.GetObjectClass (new JniObjectReference (handle)); + if (!JniEnvironment.Types.IsAssignableFrom (handleClass, typeClass)) { + return null; + } + } finally { + JniObjectReference.Dispose (ref handleClass); + JniObjectReference.Dispose (ref typeClass); + } + + IJavaPeerable? result = null; + + try { + result = (IJavaPeerable) TypeManager.CreateProxy (type, handle, transfer); + if (Java.Interop.Runtime.IsGCUserPeer (result.PeerReference.Handle)) { + result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + } + } catch (MissingMethodException e) { + var key_handle = JNIEnv.IdentityHash (handle); + JNIEnv.DeleteRef (handle, transfer); + throw new NotSupportedException (FormattableString.Invariant ( + $"Unable to activate instance of type {type} from native handle 0x{handle:x} (key_handle 0x{key_handle:x})."), e); + } + return result; + } + + public override void AddPeer (IJavaPeerable value) + { + if (value == null) + throw new ArgumentNullException (nameof (value)); + if (!value.PeerReference.IsValid) + throw new ArgumentException ("Must have a valid JNI object reference!", nameof (value)); + + var reference = value.PeerReference; + var hash = JNIEnv.IdentityHash (reference.Handle); + + AddPeer (value, reference, hash); + } + + internal void AddPeer (IJavaPeerable value, JniObjectReference reference, IntPtr hash) + { + lock (instances) { + if (!instances.TryGetValue (hash, out var targets)) { + targets = new IdentityHashTargets (value); + instances.Add (hash, targets); + return; + } + bool found = false; + for (int i = 0; i < targets.Count; ++i) { + IJavaPeerable? target; + var wref = targets [i]; + if (ShouldReplaceMapping (wref!, reference, value, out target)) { + found = true; + targets [i] = IdentityHashTargets.CreateWeakReference (value); + break; + } + if (JniEnvironment.Types.IsSameObject (value.PeerReference, target!.PeerReference)) { + found = true; + if (Logger.LogGlobalRef) { + Logger.Log (LogLevel.Info, "monodroid-gref", FormattableString.Invariant ( + $"warning: not replacing previous registered handle {target.PeerReference} with handle {reference} for key_handle 0x{hash:x}")); + } + } + } + if (!found) { + targets.Add (value); + } + } + } + + internal void AddPeer (IJavaPeerable value, IntPtr handle, JniHandleOwnership transfer, out IntPtr handleField) + { + if (handle == IntPtr.Zero) { + handleField = handle; + return; + } + + var transferType = transfer & (JniHandleOwnership.DoNotTransfer | JniHandleOwnership.TransferLocalRef | JniHandleOwnership.TransferGlobalRef); + switch (transferType) { + case JniHandleOwnership.DoNotTransfer: + handleField = JNIEnv.NewGlobalRef (handle); + break; + case JniHandleOwnership.TransferLocalRef: + handleField = JNIEnv.NewGlobalRef (handle); + JNIEnv.DeleteLocalRef (handle); + break; + case JniHandleOwnership.TransferGlobalRef: + handleField = handle; + break; + default: + throw new ArgumentOutOfRangeException ("transfer", transfer, + "Invalid `transfer` value: " + transfer + " on type " + value.GetType ()); + } + if (handleField == IntPtr.Zero) + throw new InvalidOperationException ("Unable to allocate Global Reference for object '" + value.ToString () + "'!"); + + IntPtr hash = JNIEnv.IdentityHash (handleField); + value.SetJniIdentityHashCode ((int) hash); + if ((transfer & JniHandleOwnership.DoNotRegister) == 0) { + AddPeer (value, new JniObjectReference (handleField, JniObjectReferenceType.Global), hash); + } + + if (Logger.LogGlobalRef) { + RuntimeNativeMethods._monodroid_gref_log ( + FormattableString.Invariant ( + $"handle 0x{handleField:x}; key_handle 0x{hash:x}: Java Type: `{JNIEnv.GetClassNameFromInstance (handleField)}`; MCW type: `{value.GetType ().FullName}`\n")); + } + } + + bool ShouldReplaceMapping (WeakReference current, JniObjectReference reference, IJavaPeerable value, out IJavaPeerable? target) + { + target = null; + + if (current == null) + return true; + + // Target has been GC'd; see also FIXME, above, in finalizer + if (!current.TryGetTarget (out target) || target == null) + return true; + + // It's possible that the instance was GC'd, but the finalizer + // hasn't executed yet, so the `instances` entry is stale. + if (!target.PeerReference.IsValid) + return true; + + if (!JniEnvironment.Types.IsSameObject (target.PeerReference, reference)) + return false; + + // JNIEnv.NewObject/JNIEnv.CreateInstance() compatibility. + // When two MCW's are created for one Java instance [0], + // we want the 2nd MCW to replace the 1st, as the 2nd is + // the one the dev created; the 1st is an implicit intermediary. + // + // Meanwhile, a new "replaceable" instance should *not* replace an + // existing "replaceable" instance; see dotnet/android#9862. + // + // [0]: If Java ctor invokes overridden virtual method, we'll + // transition into managed code w/o a registered instance, and + // thus will create an "intermediary" via + // (IntPtr, JniHandleOwnership) .ctor. + if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) && + !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { + return true; + } + + return false; + } + + public override void RemovePeer (IJavaPeerable value) + { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + var reference = value.PeerReference; + if (!reference.IsValid) { + // Likely an idempotent DIspose(); ignore. + return; + } + var hash = JNIEnv.IdentityHash (reference.Handle); + + RemovePeer (value, hash); + } + + internal void RemovePeer (IJavaPeerable value, IntPtr hash) + { + lock (instances) { + if (!instances.TryGetValue (hash, out var targets)) { + return; + } + for (int i = targets.Count - 1; i >= 0; i--) { + var wref = targets [i]; + if (!wref!.TryGetTarget (out var target)) { + // wref is invalidated; remove it. + targets.RemoveAt (i); + continue; + } + if (!object.ReferenceEquals (target, value)) { + continue; + } + targets.RemoveAt (i); + } + if (targets.Count == 0) { + instances.Remove (hash); + } + } + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + if (!reference.IsValid) + return null; + + var hash = JNIEnv.IdentityHash (reference.Handle); + lock (instances) { + if (instances.TryGetValue (hash, out var targets)) { + for (int i = targets.Count - 1; i >= 0; i--) { + var wref = targets [i]; + if (!wref!.TryGetTarget (out var result) || !result.PeerReference.IsValid) { + targets.RemoveAt (i); + continue; + } + if (!JniEnvironment.Types.IsSameObject (reference, result.PeerReference)) + continue; + return result; + } + } + } + return null; + } + + public override void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object? []? argumentValues) + { + TypeManager.Activate (reference.Handle, cinfo, argumentValues); + } + + protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) + { + var proxy = value as JavaProxyThrowable; + if (proxy != null) { + result = proxy.InnerException; + return true; + } + return base.TryUnboxPeerObject (value, out result); + } + + public override void CollectPeers () + { + GC.Collect (); + } + + public override void FinalizePeer (IJavaPeerable value) + { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (Logger.LogGlobalRef) { + RuntimeNativeMethods._monodroid_gref_log ( + string.Format (CultureInfo.InvariantCulture, + "Finalizing Instance.Type={0} PeerReference={1} IdentityHashCode=0x{2:x} Instance=0x{3:x}", + value.GetType ().ToString (), + value.PeerReference.ToString (), + value.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (value))); + } + + // FIXME: need hash cleanup mechanism. + // Finalization occurs after a test of java persistence. If the + // handle still contains a java reference, we can't finalize the + // object and should "resurrect" it. + if (value.PeerReference.IsValid) { + GC.ReRegisterForFinalize (value); + } else { + RemovePeer (value, (IntPtr) value.JniIdentityHashCode); + value.SetPeerReference (new JniObjectReference ()); + value.Finalized (); + } + } + + public override List GetSurfacedPeers () + { + lock (instances) { + var surfacedPeers = new List (instances.Count); + foreach (var e in instances) { + for (int i = 0; i < e.Value.Count; i++) { + var value = e.Value [i]; + surfacedPeers.Add (new JniSurfacedPeerInfo (e.Key.ToInt32 (), value!)); + } + } + return surfacedPeers; + } + } + } +} diff --git a/src/Mono.Android/Java.Interop/TypeMapProxyAttribute.cs b/src/Mono.Android/Java.Interop/TypeMapProxyAttribute.cs new file mode 100644 index 00000000000..24d87c69e7f --- /dev/null +++ b/src/Mono.Android/Java.Interop/TypeMapProxyAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Java.Interop +{ + public abstract class TypeMapProxyBaseAttribute : Attribute + { + public abstract string JniName { get; } + } + + public abstract class TypeMapProxyAttribute : TypeMapProxyBaseAttribute + { + public TypeMapProxyAttribute (string jniName) + { + if (string.IsNullOrEmpty (jniName)) + throw new ArgumentException ("must not be null or empty", nameof (jniName)); + JniName = jniName; + } + + public override string JniName { get; } + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs index e84a518971b..fb9139257b7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs @@ -525,4 +525,20 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t } return base.TryUnboxPeerObject (value, out result); } + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers ( + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type? targetType) + { + if (!reference.IsValid) + return null; + + var peer = Java.Interop.TypeManager.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType) as IJavaPeerable; + JniObjectReference.Dispose (ref reference, options); + return peer; + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 4924b259ef4..e8b1757a0b7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -16,6 +16,10 @@ static class RuntimeFeature internal static bool ManagedTypeMap { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}", out bool isEnabled) ? isEnabled : ManagedTypeMapEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (TypeMapAttributeTypeMap)}")] + internal static bool TypeMapAttributeTypeMap { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (TypeMapAttributeTypeMap)}", out bool isEnabled) ? isEnabled : ManagedTypeMapEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}")] internal static bool IsMonoRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}", out bool isEnabled) ? isEnabled : IsMonoRuntimeEnabledByDefault; diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 138f11f683e..7129a4967ea 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -104,6 +104,9 @@ + + + @@ -424,26 +427,10 @@ <_SourceFiles Include="$(OutputPath)Java.Interop.*" /> <_SourceFiles Include="$(OutputPath)System.IO.Hashing.dll" /> - - - - + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets index c0c7ff7de09..55a26deac1e 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets @@ -49,6 +49,10 @@ This file contains the .NET 5-specific targets to customize ILLink https://github.com/dotnet/sdk/blob/a5393731b5b7b225692fff121f747fbbc9e8b140/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets#L131 --> + + <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" + Type="Microsoft.Android.Sdk.ILLink.GenerateTypeMapAttributesStep" + BeforeStep="MarkStep" /> <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveSubStepDispatcher" /> <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.MarkJavaObjects" />