diff --git a/external/Java.Interop b/external/Java.Interop index 5d55b251071..b8f2c2b64a1 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 5d55b2510711f76a2fece20e2d07952313daed5b +Subproject commit b8f2c2b64a1299ddd72bc040502647dd8b2f2710 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/external/xamarin-android-tools b/external/xamarin-android-tools index ebd3aaf34b6..604940c3c74 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit ebd3aaf34b6650b0d0b763f824d5ba3f2d6802e3 +Subproject commit 604940c3c74ba6af59ec06733de68d5cae306189 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index d07bb062bd3..0e2b867c4f9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,15 +1,49 @@ 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 parameter types from a JNI method signature like "(Landroid/os/Bundle;)V". + /// + public static List ParseParameterTypes (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; + } + + /// + /// Parses the raw JNI type descriptor strings from a JNI method signature. + /// public static List ParseParameterTypeStrings (string jniSignature) { @@ -17,14 +51,16 @@ public static List ParseParameterTypeStrings (string jniSignature) int i = 1; // skip opening '(' while (i < jniSignature.Length && jniSignature [i] != ')') { int start = i; - SkipSingleType (jniSignature, ref i); + ParseSingleType (jniSignature, ref i); result.Add (jniSignature.Substring (start, i - start)); } return result; } /// + /// Extracts the return type descriptor from a JNI method signature. + /// public static string ParseReturnTypeString (string jniSignature) { @@ -32,22 +68,59 @@ public static string ParseReturnTypeString (string jniSignature) return jniSignature.Substring (i); } - static void SkipSingleType (string sig, ref int i) + /// + + /// Parses the return type from a JNI method signature. + + /// + public static JniParamKind ParseReturnType (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': case 'Z': case 'B': case 'C': case 'S': - case 'I': case 'J': case 'F': case 'D': - i++; - break; + 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': i = sig.IndexOf (';', i) + 1; - break; + return JniParamKind.Object; case '[': i++; - SkipSingleType (sig, ref i); - break; + 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 f96d3448647..ce34dfbb4fa 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) @@ -348,6 +428,106 @@ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes at }); } + MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + var returnKind = JniSignatureHelper.ParseReturnType (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.ParseParameterTypes (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 2de7a49ead9..e76c3810ea7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -67,6 +67,12 @@ sealed record JavaPeerInfo /// 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. @@ -179,6 +185,34 @@ sealed record JniParameterInfo 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 28941747b30..ca9057ed747 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -220,6 +220,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, MarshalMethods = marshalMethods, + JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -708,4 +709,23 @@ static List ParseJniParameters (string jniSignature) } return result; } + + 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 bb446ff3029..4a168b636a9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -67,6 +67,9 @@ protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, s { 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 = "", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index cceb2e20a62..b2282243273 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); @@ -228,7 +286,68 @@ 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 ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) +{ +var actual = JniSignatureHelper.ParseParameterTypes (signature); +Assert.Equal (expectedCount, actual.Count); +} + +[Theory] +[InlineData ("(Z)V", JniParamKind.Boolean)] +[InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)] +public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind) +{ +var types = JniSignatureHelper.ParseParameterTypes (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.ParseReturnType (signature)); +} + +} + +public class NegativeEdgeCase +{ + +[Fact] +public void ParseParameterTypes_EmptyString_ReturnsEmptyList () +{ +Assert.Empty (JniSignatureHelper.ParseParameterTypes ("")); +} + +[Fact] +public void ParseParameterTypes_InvalidSignature_Throws () +{ +Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterTypes ("not-a-sig")); +} + +[Fact] +public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList () +{ +Assert.Empty (JniSignatureHelper.ParseParameterTypes ("(")); +} + +} + +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 54d66fc03f7..0b9ae5e7fdd 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 @@ -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); + } } } @@ -479,6 +862,36 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) 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.ParseParameterTypes (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); + } }); }