diff --git a/external/debugger-libs b/external/debugger-libs index e7fbb713d15..f2572777467 160000 --- a/external/debugger-libs +++ b/external/debugger-libs @@ -1 +1 @@ -Subproject commit e7fbb713d156d11193ed404783ad6fe9c4042a6d +Subproject commit f2572777467b3dc19a2febc3642a87bd737b8bc0 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs new file mode 100644 index 00000000000..ee8af1ffa83 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// JNI primitive type kinds used for mapping JNI signatures → CLR types. +/// +enum JniParamKind +{ +Void, // V +Boolean, // Z → sbyte +Byte, // B → sbyte +Char, // C → char +Short, // S → short +Int, // I → int +Long, // J → long +Float, // F → float +Double, // D → double +Object, // L...; or [ → IntPtr +} + +/// +/// Helpers for parsing JNI method signatures. +/// +static class JniSignatureHelper +{ +/// +/// Parses the JNI parameter type descriptors from a JNI method signature +/// and returns them as records. +/// +public static List ParseParameterTypes (string jniSignature) +{ +var result = new List (); +int i = 1; // skip opening '(' +while (i < jniSignature.Length && jniSignature [i] != ')') { +int start = i; +ParseSingleType (jniSignature, ref i); +result.Add (new JniParameterInfo { JniType = jniSignature.Substring (start, i - start) }); +} +return result; +} + +/// +/// Parses the parameter types from a JNI method signature as values. +/// +public static List ParseParameterKinds (string jniSignature) +{ +var result = new List (); +int i = 1; // skip opening '(' +while (i < jniSignature.Length && jniSignature [i] != ')') { +result.Add (ParseSingleType (jniSignature, ref i)); +} +return result; +} + +/// +/// Extracts the return type descriptor from a JNI method signature. +/// +public static string ParseReturnTypeString (string jniSignature) +{ +int i = jniSignature.IndexOf (')') + 1; +return jniSignature.Substring (i); +} + +/// +/// Parses the return type from a JNI method signature. +/// +public static JniParamKind ParseReturnKind (string jniSignature) +{ +int i = jniSignature.IndexOf (')') + 1; +return ParseSingleType (jniSignature, ref i); +} + +static JniParamKind ParseSingleType (string sig, ref int i) +{ +switch (sig [i]) { +case 'V': i++; return JniParamKind.Void; +case 'Z': i++; return JniParamKind.Boolean; +case 'B': i++; return JniParamKind.Byte; +case 'C': i++; return JniParamKind.Char; +case 'S': i++; return JniParamKind.Short; +case 'I': i++; return JniParamKind.Int; +case 'J': i++; return JniParamKind.Long; +case 'F': i++; return JniParamKind.Float; +case 'D': i++; return JniParamKind.Double; +case 'L': +int end = sig.IndexOf (';', i); +if (end < 0) { +throw new ArgumentException ($"Malformed JNI signature: missing ';' after 'L' at index {i} in '{sig}'"); +} +i = end + 1; +return JniParamKind.Object; +case '[': +i++; +ParseSingleType (sig, ref i); // skip element type +return JniParamKind.Object; +default: +throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); +} +} + +/// +/// Encodes the CLR type for a JNI parameter kind into a signature type encoder. +/// +public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) +{ +switch (kind) { +case JniParamKind.Boolean: encoder.Boolean (); break; +case JniParamKind.Byte: encoder.SByte (); break; +case JniParamKind.Char: encoder.Char (); break; +case JniParamKind.Short: encoder.Int16 (); break; +case JniParamKind.Int: encoder.Int32 (); break; +case JniParamKind.Long: encoder.Int64 (); break; +case JniParamKind.Float: encoder.Single (); break; +case JniParamKind.Double: encoder.Double (); break; +case JniParamKind.Object: encoder.IntPtr (); break; +default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type"); +} +} +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 279d3e15519..ecc97fd0d82 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -136,8 +136,34 @@ sealed class JavaPeerProxyData /// public bool IsGenericDefinition { get; init; } -} + /// + + /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). + + /// + public bool IsAcw { get; init; } + + /// + + /// UCO method wrappers for marshal methods (non-constructor). + + /// + public List UcoMethods { get; } = new (); + /// + + /// UCO constructor wrappers. + + /// + public List UcoConstructors { get; } = new (); + + /// + + /// RegisterNatives registrations (method name, JNI signature, wrapper name). + + /// + public List NativeRegistrations { get; } = new (); +} /// /// A cross-assembly type reference (assembly name + full managed type name). @@ -157,6 +183,92 @@ sealed record TypeRefData public required string AssemblyName { get; init; } } +/// +/// An [UnmanagedCallersOnly] static wrapper for a marshal method. +/// Body: load all args → call n_* callback → ret. +/// +sealed record UcoMethodData +{ + /// + /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0". + /// + public required string WrapperName { get; init; } + + /// + + /// Name of the n_* callback to call, e.g., "n_OnCreate". + + /// + public required string CallbackMethodName { get; init; } + + /// + + /// Type containing the callback method. + + /// + public required TypeRefData CallbackType { get; init; } + + /// + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. + + /// + public required string JniSignature { get; init; } +} + +/// +/// An [UnmanagedCallersOnly] static wrapper for a constructor callback. +/// Signature must match the full JNI native method signature (jnienv + self + ctor params) +/// so the ABI is correct when JNI dispatches the call. +/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). +/// +sealed record UcoConstructorData +{ + /// + /// Name of the generated wrapper, e.g., "nctor_0_uco". + /// + public required string WrapperName { get; init; } + + /// + + /// Target type to pass to ActivateInstance. + + /// + public required TypeRefData TargetType { get; init; } + + /// + + /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration. + + /// + public required string JniSignature { get; init; } +} + +/// +/// One JNI native method registration in RegisterNatives. +/// +sealed record NativeRegistrationData +{ + /// + /// JNI method name to register, e.g., "n_onCreate" or "nctor_0". + /// + public required string JniMethodName { get; init; } + + /// + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + + /// + public required string JniSignature { get; init; } + + /// + + /// Name of the UCO wrapper method whose function pointer to register. + + /// + public required string WrapperMethodName { get; init; } +} + /// /// Describes how the proxy's CreateInstance should construct the managed peer. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index e64d14849a9..19ba83374ce 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -83,6 +83,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri var referencedAssemblies = new SortedSet (StringComparer.Ordinal); foreach (var proxy in model.ProxyTypes) { AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName); + foreach (var uco in proxy.UcoMethods) { + AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName); + } if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } @@ -103,10 +106,11 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; + bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; JavaPeerProxyData? proxy = null; if (hasProxy) { - proxy = BuildProxyType (peer); + proxy = BuildProxyType (peer, isAcw); model.ProxyTypes.Add (proxy); } @@ -178,7 +182,7 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer) + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) { // Use managed type name for proxy naming to guarantee uniqueness across aliases // (two types with the same JNI name will have different managed names). @@ -190,6 +194,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer) ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, + IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, }; @@ -212,9 +217,80 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer) }; } + if (isAcw) { + BuildUcoMethods (peer, proxy); + BuildUcoConstructors (peer, proxy); + BuildNativeRegistrations (proxy); + } + return proxy; } + static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) + { + int ucoIndex = 0; + for (int i = 0; i < peer.MarshalMethods.Count; i++) { + var mm = peer.MarshalMethods [i]; + if (mm.IsConstructor) { + continue; + } + + proxy.UcoMethods.Add (new UcoMethodData { + WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", + CallbackMethodName = mm.NativeCallbackName, + CallbackType = new TypeRefData { + ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, + AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + }); + ucoIndex++; + } + } + + static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) + { + if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) { + return; + } + + foreach (var ctor in peer.JavaConstructors) { + proxy.UcoConstructors.Add (new UcoConstructorData { + WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", + JniSignature = ctor.JniSignature, + TargetType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + }); + } + } + + static void BuildNativeRegistrations (JavaPeerProxyData proxy) + { + foreach (var uco in proxy.UcoMethods) { + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = uco.CallbackMethodName, + JniSignature = uco.JniSignature, + WrapperMethodName = uco.WrapperName, + }); + } + + foreach (var uco in proxy.UcoConstructors) { + string jniName = uco.WrapperName; + int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); + if (ucoSuffix >= 0) { + jniName = jniName.Substring (0, ucoSuffix); + } + + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = jniName, + JniSignature = uco.JniSignature, + WrapperMethodName = uco.WrapperName, + }); + } + } + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, string outputAssemblyName, string jniName) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 13d6952378e..8390630ee1a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -18,8 +18,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// [assembly: TypeMap<Java.Lang.Object>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW) /// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias /// -/// // One proxy type per Java peer that needs activation: -/// public sealed class Activity_Proxy : JavaPeerProxy +/// // One proxy type per Java peer that needs activation or UCO wrappers: +/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only /// { /// public Activity_Proxy() : base() { } /// @@ -34,9 +34,25 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// public override Type TargetType => typeof(Activity); /// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only +/// +/// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only): +/// [UnmanagedCallersOnly] +/// public static void n_OnCreate_uco_0(IntPtr jnienv, IntPtr self, IntPtr p0) +/// => Activity.n_OnCreate(jnienv, self, p0); +/// +/// [UnmanagedCallersOnly] +/// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) +/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// +/// // Registers JNI native methods (ACWs only): +/// public void RegisterNatives(JniType jniType) +/// { +/// TrimmableNativeRegistration.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); +/// TrimmableNativeRegistration.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); +/// } /// } /// -/// // Emitted so the proxy assembly can access internal members in the target assembly: +/// // Emitted so the proxy assembly can access internal n_* callbacks in the target assembly: /// [assembly: IgnoresAccessChecksTo("Mono.Android")] /// /// @@ -51,8 +67,11 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _javaPeerProxyRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _jniTypeRef; + TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; @@ -60,6 +79,10 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _registerMethodRef; + MemberReferenceHandle _ucoAttrCtorRef; + BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; @@ -96,8 +119,11 @@ public void Emit (TypeMapAssemblyData model, string outputPath) EmitTypeReferences (); EmitMemberReferences (); + // Track wrapper method names → handles for RegisterNatives + var wrapperHandles = new Dictionary (); + foreach (var proxy in model.ProxyTypes) { - EmitProxyType (proxy); + EmitProxyType (proxy, wrapperHandles); } foreach (var entry in model.Entries) { @@ -121,10 +147,16 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _iAndroidCallableWrapperRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper")); _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); + _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); + _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -151,6 +183,33 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().String ())); + _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + _registerMethodRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "RegisterMethod", + sig => sig.MethodSignature ().Parameters (4, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_jniTypeRef, false); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().IntPtr (); + })); + + var ucoAttrTypeRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + _pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), + _pe.Metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute")); + _ucoAttrCtorRef = _pe.AddMemberRef (ucoAttrTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) + _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { }); + EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); } @@ -204,10 +263,11 @@ void EmitTypeMapAssociationAttributeCtorRef () })); } - void EmitProxyType (JavaPeerProxyData proxy) + + void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { var metadata = _pe.Metadata; - metadata.AddTypeDefinition ( + var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, metadata.GetOrAddString (proxy.Namespace), metadata.GetOrAddString (proxy.TypeName), @@ -215,6 +275,10 @@ void EmitProxyType (JavaPeerProxyData proxy) MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + if (proxy.IsAcw) { + metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef); + } + // .ctor _pe.EmitBody (".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, @@ -237,6 +301,22 @@ void EmitProxyType (JavaPeerProxyData proxy) EmitTypeGetter ("get_InvokerType", proxy.InvokerType, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); } + + // UCO wrappers + foreach (var uco in proxy.UcoMethods) { + var handle = EmitUcoMethod (uco); + wrapperHandles [uco.WrapperName] = handle; + } + + foreach (var uco in proxy.UcoConstructors) { + var handle = EmitUcoConstructor (uco); + wrapperHandles [uco.WrapperName] = handle; + } + + // RegisterNatives + if (proxy.IsAcw) { + EmitRegisterNatives (proxy.NativeRegistrations, wrapperHandles); + } } void EmitCreateInstance (JavaPeerProxyData proxy) @@ -369,6 +449,106 @@ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes at }); } + MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco) + { + var jniParams = JniSignatureHelper.ParseParameterKinds (uco.JniSignature); + var returnKind = JniSignatureHelper.ParseReturnKind (uco.JniSignature); + int paramCount = 2 + jniParams.Count; + bool isVoid = returnKind == JniParamKind.Void; + + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }); + + var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType); + var callbackRef = _pe.AddMemberRef (callbackTypeHandle, uco.CallbackMethodName, encodeSig); + + var handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + for (int p = 0; p < paramCount; p++) + encoder.LoadArgument (p); + encoder.Call (callbackRef); + encoder.OpCode (ILOpCode.Ret); + }); + + AddUnmanagedCallersOnlyAttribute (handle); + return handle; + } + + MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco) + { + var userTypeRef = _pe.ResolveTypeRef (uco.TargetType); + + // UCO constructor wrappers must match the JNI native method signature exactly. + // The Java JCW declares e.g. "private native void nctor_0(Context p0)" and calls + // it with arguments. JNI dispatches with (JNIEnv*, jobject, ), + // so the wrapper signature must include all parameters to match the ABI. + // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters + // are not forwarded because ActivateInstance creates the managed peer using the + // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. + var jniParams = JniSignatureHelper.ParseParameterKinds (uco.JniSignature); + int paramCount = 2 + jniParams.Count; + + var handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (paramCount, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // self + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }), + encoder => { + encoder.LoadArgument (1); // self + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (userTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_activateInstanceRef); + encoder.OpCode (ILOpCode.Ret); + }); + + AddUnmanagedCallersOnlyAttribute (handle); + return handle; + } + + void EmitRegisterNatives (List registrations, + Dictionary wrapperHandles) + { + _pe.EmitBody ("RegisterNatives", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.NewSlot | MethodAttributes.Final, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniTypeRef, false)), + encoder => { + foreach (var reg in registrations) { + if (!wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { + continue; + } + encoder.LoadArgument (1); + encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniMethodName)); + encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniSignature)); + encoder.OpCode (ILOpCode.Ldftn); + encoder.Token (wrapperHandle); + encoder.Call (_registerMethodRef); + } + encoder.OpCode (ILOpCode.Ret); + }); + } + + void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle) + { + _pe.Metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, _ucoAttrBlobHandle); + } + void EmitTypeMapAttribute (TypeMapAttributeData entry) { var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index df714fdb52d..e76c3810ea7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -13,6 +16,13 @@ sealed record JavaPeerInfo /// public required string JavaName { get; init; } + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public required string CompatJniName { get; init; } + /// /// Full managed type name, e.g., "Android.App.Activity". /// @@ -49,6 +59,20 @@ sealed record JavaPeerInfo /// public bool IsUnconditional { get; init; } + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); + + /// + /// Java constructors to emit in the JCW .java file. + /// Each has a JNI signature and an ordinal index for the nctor_N native method. + /// + public IReadOnlyList JavaConstructors { get; init; } = Array.Empty (); + /// /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. @@ -68,6 +92,127 @@ sealed record JavaPeerInfo public bool IsGenericDefinition { get; init; } } +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed record MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public required string JniName { get; init; } + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public required string JniSignature { get; init; } + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; init; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public required string ManagedMethodName { get; init; } + + /// + /// Full name of the type that declares the managed method (may be a base type). + /// Empty when the declaring type is the same as the peer type. + /// + public string DeclaringTypeName { get; init; } = ""; + + /// + /// Assembly name of the type that declares the managed method. + /// Needed for cross-assembly UCO wrapper generation. + /// Empty when the declaring type is the same as the peer type. + /// + public string DeclaringAssemblyName { get; init; } = ""; + + /// + /// The native callback method name, e.g., "n_onCreate". + /// This is the actual method the UCO wrapper delegates to. + /// + public required string NativeCallbackName { get; init; } + + /// + /// JNI parameter types for UCO generation. + /// + public IReadOnlyList Parameters { get; init; } = Array.Empty (); + + /// + /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". + /// + public required string JniReturnType { get; init; } + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; init; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; init; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; init; } +} + +/// +/// Describes a JNI parameter for UCO method generation. +/// +sealed record JniParameterInfo +{ + /// + /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z". + /// + public required string JniType { get; init; } + + /// + /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32". + /// + public string ManagedType { get; init; } = ""; +} + +/// +/// Describes a Java constructor to emit in the JCW .java source file. +/// +sealed record JavaConstructorInfo +{ + /// + /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". + /// + public required string JniSignature { get; init; } + + /// + /// Ordinal index for the native constructor method (nctor_0, nctor_1, ...). + /// + public required int ConstructorIndex { get; init; } + + /// + /// JNI parameter types parsed from the signature. + /// Used to generate the Java constructor parameter list. + /// + public IReadOnlyList Parameters { get; init; } = Array.Empty (); + + /// + /// For [Export] constructors: super constructor arguments string. + /// Null for [Register] constructors. + /// + public string? SuperArgumentsString { get; init; } +} + /// /// Describes how to call the activation constructor for a Java peer type. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 82b13d1cb3f..52b318a98d2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -163,6 +164,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // 3. Extends a known Java peer → auto-compute JNI name via CRC64 // 4. None of the above → not a Java peer, skip string? jniName = null; + string? compatJniName = null; bool doNotGenerateAcw = false; index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); @@ -170,15 +172,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary results if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; + compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] jniName = attrInfo.JniName; + compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. // If so, auto-compute JNI name from the managed type name via CRC64. if (ExtendsJavaPeer (typeDef, index)) { - jniName = ComputeAutoJniName (typeDef, index); + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); } else { continue; } @@ -193,6 +197,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isUnconditional = attrInfo is not null; string? invokerTypeName = null; + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -203,6 +210,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var peer = new JavaPeerInfo { JavaName = jniName, + CompatJniName = compatJniName, ManagedTypeName = fullName, ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), @@ -211,6 +219,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, + JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -220,6 +230,186 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister is null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature is null && registerInfo.Connector is null) { + return; + } + + bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; + string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}"; + if (isConstructor) { + int ctorIndex = 0; + foreach (var method in methods) { + if (method.IsConstructor) { + ctorIndex++; + } + } + nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}"; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + NativeCallbackName = nativeCallbackName, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), + Parameters = JniSignatureHelper.ParseParameterTypes (registerInfo.Signature ?? "()V"), + IsConstructor = isConstructor, + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, + }); + } + + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + { + exportInfo = null; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = index.ParseRegisterAttribute (ca); + return true; + } + + if (attrName == "ExportAttribute") { + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; + } + } + registerInfo = null; + return false; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return index.ParseRegisterAttribute (ca); + } + } + return null; + } + + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = index.DecodeAttribute (ca); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (string.IsNullOrEmpty (exportName)) { + exportName = index.Reader.GetString (methodDef.Name); + } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + } + return "Ljava/lang/Object;"; + } + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); @@ -426,21 +616,29 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) } /// - /// Compute JNI name for a type without [Register] or component Name. + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. - /// If a declaring type has [Register], its JNI name is used as prefix. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. /// - static string ComputeAutoJniName (TypeDefinition typeDef, AssemblyIndex index) + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); if (parentJniName is not null) { - return $"{parentJniName}_{typeName}"; + var name = $"{parentJniName}_{typeName}"; + return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - return $"{packageName}/{typeName}"; + var jniName = $"{packageName}/{typeName}"; + + string compatName = ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + + return (jniName, compatName); } /// @@ -514,4 +712,23 @@ static string ExtractShortName (string fullName) int lastPlus = typePart.LastIndexOf ('+'); return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } + + static List BuildJavaConstructors (List marshalMethods) + { + var ctors = new List (); + int ctorIndex = 0; + foreach (var mm in marshalMethods) { + if (!mm.IsConstructor) { + continue; + } + ctors.Add (new JavaConstructorInfo { + JniSignature = mm.JniSignature, + ConstructorIndex = ctorIndex, + Parameters = mm.Parameters, + SuperArgumentsString = mm.SuperArgumentsString, + }); + ctorIndex++; + } + return ctors; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 605a8127fba..3b62a8be94b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -40,6 +40,7 @@ protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, s var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart; return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, @@ -57,18 +58,36 @@ protected static JavaPeerInfo MakePeerWithActivation (string jniName, string man } protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) - => MakePeerWithActivation (jniName, managedName, asmName); + { + var peer = MakePeerWithActivation (jniName, managedName, asmName); + peer.DoNotGenerateAcw = false; + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }; + peer.MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + JniReturnType = "V", + ManagedMethodName = ".ctor", + IsConstructor = true, + }, + }; + return peer; + } protected static JavaPeerInfo MakeInterfacePeer ( - string jniName, - string managedName, - string asmName, - string invokerName) + string jniName = "android/view/View$OnClickListener", + string managedName = "Android.Views.View+IOnClickListener", + string asmName = "Mono.Android", + string invokerName = "Android.Views.View+IOnClickListenerInvoker") { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, @@ -77,4 +96,16 @@ protected static JavaPeerInfo MakeInterfacePeer ( InvokerTypeName = invokerName, }; } + + protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig), + ManagedMethodName = jniName, + IsConstructor = isConstructor, + }; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index cceb2e20a62..f8d5ed0f4d5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -127,7 +127,65 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () } - public class IgnoresAccessChecksTo : IDisposable +public class AcwProxy : IDisposable +{ +readonly string _outputDir = CreateTempDir (); +public void Dispose () => DeleteTempDir (_outputDir); + +[Fact] +public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () +{ +var peers = ScanFixtures (); +var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); +var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "AcwTest"); +var (pe, reader) = OpenAssembly (path); +using (pe) { +var proxy = reader.TypeDefinitions +.Select (h => reader.GetTypeDefinition (h)) +.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + +var methods = proxy.GetMethods () +.Select (h => reader.GetMethodDefinition (h)) +.Select (m => reader.GetString (m.Name)) +.ToList (); + +Assert.Contains ("RegisterNatives", methods); +Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0")); +} +} + +[Fact] +public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () +{ +var peers = ScanFixtures (); +var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); +var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "UcoTest"); +var (pe, reader) = OpenAssembly (path); +using (pe) { +var proxy = reader.TypeDefinitions +.Select (h => reader.GetTypeDefinition (h)) +.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + +var ucoMethod = proxy.GetMethods () +.Select (h => reader.GetMethodDefinition (h)) +.First (m => reader.GetString (m.Name).Contains ("_uco_")); + +var attrNames = ucoMethod.GetCustomAttributes () +.Select (h => reader.GetCustomAttribute (h)) +.Select (a => { +var ctorHandle = (MemberReferenceHandle) a.Constructor; +var ctor = reader.GetMemberReference (ctorHandle); +var typeRef = reader.GetTypeReference ((TypeReferenceHandle) ctor.Parent); +return $"{reader.GetString (typeRef.Namespace)}.{reader.GetString (typeRef.Name)}"; +}) +.ToList (); +Assert.Contains ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", attrNames); +} +} + +} + +public class IgnoresAccessChecksTo : IDisposable { readonly string _outputDir = CreateTempDir (); public void Dispose () => DeleteTempDir (_outputDir); @@ -158,6 +216,7 @@ public class Alias : IDisposable static List MakeDuplicateAliasPeers () => new List { new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate1", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate1", @@ -170,6 +229,7 @@ public class Alias : IDisposable }, new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate2", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate2", @@ -228,7 +288,69 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () } - public class CreateInstancePaths : IDisposable +public class JniSignatureHelperTests +{ + +[Theory] +[InlineData ("()V", 0)] +[InlineData ("(I)V", 1)] +[InlineData ("(Landroid/os/Bundle;)V", 1)] +[InlineData ("(IFJ)V", 3)] +[InlineData ("(ZLandroid/view/View;I)Z", 3)] +[InlineData ("([Ljava/lang/String;)V", 1)] +public void ParseParameterKinds_ParsesCorrectCount (string signature, int expectedCount) +{ +var actual = JniSignatureHelper.ParseParameterKinds (signature); +Assert.Equal (expectedCount, actual.Count); +} + +[Theory] +[InlineData ("(Z)V", JniParamKind.Boolean)] +[InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)] +public void ParseParameterKinds_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind) +{ +var types = JniSignatureHelper.ParseParameterKinds (signature); +Assert.Single (types); +Assert.Equal (expectedKind, types [0]); +} + +[Theory] +[InlineData ("()V", JniParamKind.Void)] +[InlineData ("()I", JniParamKind.Int)] +[InlineData ("()Z", JniParamKind.Boolean)] +[InlineData ("()Ljava/lang/String;", JniParamKind.Object)] +public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind) +{ +Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnKind (signature)); +} + +} + +public class NegativeEdgeCase +{ + +[Fact] +public void ParseParameterKinds_EmptyString_ReturnsEmptyList () +{ +Assert.Empty (JniSignatureHelper.ParseParameterKinds ("")); +} + +[Fact] +public void ParseParameterKinds_InvalidSignature_Throws () +{ +Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterKinds ("not-a-sig")); +} + +[Fact] +public void ParseParameterKinds_UnterminatedSignature_ReturnsEmptyList () +{ +Assert.Empty (JniSignatureHelper.ParseParameterKinds ("(")); +} + +} + + +public class CreateInstancePaths : IDisposable { readonly string _outputDir = CreateTempDir (); public void Dispose () => DeleteTempDir (_outputDir); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9e1f167edd4..3f6a3487e29 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -45,6 +45,25 @@ public void Build_ExplicitAssemblyName_OverridesOutputPath () Assert.Equal ("MyAssembly", model.AssemblyName); } + [Fact] + public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + { + var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp"); + ((List) peer.MarshalMethods).Add (new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + IsConstructor = false, + DeclaringTypeName = "Android.App.Activity", + DeclaringAssemblyName = "Mono.Android", + }); + var model = BuildModel (new [] { peer }); + // The UCO callback type references Mono.Android, which is cross-assembly + Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); + // The output assembly itself should not appear + Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo); + } + } public class TypeMapEntries @@ -195,7 +214,7 @@ public void Build_PeerWithActivationCtor_CreatesProxy () [Fact] public void Build_PeerWithInvoker_CreatesProxy () { - var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker"); + var peer = MakeInterfacePeer (); var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); @@ -216,6 +235,257 @@ public void Build_ProxyNaming_ReplacesDotAndPlus () } + public class AcwDetection + { + + [Fact] + public void Build_AcwType_IsAcwTrue () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.True (model.ProxyTypes [0].IsAcw); + } + + [Fact] + public void Build_McwType_IsAcwFalse () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + } + + [Fact] + public void Build_InterfaceWithMarshalMethods_IsNotAcw () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + MarshalMethods = new List { + MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"), + }, + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + // Interface is NOT an ACW even with marshal methods + Assert.False (model.ProxyTypes [0].IsAcw); + } + + [Fact] + public void Build_DoNotGenerateAcw_IsNotAcw () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + peer.DoNotGenerateAcw = true; + peer.MarshalMethods = new List { + MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"), + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + } + + } + + public class UcoMethods + { + + [Fact] + public void Build_AcwWithMarshalMethods_CreatesUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + MakeMarshalMethod ("onResume", "n_OnResume", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Equal (2, proxy.UcoMethods.Count); + Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName); + Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature); + + Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName); + Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName); + } + + [Fact] + public void Build_UcoMethod_CallbackTypeIsDeclaringType () + { + var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"); + mm.DeclaringTypeName = "Java.Lang.Object"; + mm.DeclaringAssemblyName = "Mono.Android"; + + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + mm, + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName); + Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName); + } + + [Fact] + public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onPause", "n_OnPause", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName); + Assert.Equal ("App", uco.CallbackType.AssemblyName); + } + + [Fact] + public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true), + MakeMarshalMethod ("onStart", "n_OnStart", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + // Only 1 UCO method (constructors are skipped from UcoMethods) + Assert.Single (proxy.UcoMethods); + Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName); + } + + } + + public class UcoConstructors + { + + [Fact] + public void Build_AcwWithConstructors_CreatesUcoConstructors () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Single (proxy.UcoConstructors); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + } + + [Fact] + public void Build_PeerWithoutActivationCtor_NoUcoConstructors () + { + // Peer with marshal methods but no activation ctor + var peer = new JavaPeerInfo { + JavaName = "my/app/Foo", + ManagedTypeName = "MyApp.Foo", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "Foo", + AssemblyName = "App", + InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy + MarshalMethods = new List { + MakeMarshalMethod ("bar", "n_Bar", "()V"), + }, + JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }, + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Empty (proxy.UcoConstructors); + } + + } + + public class NativeRegistrations + { + + [Fact] + public void Build_NativeRegistrations_MatchUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + // 1 registration for method + 1 for constructor + Assert.Equal (2, proxy.NativeRegistrations.Count); + + var methodReg = proxy.NativeRegistrations [0]; + Assert.Equal ("n_OnCreate", methodReg.JniMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature); + Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName); + + var ctorReg = proxy.NativeRegistrations [1]; + Assert.Equal ("nctor_0", ctorReg.JniMethodName); + Assert.Equal ("()V", ctorReg.JniSignature); + Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName); + } + + [Fact] + public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature () + { + var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App"); + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V", + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + } + }, + }; + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); + + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + } + + [Fact] + public void Build_NonAcwProxy_NoNativeRegistrations () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.Empty (model.ProxyTypes [0].NativeRegistrations); + } + + } + public class FixtureScan { @@ -245,6 +515,20 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string Assert.Equal (expectedShortName, peer.ManagedTypeShortName); } + [Fact] + public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () + { + var peers = ScanFixtures (); + var model = BuildModel (peers); + + var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); + Assert.NotEmpty (acwProxies); + + foreach (var proxy in acwProxies) { + Assert.NotEmpty (proxy.NativeRegistrations); + } + } + } public class FixtureConditionalAttributes @@ -302,6 +586,11 @@ public void Fixture_McwType_HasActivation_CreatesProxy (string javaName, string Assert.NotNull (proxy); Assert.True (proxy!.HasActivation); Assert.Equal (expectedManagedName, proxy.TargetType.ManagedTypeName); + // MCW types with DoNotGenerateAcw → not ACW + Assert.False (proxy.IsAcw); + Assert.Empty (proxy.UcoMethods); + Assert.Empty (proxy.UcoConstructors); + Assert.Empty (proxy.NativeRegistrations); } [Fact] @@ -331,19 +620,113 @@ public void Fixture_Service_NoActivation_NoProxy () } } + public class FixtureAcwTypes + { + + [Fact] + public void Fixture_MainActivity_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.NotEmpty (peer.MarshalMethods); + Assert.NotNull (peer.ActivationCtor); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.True (proxy.HasActivation); + } + + [Fact] + public void Fixture_MainActivity_UcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; + + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); + + var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate"); + Assert.NotNull (onCreateUco); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature); + Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName); + } + + } + + public class FixtureTouchHandler + { + + [Fact] + public void Fixture_TouchHandler_AllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy"); + Assert.NotNull (proxy); + + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count); + + // onTouch: (Landroid/view/View;I)Z + var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch"); + Assert.NotNull (onTouchUco); + Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature); + + // onFocusChange: (Landroid/view/View;Z)V + var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange"); + Assert.NotNull (onFocusUco); + Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature); + + // onScroll: (IFJD)V + var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll"); + Assert.NotNull (onScrollUco); + Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature); + + // getText: ()Ljava/lang/String; + var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText"); + Assert.NotNull (getTextUco); + Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature); + + // setItems: ([Ljava/lang/String;)V + var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems"); + Assert.NotNull (setItemsUco); + Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature); + } + } public class FixtureCustomView { [Fact] - public void Fixture_CustomView_HasTwoConstructors () + public void Fixture_CustomView_HasTwoConstructorWrappers () { var peer = FindFixtureByJavaName ("my/app/CustomView"); var model = BuildModel (new [] { peer }, "TypeMap"); var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); Assert.NotNull (proxy); + + if (proxy!.IsAcw) { + Assert.Equal (2, proxy.UcoConstructors.Count); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); + + // Constructor JNI signatures should be propagated + Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature); + + // Constructor registrations must use the actual JNI signatures + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + } } } @@ -476,9 +859,39 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName); Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + } + } + + [Fact] + public void Fixture_ClickableView_HasOnClickUcoWrapper () + { + var peer = FindFixtureByJavaName ("my/app/ClickableView"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); + Assert.NotNull (proxy); + var onClick = proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + } + } + + [Fact] + public void Fixture_MultiInterfaceView_HasAllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); + Assert.NotNull (proxy); + Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); + Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); } } @@ -589,6 +1002,31 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () }); } + [Fact] + public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "UcoAttrTest"); + + EmitAndVerify (model, "UcoAttrTest", (pe, reader) => { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); + + var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList (); + Assert.NotEmpty (ucoMethods); + + foreach (var uco in ucoMethods) { + var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); + Assert.NotEmpty (attrs); + } + }); + } + [Fact] public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () { @@ -607,6 +1045,47 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () Assert.Contains (".ctor", methodNames); Assert.Contains ("CreateInstance", methodNames); Assert.Contains ("get_TargetType", methodNames); + + if (model.ProxyTypes [0].IsAcw) { + Assert.Contains ("RegisterNatives", methodNames); + Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + } + }); + } + + [Fact] + public void FullPipeline_CustomView_UcoConstructorMatchesJniSignature () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorSigTest"); + + EmitAndVerify (model, "CtorSigTest", (pe, reader) => { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + var ucoCtors = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco")) + .ToList (); + + Assert.NotEmpty (ucoCtors); + + foreach (var uco in ucoCtors) { + var name = reader.GetString (uco.Name); + var modelUco = model.ProxyTypes + .SelectMany (p => p.UcoConstructors) + .First (u => u.WrapperName == name); + + // UCO constructor signature: jnienv + self + JNI params + int expectedJniParams = JniSignatureHelper.ParseParameterKinds (modelUco.JniSignature).Count; + int expectedTotal = 2 + expectedJniParams; + + var sig = reader.GetBlobReader (uco.Signature); + var header = sig.ReadSignatureHeader (); + int paramCount = sig.ReadCompressedInteger (); + Assert.Equal (expectedTotal, paramCount); + } }); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 7a654f4693d..81aabe9ad74 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -1,9 +1,61 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public partial class JavaPeerScannerTests { + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var peers = ScanFixtures (); + var method = FindByJavaName (peers, javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var peers = ScanFixtures (); + + var ctors = FindByJavaName (peers, "my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindByJavaName (peers, "android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] @@ -44,6 +96,24 @@ public void Scan_MultipleInterfaces_AllResolved () Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); } + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + var peers = ScanFixtures (); + Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 4c7fa70aee1..ec368ed6a44 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -41,6 +42,8 @@ public void Scan_EmptyNamespace_Handled () { var peers = ScanFixtures (); Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); } [Theory] @@ -54,4 +57,13 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); } + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var peers = ScanFixtures (); + var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } }