diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs new file mode 100644 index 00000000000..1fe321713eb --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs @@ -0,0 +1,615 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +sealed class ExportMethodDispatchEmitter +{ + readonly PEAssemblyBuilder _pe; + readonly ExportMethodDispatchEmitterContext _context; + + public ExportMethodDispatchEmitter (PEAssemblyBuilder pe, ExportMethodDispatchEmitterContext context) + { + _pe = pe ?? throw new ArgumentNullException (nameof (pe)); + _context = context ?? throw new ArgumentNullException (nameof (context)); + } + + public MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco) + { + var exportMethodDispatch = GetRequiredExportMethodDispatch (uco); + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature); + int paramCount = 2 + jniParams.Count; + bool isVoid = returnKind == JniParamKind.Void; + var exportMethodDispatchLocals = CreateExportMethodDispatchLocals (exportMethodDispatch, isVoid, returnKind); + + // UCO wrapper signature: uses JNI ABI types (byte for boolean) + 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]); + } + }); + + // Callback member reference: uses MCW n_* types (sbyte for boolean) + Action encodeCallbackSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + for (int j = 0; j < jniParams.Count; j++) { + JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]); + } + }); + + var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType); + var callbackRef = AddExportMethodDispatchRef (uco, callbackTypeHandle); + + // Wrap the dispatch in the standard BeginMarshalMethod/try/catch/finally pattern so + // managed exceptions thrown from the [Export] body are routed through + // JniRuntime.OnUserUnhandledException — matching the legacy LLVM-IR contract + // (Mono.Android.Export/CallbackCode.cs) and the trimmable UCO ctor wrapper. + var handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => { + EmitWrappedExportMethodDispatch (encoder, cfb, uco, callbackTypeHandle, callbackRef, + jniParams, returnKind, exportMethodDispatchLocals); + }, + exportMethodDispatchLocals.EncodeLocals); + + AddUnmanagedCallersOnlyAttribute (handle); + return handle; + } + + void EmitWrappedExportMethodDispatch (InstructionEncoder encoder, ControlFlowBuilder cfb, + UcoMethodData uco, EntityHandle callbackTypeHandle, MemberReferenceHandle callbackRef, + List jniParams, JniParamKind returnKind, ExportMethodDispatchLocals locals) + { + bool isVoid = returnKind == JniParamKind.Void; + var tryStart = encoder.DefineLabel (); + var catchStart = encoder.DefineLabel (); + var finallyStart = encoder.DefineLabel (); + var afterAll = encoder.DefineLabel (); + var endCatch = encoder.DefineLabel (); + + // Preamble: if (!BeginMarshalMethod(jnienv, out envp, out runtime)) goto afterAll; + // On the false path, the ABI return local is zero-initialized (InitLocals=true) so + // it returns the appropriate default (0 / IntPtr.Zero) for the JNI return kind. + encoder.LoadArgument (0); + encoder.LoadLocalAddress (0); + encoder.LoadLocalAddress (1); + encoder.Call (_context.BeginMarshalMethodRef); + encoder.Branch (ILOpCode.Brfalse, afterAll); + + // TRY: dispatch + (if non-void) store ABI return value to the survival local. + encoder.MarkLabel (tryStart); + EmitExportMethodDispatch (encoder, uco, callbackTypeHandle, callbackRef, jniParams, returnKind, locals); + if (!isVoid) { + encoder.StoreLocal (locals.AbiReturnLocalIndex); + } + encoder.Branch (ILOpCode.Leave, afterAll); + + // CATCH (System.Exception e): runtime?.OnUserUnhandledException(ref envp, e); + encoder.MarkLabel (catchStart); + encoder.StoreLocal (2); + encoder.LoadLocal (1); + encoder.Branch (ILOpCode.Brfalse, endCatch); + encoder.LoadLocal (1); + encoder.LoadLocalAddress (0); + encoder.LoadLocal (2); + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (_context.OnUserUnhandledExceptionRef); + encoder.MarkLabel (endCatch); + encoder.Branch (ILOpCode.Leave, afterAll); + + // FINALLY: EndMarshalMethod(ref envp); + encoder.MarkLabel (finallyStart); + encoder.LoadLocalAddress (0); + encoder.Call (_context.EndMarshalMethodRef); + encoder.OpCode (ILOpCode.Endfinally); + + // AFTER: load ABI return (if non-void) and return. + encoder.MarkLabel (afterAll); + if (!isVoid) { + encoder.LoadLocal (locals.AbiReturnLocalIndex); + } + encoder.OpCode (ILOpCode.Ret); + + cfb.AddCatchRegion (tryStart, catchStart, catchStart, finallyStart, _context.ExceptionRef); + cfb.AddFinallyRegion (tryStart, finallyStart, finallyStart, afterAll); + } + + sealed class ExportMethodDispatchLocals + { + public ExportMethodDispatchLocals (Dictionary arrayParameterLocals, int returnLocalIndex, int abiReturnLocalIndex, Action encodeLocals) + { + ArrayParameterLocals = arrayParameterLocals; + ReturnLocalIndex = returnLocalIndex; + AbiReturnLocalIndex = abiReturnLocalIndex; + EncodeLocals = encodeLocals; + } + + public Dictionary ArrayParameterLocals { get; } + + /// Local that holds the managed return value across array copy-backs (-1 if not needed). + public int ReturnLocalIndex { get; } + + /// Local that holds the JNI ABI return value across try/finally so it survives 'leave' (-1 if void). + public int AbiReturnLocalIndex { get; } + + public Action EncodeLocals { get; } + + public bool HasArrayParameters => ArrayParameterLocals.Count > 0; + } + + static ExportMethodDispatchData GetRequiredExportMethodDispatch (UcoMethodData uco) + { + return uco.ExportMethodDispatch ?? throw new InvalidOperationException ($"ExportMethodDispatchEmitter only supports UCO methods with ExportMethodDispatch metadata."); + } + + ExportMethodDispatchLocals CreateExportMethodDispatchLocals (ExportMethodDispatchData exportMethodDispatch, bool isVoid, JniParamKind returnKind) + { + // Local layout (fixed prefix shared with the UCO ctor wrapper): + // 0 = JniTransition envp (valuetype) + // 1 = JniRuntime? runtime (class) + // 2 = Exception e (class) + // Then: + // 3..N = managed array-param copy-back locals (one per array parameter) + // (next) = managed return temp — only when there are array params and return is non-void + // (next) = ABI return temp — only when return is non-void; survives try/finally → afterAll + var arrayParameterLocals = new Dictionary (); + var arrayLocalTypes = new List (); + int nextLocalIndex = 3; + + for (int i = 0; i < exportMethodDispatch.ParameterTypes.Count; i++) { + if (!IsManagedArrayType (exportMethodDispatch.ParameterTypes [i].ManagedTypeName)) { + continue; + } + + arrayParameterLocals.Add (i, nextLocalIndex++); + arrayLocalTypes.Add (exportMethodDispatch.ParameterTypes [i]); + } + + int returnLocalIndex = -1; + TypeRefData? managedReturnType = null; + if (arrayParameterLocals.Count > 0 && !isVoid) { + returnLocalIndex = nextLocalIndex++; + managedReturnType = exportMethodDispatch.ReturnType; + } + + int abiReturnLocalIndex = -1; + if (!isVoid) { + abiReturnLocalIndex = nextLocalIndex++; + } + + return new ExportMethodDispatchLocals ( + arrayParameterLocals, + returnLocalIndex, + abiReturnLocalIndex, + blob => EncodeAllLocals (blob, arrayLocalTypes, managedReturnType, isVoid, returnKind)); + } + + void EncodeAllLocals (BlobBuilder blob, IReadOnlyList arrayLocalTypes, + TypeRefData? managedReturnType, bool isVoid, JniParamKind returnKind) + { + int total = 3 + arrayLocalTypes.Count + (managedReturnType is not null ? 1 : 0) + (isVoid ? 0 : 1); + + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (total); + + // 0: JniTransition (valuetype) + blob.WriteByte (0x11); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_context.JniTransitionRef)); + // 1: JniRuntime (class) + blob.WriteByte (0x12); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_context.JniRuntimeRef)); + // 2: Exception (class) + blob.WriteByte (0x12); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_context.ExceptionRef)); + + // 3..N: managed array-parameter copy-back locals + foreach (var localType in arrayLocalTypes) { + EncodeManagedType (new SignatureTypeEncoder (blob), localType); + } + + // Managed return temp (managed type — same encoding as method parameters) + if (managedReturnType is not null) { + EncodeManagedType (new SignatureTypeEncoder (blob), managedReturnType); + } + + // ABI return temp (JNI ABI type — byte for boolean, IntPtr for object handles, etc.) + if (!isVoid) { + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (blob), returnKind); + } + } + + static bool IsManagedArrayType (string managedTypeName) + => managedTypeName.EndsWith ("[]", StringComparison.Ordinal); + + MemberReferenceHandle AddExportMethodDispatchRef (UcoMethodData uco, EntityHandle callbackTypeHandle) + { + var exportMethodDispatch = GetRequiredExportMethodDispatch (uco); + + return _pe.AddMemberRef (callbackTypeHandle, exportMethodDispatch.ManagedMethodName, + sig => sig.MethodSignature (isInstanceMethod: !exportMethodDispatch.IsStatic).Parameters (exportMethodDispatch.ParameterTypes.Count, + rt => { + if (exportMethodDispatch.ReturnType.ManagedTypeName == "System.Void") { + rt.Void (); + } else { + EncodeManagedType (rt.Type (), exportMethodDispatch.ReturnType); + } + }, + p => { + for (int i = 0; i < exportMethodDispatch.ParameterTypes.Count; i++) { + EncodeManagedType (p.AddParameter ().Type (), exportMethodDispatch.ParameterTypes [i]); + } + })); + } + + void EmitExportMethodDispatch (InstructionEncoder encoder, UcoMethodData uco, EntityHandle callbackTypeHandle, + MemberReferenceHandle callbackRef, List jniParams, JniParamKind returnKind, + ExportMethodDispatchLocals exportMethodDispatchLocals) + { + var exportMethodDispatch = GetRequiredExportMethodDispatch (uco); + + if (!exportMethodDispatch.IsStatic) { + encoder.LoadArgument (1); + encoder.LoadConstantI4 (0); + EmitManagedTypeToken (encoder, callbackTypeHandle); + encoder.Call (_context.JavaLangObjectGetObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (callbackTypeHandle); + } + + for (int i = 0; i < exportMethodDispatch.ParameterTypes.Count; i++) { + LoadManagedArgument (encoder, + exportMethodDispatch.ParameterTypes [i], + GetExportMethodDispatchParameterKind (exportMethodDispatch, i), + jniParams [i], + 2 + i); + + if (exportMethodDispatchLocals.ArrayParameterLocals.TryGetValue (i, out var localIndex)) { + encoder.StoreLocal (localIndex); + encoder.LoadLocal (localIndex); + } + } + + if (exportMethodDispatch.IsStatic) { + encoder.Call (callbackRef); + } else { + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (callbackRef); + } + + EmitManagedArrayCopyBacks (encoder, exportMethodDispatch, returnKind, exportMethodDispatchLocals); + ConvertManagedReturnValue (encoder, exportMethodDispatch.ReturnType, exportMethodDispatch.ReturnKind, returnKind); + } + + static ExportParameterKindInfo GetExportMethodDispatchParameterKind (ExportMethodDispatchData exportMethodDispatch, int index) + => index < exportMethodDispatch.ParameterKinds.Count ? exportMethodDispatch.ParameterKinds [index] : ExportParameterKindInfo.Unspecified; + + void EmitManagedArrayCopyBacks (InstructionEncoder encoder, ExportMethodDispatchData exportMethodDispatch, JniParamKind returnKind, ExportMethodDispatchLocals exportMethodDispatchLocals) + { + if (!exportMethodDispatchLocals.HasArrayParameters) { + return; + } + + if (returnKind != JniParamKind.Void) { + encoder.StoreLocal (exportMethodDispatchLocals.ReturnLocalIndex); + } + + foreach (var kvp in exportMethodDispatchLocals.ArrayParameterLocals) { + var skipCopy = encoder.DefineLabel (); + encoder.LoadLocal (kvp.Value); + encoder.Branch (ILOpCode.Brfalse_s, skipCopy); + encoder.LoadLocal (kvp.Value); + EmitManagedArrayElementTypeToken (encoder, exportMethodDispatch.ParameterTypes [kvp.Key]); + encoder.LoadArgument (2 + kvp.Key); + encoder.Call (_context.JniEnvCopyArrayRef); + encoder.MarkLabel (skipCopy); + } + + if (returnKind != JniParamKind.Void) { + encoder.LoadLocal (exportMethodDispatchLocals.ReturnLocalIndex); + } + } + + /// + /// Emits IL that loads JNI argument onto the + /// stack and converts it to the managed type expected by the user-visible + /// method or constructor parameter. Handles primitives (with byte → bool + /// conversion for System.Boolean), strings, arrays, [Export] + /// parameter kinds (streams / XML parsers), and object peers via + /// Java.Lang.Object.GetObject (IntPtr, JniHandleOwnership, Type). + /// + internal void LoadManagedArgument (InstructionEncoder encoder, TypeRefData managedType, ExportParameterKindInfo exportKind, JniParamKind jniKind, int argumentIndex) + { + string managedTypeName = managedType.ManagedTypeName; + + ThrowIfUnsupportedManagedType (managedTypeName); + + if (TryEmitExportParameterArgument (encoder, exportKind, argumentIndex)) { + return; + } + + if (TryEmitPrimitiveManagedArgument (encoder, managedTypeName, argumentIndex)) { + return; + } + + if (jniKind != JniParamKind.Object) { + encoder.LoadArgument (argumentIndex); + return; + } + + if (IsManagedArrayType (managedTypeName)) { + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + EmitManagedArrayElementTypeToken (encoder, managedType); + encoder.Call (_context.JniEnvGetArrayRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (ResolveManagedTypeHandle (managedType)); + return; + } + + EmitManagedObjectArgument (encoder, managedType, argumentIndex); + } + + void ConvertManagedReturnValue (InstructionEncoder encoder, TypeRefData managedReturnType, ExportParameterKindInfo exportKind, JniParamKind returnKind) + { + string managedReturnTypeName = managedReturnType.ManagedTypeName; + + if (returnKind == JniParamKind.Void) { + return; + } + + if (returnKind != JniParamKind.Object) { + if (managedReturnTypeName == "System.Boolean") { + encoder.OpCode (ILOpCode.Conv_u1); + } + return; + } + + if (managedReturnTypeName == "System.String") { + encoder.Call (_context.JniEnvNewStringRef); + return; + } + + if (managedReturnTypeName == "System.Void") { + return; + } + + if (IsManagedArrayType (managedReturnTypeName)) { + EmitManagedArrayReturn (encoder, managedReturnType); + return; + } + + if (TryEmitExportParameterReturn (encoder, exportKind)) { + return; + } + + // Reference-type returns that need dedicated marshalling. Mirrors the + // SymbolKind dispatch in legacy Mono.Android.Export/CallbackCode.cs: + // - CharSequence.ToLocalJniHandle handles 'string'-as-ICharSequence, + // not just IJavaObject-derived peers. + // - JavaList/JavaDictionary/JavaCollection.ToLocalJniHandle wrap raw + // managed collections without a Java peer. + if (managedReturnTypeName == "Java.Lang.ICharSequence") { + encoder.Call (_context.CharSequenceToLocalJniHandleRef); + return; + } + if (managedReturnTypeName == "System.Collections.IList") { + encoder.Call (_context.JavaListToLocalJniHandleRef); + return; + } + if (managedReturnTypeName == "System.Collections.IDictionary") { + encoder.Call (_context.JavaDictionaryToLocalJniHandleRef); + return; + } + if (managedReturnTypeName == "System.Collections.ICollection") { + encoder.Call (_context.JavaCollectionToLocalJniHandleRef); + return; + } + + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (_context.IJavaObjectRef); + encoder.Call (_context.JniEnvToLocalJniHandleRef); + } + + void ThrowIfUnsupportedManagedType (string managedTypeName) + { + if (managedTypeName.EndsWith ("&", StringComparison.Ordinal) || managedTypeName.EndsWith ("*", StringComparison.Ordinal)) { + throw new NotSupportedException ($"[Export] methods with by-ref or pointer signature types are not supported: '{managedTypeName}'."); + } + + if (managedTypeName.IndexOf ('<') >= 0) { + throw new NotSupportedException ($"[Export] methods with generic signature types are not supported: '{managedTypeName}'."); + } + } + + bool TryEmitExportParameterArgument (InstructionEncoder encoder, ExportParameterKindInfo exportKind, int argumentIndex) + { + switch (exportKind) { + case ExportParameterKindInfo.InputStream: + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.Call (_context.InputStreamInvokerFromJniHandleRef); + return true; + case ExportParameterKindInfo.OutputStream: + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.Call (_context.OutputStreamInvokerFromJniHandleRef); + return true; + case ExportParameterKindInfo.XmlPullParser: + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.Call (_context.XmlPullParserReaderFromJniHandleRef); + return true; + case ExportParameterKindInfo.XmlResourceParser: + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.Call (_context.XmlResourceParserReaderFromJniHandleRef); + return true; + default: + return false; + } + } + + bool TryEmitPrimitiveManagedArgument (InstructionEncoder encoder, string managedTypeName, int argumentIndex) + { + switch (managedTypeName) { + case "System.Boolean": + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.OpCode (ILOpCode.Cgt_un); + return true; + case "System.Byte": + case "System.SByte": + case "System.Char": + case "System.Int16": + case "System.UInt16": + case "System.Int32": + case "System.UInt32": + case "System.Int64": + case "System.UInt64": + case "System.Single": + case "System.Double": + case "System.IntPtr": + encoder.LoadArgument (argumentIndex); + return true; + case "System.String": + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + encoder.Call (_context.JniEnvGetStringRef); + return true; + default: + return false; + } + } + + void EmitManagedObjectArgument (InstructionEncoder encoder, TypeRefData managedType, int argumentIndex) + { + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); + if (managedType.ManagedTypeName == "System.Object") { + encoder.OpCode (ILOpCode.Ldnull); + } else { + EmitManagedTypeToken (encoder, ResolveManagedTypeHandle (managedType)); + } + encoder.Call (_context.JavaLangObjectGetObjectRef); + + if (managedType.ManagedTypeName != "System.Object") { + var managedTypeHandle = ResolveManagedTypeHandle (managedType); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (managedTypeHandle); + } + } + + void EmitManagedArrayReturn (InstructionEncoder encoder, TypeRefData managedReturnType) + { + var nonNullArray = encoder.DefineLabel (); + var done = encoder.DefineLabel (); + + encoder.OpCode (ILOpCode.Dup); + encoder.Branch (ILOpCode.Brtrue_s, nonNullArray); + encoder.OpCode (ILOpCode.Pop); + encoder.LoadConstantI4 (0); + encoder.Branch (ILOpCode.Br_s, done); + encoder.MarkLabel (nonNullArray); + EmitManagedArrayElementTypeToken (encoder, managedReturnType); + encoder.Call (_context.JniEnvNewArrayRef); + encoder.MarkLabel (done); + } + + bool TryEmitExportParameterReturn (InstructionEncoder encoder, ExportParameterKindInfo exportKind) + { + switch (exportKind) { + case ExportParameterKindInfo.InputStream: + encoder.Call (_context.InputStreamAdapterToLocalJniHandleRef); + return true; + case ExportParameterKindInfo.OutputStream: + encoder.Call (_context.OutputStreamAdapterToLocalJniHandleRef); + return true; + case ExportParameterKindInfo.XmlPullParser: + encoder.Call (_context.XmlReaderPullParserToLocalJniHandleRef); + return true; + case ExportParameterKindInfo.XmlResourceParser: + encoder.Call (_context.XmlReaderResourceParserToLocalJniHandleRef); + return true; + default: + return false; + } + } + + void EmitManagedTypeToken (InstructionEncoder encoder, EntityHandle typeHandle) + { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (typeHandle); + encoder.Call (_context.GetTypeFromHandleRef); + } + + void EmitManagedArrayElementTypeToken (InstructionEncoder encoder, TypeRefData arrayType) + { + var elementType = arrayType with { + ManagedTypeName = arrayType.ManagedTypeName.Substring (0, arrayType.ManagedTypeName.Length - 2), + }; + EmitManagedTypeToken (encoder, ResolveManagedTypeHandle (elementType)); + } + + EntityHandle ResolveManagedTypeHandle (TypeRefData managedType) + { + if (IsManagedArrayType (managedType.ManagedTypeName)) { + var blob = new BlobBuilder (); + EncodeManagedType (new SignatureTypeEncoder (blob), managedType); + return _pe.Metadata.AddTypeSpecification (_pe.Metadata.GetOrAddBlob (blob)); + } + + return _pe.ResolveTypeRef (managedType); + } + + void EncodeManagedType (SignatureTypeEncoder encoder, TypeRefData managedType) + { + string managedTypeName = managedType.ManagedTypeName; + + ThrowIfUnsupportedManagedType (managedTypeName); + if (managedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + EncodeManagedType (encoder.SZArray (), managedType with { + ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 2), + }); + return; + } + + switch (managedTypeName) { + case "System.Boolean": encoder.Boolean (); return; + case "System.Byte": encoder.Byte (); return; + case "System.SByte": encoder.SByte (); return; + case "System.Char": encoder.Char (); return; + case "System.Int16": encoder.Int16 (); return; + case "System.UInt16": encoder.UInt16 (); return; + case "System.Int32": encoder.Int32 (); return; + case "System.UInt32": encoder.UInt32 (); return; + case "System.Int64": encoder.Int64 (); return; + case "System.UInt64": encoder.UInt64 (); return; + case "System.Single": encoder.Single (); return; + case "System.Double": encoder.Double (); return; + case "System.String": encoder.String (); return; + case "System.Object": encoder.Object (); return; + case "System.IntPtr": encoder.IntPtr (); return; + } + + var typeHandle = ResolveManagedTypeHandle (managedType); + encoder.Type (typeHandle, isValueType: managedType.IsEnum); + } + + void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle) + { + _pe.Metadata.AddCustomAttribute (handle, _context.UcoAttrCtorRef, _context.UcoAttrBlobHandle); + } + +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitterContext.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitterContext.cs new file mode 100644 index 00000000000..95afef57d86 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitterContext.cs @@ -0,0 +1,228 @@ +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Holds pre-resolved metadata references needed by +/// for generating [Export] method dispatch IL. Created once per emit pass and reused +/// for all export methods. +/// +sealed class ExportMethodDispatchEmitterContext +{ + public static ExportMethodDispatchEmitterContext Create ( + PEAssemblyBuilder pe, + TypeReferenceHandle iJavaPeerableRef, + TypeReferenceHandle jniHandleOwnershipRef, + TypeReferenceHandle jniEnvRef, + TypeReferenceHandle systemTypeRef, + MemberReferenceHandle getTypeFromHandleRef, + MemberReferenceHandle ucoAttrCtorRef, + BlobHandle ucoAttrBlobHandle, + TypeReferenceHandle jniTransitionRef, + TypeReferenceHandle jniRuntimeRef, + TypeReferenceHandle exceptionRef, + MemberReferenceHandle beginMarshalMethodRef, + MemberReferenceHandle endMarshalMethodRef, + MemberReferenceHandle onUserUnhandledExceptionRef) + { + var metadata = pe.Metadata; + var iJavaObjectRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("IJavaObject")); + var javaLangObjectRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + var systemArrayRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Array")); + var systemStreamRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.IO"), metadata.GetOrAddString ("Stream")); + var systemXmlRef = pe.FindOrAddAssemblyRef ("System.Xml.ReaderWriter"); + var systemXmlReaderRef = metadata.AddTypeReference (systemXmlRef, + metadata.GetOrAddString ("System.Xml"), metadata.GetOrAddString ("XmlReader")); + var inputStreamInvokerRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("InputStreamInvoker")); + var outputStreamInvokerRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("OutputStreamInvoker")); + var inputStreamAdapterRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("InputStreamAdapter")); + var outputStreamAdapterRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("OutputStreamAdapter")); + var xmlPullParserReaderRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("XmlPullParserReader")); + var xmlResourceParserReaderRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("XmlResourceParserReader")); + var xmlReaderPullParserRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("XmlReaderPullParser")); + var xmlReaderResourceParserRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("XmlReaderResourceParser")); + var charSequenceRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("CharSequence")); + var iCharSequenceRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("ICharSequence")); + var javaListRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JavaList")); + var javaDictionaryRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JavaDictionary")); + var javaCollectionRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JavaCollection")); + var systemCollectionsIListRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.Collections"), metadata.GetOrAddString ("IList")); + var systemCollectionsIDictionaryRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.Collections"), metadata.GetOrAddString ("IDictionary")); + var systemCollectionsICollectionRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.Collections"), metadata.GetOrAddString ("ICollection")); + + return new ExportMethodDispatchEmitterContext { + IJavaObjectRef = iJavaObjectRef, + GetTypeFromHandleRef = getTypeFromHandleRef, + JniEnvGetStringRef = pe.AddMemberRef (jniEnvRef, "GetString", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + })), + JniEnvGetArrayRef = pe.AddMemberRef (jniEnvRef, "GetArray", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Type (systemArrayRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + p.AddParameter ().Type ().Type (systemTypeRef, false); + })), + JniEnvCopyArrayRef = pe.AddMemberRef (jniEnvRef, "CopyArray", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (systemArrayRef, false); + p.AddParameter ().Type ().Type (systemTypeRef, false); + p.AddParameter ().Type ().IntPtr (); + })), + JniEnvNewArrayRef = pe.AddMemberRef (jniEnvRef, "NewArray", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().IntPtr (), + p => { + p.AddParameter ().Type ().Type (systemArrayRef, false); + p.AddParameter ().Type ().Type (systemTypeRef, false); + })), + JniEnvNewStringRef = pe.AddMemberRef (jniEnvRef, "NewString", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().String ())), + JniEnvToLocalJniHandleRef = pe.AddMemberRef (jniEnvRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (iJavaObjectRef, false))), + JavaLangObjectGetObjectRef = pe.AddMemberRef (javaLangObjectRef, "GetObject", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Type (iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + p.AddParameter ().Type ().Type (systemTypeRef, false); + })), + InputStreamInvokerFromJniHandleRef = pe.AddMemberRef (inputStreamInvokerRef, "FromJniHandle", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().Type (systemStreamRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + })), + OutputStreamInvokerFromJniHandleRef = pe.AddMemberRef (outputStreamInvokerRef, "FromJniHandle", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().Type (systemStreamRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + })), + InputStreamAdapterToLocalJniHandleRef = pe.AddMemberRef (inputStreamAdapterRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemStreamRef, false))), + OutputStreamAdapterToLocalJniHandleRef = pe.AddMemberRef (outputStreamAdapterRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemStreamRef, false))), + XmlPullParserReaderFromJniHandleRef = pe.AddMemberRef (xmlPullParserReaderRef, "FromJniHandle", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().Type (systemXmlReaderRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + })), + XmlResourceParserReaderFromJniHandleRef = pe.AddMemberRef (xmlResourceParserReaderRef, "FromJniHandle", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().Type (systemXmlReaderRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (jniHandleOwnershipRef, true); + })), + XmlReaderPullParserToLocalJniHandleRef = pe.AddMemberRef (xmlReaderPullParserRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemXmlReaderRef, false))), + XmlReaderResourceParserToLocalJniHandleRef = pe.AddMemberRef (xmlReaderResourceParserRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemXmlReaderRef, false))), + CharSequenceToLocalJniHandleRef = pe.AddMemberRef (charSequenceRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (iCharSequenceRef, false))), + JavaListToLocalJniHandleRef = pe.AddMemberRef (javaListRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemCollectionsIListRef, false))), + JavaDictionaryToLocalJniHandleRef = pe.AddMemberRef (javaDictionaryRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemCollectionsIDictionaryRef, false))), + JavaCollectionToLocalJniHandleRef = pe.AddMemberRef (javaCollectionRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (systemCollectionsICollectionRef, false))), + UcoAttrCtorRef = ucoAttrCtorRef, + UcoAttrBlobHandle = ucoAttrBlobHandle, + JniTransitionRef = jniTransitionRef, + JniRuntimeRef = jniRuntimeRef, + ExceptionRef = exceptionRef, + BeginMarshalMethodRef = beginMarshalMethodRef, + EndMarshalMethodRef = endMarshalMethodRef, + OnUserUnhandledExceptionRef = onUserUnhandledExceptionRef, + }; + } + + public required TypeReferenceHandle IJavaObjectRef { get; init; } + public required MemberReferenceHandle GetTypeFromHandleRef { get; init; } + public required MemberReferenceHandle JniEnvGetStringRef { get; init; } + public required MemberReferenceHandle JniEnvGetArrayRef { get; init; } + public required MemberReferenceHandle JniEnvCopyArrayRef { get; init; } + public required MemberReferenceHandle JniEnvNewArrayRef { get; init; } + public required MemberReferenceHandle JniEnvNewStringRef { get; init; } + public required MemberReferenceHandle JniEnvToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle JavaLangObjectGetObjectRef { get; init; } + public required MemberReferenceHandle InputStreamInvokerFromJniHandleRef { get; init; } + public required MemberReferenceHandle OutputStreamInvokerFromJniHandleRef { get; init; } + public required MemberReferenceHandle InputStreamAdapterToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle OutputStreamAdapterToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle XmlPullParserReaderFromJniHandleRef { get; init; } + public required MemberReferenceHandle XmlResourceParserReaderFromJniHandleRef { get; init; } + public required MemberReferenceHandle XmlReaderPullParserToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle XmlReaderResourceParserToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle CharSequenceToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle JavaListToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle JavaDictionaryToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle JavaCollectionToLocalJniHandleRef { get; init; } + public required MemberReferenceHandle UcoAttrCtorRef { get; init; } + + public required BlobHandle UcoAttrBlobHandle { get; init; } + + // Marshal-method wrapper plumbing — mirrors the UCO ctor wrapper used by + // TypeMapAssemblyEmitter so that managed exceptions thrown from [Export] method + // bodies surface as Java exceptions instead of crashing the runtime. + public required TypeReferenceHandle JniTransitionRef { get; init; } + public required TypeReferenceHandle JniRuntimeRef { get; init; } + public required TypeReferenceHandle ExceptionRef { get; init; } + public required MemberReferenceHandle BeginMarshalMethodRef { get; init; } + public required MemberReferenceHandle EndMarshalMethodRef { get; init; } + public required MemberReferenceHandle OnUserUnhandledExceptionRef { get; init; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 0d2b15f803c..38b4026a4aa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -262,13 +262,14 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) """); } else { string access = method.IsExport && method.JavaAccess != null ? method.JavaAccess : "public"; + string staticKeyword = method.IsStatic ? "static " : ""; writer.Write ($$""" - {{access}} {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} + {{access}} {{staticKeyword}}{{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { {{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } - {{access}} native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); + {{access}} {{staticKeyword}}native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); """); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index b9126586bf4..14c00a343dd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -142,6 +142,13 @@ sealed class JavaPeerProxyData /// public bool IsGenericDefinition { get; init; } + /// + /// True when the Java stub must not call RegisterNatives from a static initializer because + /// the type can be instantiated before the runtime is fully ready (for example Application + /// or Instrumentation subclasses). + /// + public bool CannotRegisterInStaticConstructor { get; init; } + /// /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). /// @@ -166,7 +173,7 @@ sealed class JavaPeerProxyData /// /// A cross-assembly type reference (assembly name + full managed type name). /// -sealed record TypeRefData +public sealed record TypeRefData { /// /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". @@ -177,11 +184,19 @@ sealed record TypeRefData /// Assembly containing the type, e.g., "Mono.Android". /// public required string AssemblyName { get; init; } + + /// + /// True if this type — or, for array types, the element type — is an enum. + /// Used by the IL emitter to encode the type as ELEMENT_TYPE_VALUETYPE + /// rather than ELEMENT_TYPE_CLASS in member references and signatures. + /// + public bool IsEnum { get; init; } } /// /// An [UnmanagedCallersOnly] static wrapper for a marshal method. -/// Body: load all args → call n_* callback → ret. +/// Body: either forward to an existing n_* callback or dispatch directly to the +/// managed export target when the trimmable path can avoid dynamic callback generation. /// sealed record UcoMethodData { @@ -191,7 +206,7 @@ sealed record UcoMethodData public required string WrapperName { get; init; } /// - /// Name of the n_* callback to call, e.g., "n_OnCreate". + /// Java/JNI-visible native method name, e.g., "n_OnCreate". /// public required string CallbackMethodName { get; init; } @@ -204,6 +219,53 @@ sealed record UcoMethodData /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. /// public required string JniSignature { get; init; } + + /// + /// Optional [Export]-only metadata for wrappers that dispatch directly to the + /// managed export target instead of forwarding to a generated n_* callback. + /// + public ExportMethodDispatchData? ExportMethodDispatch { get; init; } + + /// + /// True when this wrapper performs the static [Export] direct-dispatch path. + /// + public bool UsesExportMethodDispatch => ExportMethodDispatch != null; +} + +sealed record ExportMethodDispatchData +{ + /// + /// Managed method name on the callback type that should be invoked for [Export]. + /// + public required string ManagedMethodName { get; init; } + + /// + /// Managed parameter types for the target method, including the defining assembly. + /// + public IReadOnlyList ParameterTypes { get; init; } = []; + + /// + /// Per-parameter [ExportParameter] kinds for legacy callback marshalling. + /// + public IReadOnlyList ParameterKinds { get; init; } = []; + + /// + /// Managed return type for the target method, including the defining assembly. + /// + public TypeRefData ReturnType { get; init; } = new () { + ManagedTypeName = "System.Void", + AssemblyName = "System.Runtime", + }; + + /// + /// [ExportParameter] kind applied to the return value, if any. + /// + public ExportParameterKindInfo ReturnKind { get; init; } + + /// + /// Whether the managed target method is static. + /// + public bool IsStatic { get; init; } } /// @@ -228,6 +290,24 @@ sealed record UcoConstructorData /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration. /// public required string JniSignature { get; init; } + + /// + /// when the UCO codegen can statically prove the managed + /// type defines a matching user-visible ctor with this signature. When + /// , the codegen must use the legacy activation-ctor + /// `(IntPtr, JniHandleOwnership)` path instead of emitting a member ref to + /// a (potentially non-existent) user ctor. + /// + public required bool HasMatchingManagedCtor { get; init; } + + /// + /// Managed parameter types of the matching user-visible ctor, in declaration + /// order. Empty for `()V`. Non-empty when + /// is and the ctor takes parameters; the emitter uses + /// this to build the member ref signature and to marshal each JNI argument + /// to the corresponding managed type before calling the user ctor. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 7547c5ac38f..3337bf2d4bf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -298,6 +298,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash }, IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, + CannotRegisterInStaticConstructor = peer.CannotRegisterInStaticConstructor, }; if (peer.InvokerTypeName != null) { @@ -346,6 +347,14 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) AssemblyName = !mm.DeclaringAssemblyName.IsNullOrEmpty () ? mm.DeclaringAssemblyName : peer.AssemblyName, }, JniSignature = mm.JniSignature, + ExportMethodDispatch = mm.IsExport ? new ExportMethodDispatchData { + ManagedMethodName = mm.ManagedMethodName, + ParameterTypes = mm.ManagedParameterTypes, + ParameterKinds = mm.ManagedParameterExportKinds, + ReturnType = mm.ManagedReturnType, + ReturnKind = mm.ManagedReturnExportKind, + IsStatic = mm.IsStatic, + } : null, }); ucoIndex++; } @@ -361,6 +370,8 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) proxy.UcoConstructors.Add (new UcoConstructorData { WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", JniSignature = ctor.JniSignature, + HasMatchingManagedCtor = ctor.HasMatchingManagedCtor, + ManagedParameterTypes = ctor.ManagedParameterTypes, TargetType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index af36173550f..6c3506f3efa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -38,15 +38,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// // JniName / TargetType / InvokerType are supplied by the base JavaPeerProxy constructor. /// /// // 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) -/// { -/// AndroidRuntimeInternal.WaitForBridgeProcessing(); -/// try { -/// Activity.n_OnCreate(jnienv, self, p0); -/// } catch (Exception e) { -/// AndroidEnvironmentInternal.UnhandledException(e); -/// } -/// } +/// => Activity.n_OnCreate(jnienv, self, p0); /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) @@ -97,10 +91,10 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _iJavaPeerableSetPeerReferenceRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; - MemberReferenceHandle _waitForBridgeProcessingRef; - MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; + MemberReferenceHandle _withinNewObjectScopeRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -114,8 +108,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniTransitionRef; TypeReferenceHandle _jniRuntimeRef; TypeReferenceHandle _exceptionRef; - TypeReferenceHandle _androidRuntimeInternalRef; - TypeReferenceHandle _androidEnvironmentInternalRef; MemberReferenceHandle _beginMarshalMethodRef; MemberReferenceHandle _endMarshalMethodRef; @@ -129,6 +121,8 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; + ExportMethodDispatchEmitter? _exportMethodDispatchEmitter; + // Per-rank array sentinel TypeDefs, 0-indexed by (rank - 1). Empty when array entries // aren't emitted. EntityHandle [] _rankAnchorHandles = []; @@ -259,11 +253,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime")); _exceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); - var monoAndroidRuntimeRef = _pe.AddAssemblyRef ("Mono.Android.Runtime", new Version (0, 0, 0, 0)); - _androidRuntimeInternalRef = metadata.AddTypeReference (monoAndroidRuntimeRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidRuntimeInternal")); - _androidEnvironmentInternalRef = metadata.AddTypeReference (monoAndroidRuntimeRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidEnvironmentInternal")); // ReadOnlySpan — TypeSpec for generic instantiation _readOnlySpanOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -340,6 +329,16 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniObjectReferenceTypeRef, true); })); + // IJavaPeerable.SetPeerReference(JniObjectReference) — instance interface method. + // Used by UCO constructor wrappers (parameterless `()V`) to mirror TypeManager.Activate: + // after GetUninitializedObject we set the peer reference directly, then invoke the + // user-visible parameterless ctor (whose base ctor chain into Java.Lang.Object is a + // no-op when the peer is already set). + _iJavaPeerableSetPeerReferenceRef = _pe.AddMemberRef (_iJavaPeerableRef, "SetPeerReference", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniObjectReferenceRef, true))); + // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal // Used by JI-style activation to clean up the original handle after constructing the peer. // Matches the legacy TypeManager.CreateProxy behavior. @@ -357,13 +356,11 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { p.AddParameter ().Type ().IntPtr (); })); - _waitForBridgeProcessingRef = _pe.AddMemberRef (_androidRuntimeInternalRef, "WaitForBridgeProcessing", - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); - - _androidEnvironmentUnhandledExceptionRef = _pe.AddMemberRef (_androidEnvironmentInternalRef, "UnhandledException", - sig => sig.MethodSignature ().Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().Type (_exceptionRef, false))); + // JniEnvironment.get_WithinNewObjectScope() -> bool (static property) + _withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope", + sig => sig.MethodSignature ().Parameters (0, + rt => rt.Type ().Boolean (), + p => { })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", @@ -406,7 +403,7 @@ void EmitMemberReferences () _ucoAttrCtorRef = _pe.AddMemberRef (ucoAttrTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - // Legacy marshal-method UCO wrappers use the default unmanaged calling convention. + // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { }); // JniEnvironment.BeginMarshalMethod(nint jnienv, out JniTransition, out JniRuntime?) -> bool @@ -506,6 +503,34 @@ void EmitTypeMapAssociationAttributeCtorRef () })); } + ExportMethodDispatchEmitterContext CreateExportMethodDispatchEmitterContext () + { + return ExportMethodDispatchEmitterContext.Create ( + _pe, + _iJavaPeerableRef, + _jniHandleOwnershipRef, + _jniEnvRef, + _systemTypeRef, + _getTypeFromHandleRef, + _ucoAttrCtorRef, + _ucoAttrBlobHandle, + _jniTransitionRef, + _jniRuntimeRef, + _exceptionRef, + _beginMarshalMethodRef, + _endMarshalMethodRef, + _onUserUnhandledExceptionRef + ); + } + + ExportMethodDispatchEmitter GetExportMethodDispatchEmitter () + { + // [Export] is a niche feature; create the emitter lazily so we only pay + // for it in assemblies that actually contain export-attributed methods. + _exportMethodDispatchEmitter ??= new ExportMethodDispatchEmitter (_pe, CreateExportMethodDispatchEmitterContext ()); + return _exportMethodDispatchEmitter; + } + void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { if (proxy.IsAcw) { @@ -600,7 +625,9 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary EmitUcoForwarderBody (encoder, cfb, returnKind, enc => { + encoder => { for (int p = 0; p < paramCount; p++) - enc.LoadArgument (p); - enc.Call (callbackRef); - }), - blob => EncodeUcoForwarderLegacyLocals (blob, returnKind)); + encoder.LoadArgument (p); + encoder.Call (callbackRef); + encoder.OpCode (ILOpCode.Ret); + }); AddUnmanagedCallersOnlyAttribute (handle); return handle; } - void EmitUcoForwarderBody (InstructionEncoder encoder, ControlFlowBuilder cfb, JniParamKind returnKind, Action emitCallback) - { - bool isVoid = returnKind == JniParamKind.Void; - var tryStart = encoder.DefineLabel (); - var catchStart = encoder.DefineLabel (); - var afterAll = encoder.DefineLabel (); - - encoder.Call (_waitForBridgeProcessingRef); - encoder.MarkLabel (tryStart); - emitCallback (encoder); - if (!isVoid) { - encoder.StoreLocal (0); - } - encoder.Branch (ILOpCode.Leave, afterAll); - - encoder.MarkLabel (catchStart); - encoder.StoreLocal (isVoid ? 0 : 1); - encoder.LoadLocal (isVoid ? 0 : 1); - encoder.Call (_androidEnvironmentUnhandledExceptionRef); - encoder.Branch (ILOpCode.Leave, afterAll); - - encoder.MarkLabel (afterAll); - if (!isVoid) { - encoder.LoadLocal (0); - } - encoder.OpCode (ILOpCode.Ret); - - cfb.AddCatchRegion (tryStart, catchStart, catchStart, afterAll, _exceptionRef); - } - - void EncodeUcoForwarderLegacyLocals (BlobBuilder blob, JniParamKind returnKind) - { - bool isVoid = returnKind == JniParamKind.Void; - blob.WriteByte (0x07); // LOCAL_SIG - blob.WriteCompressedInteger (isVoid ? 1 : 2); - if (!isVoid) { - JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (blob), returnKind); - } - blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_exceptionRef)); - } - MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxyData proxy) { + var targetTypeRef = _pe.ResolveTypeRef (uco.TargetType); var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ( $"UCO constructor wrapper requires an activation ctor for '{uco.TargetType.ManagedTypeName}'"); - // UCO constructor wrappers must match the JNI native method signature exactly. - // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because we create the managed peer using the - // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. + // UCO constructor wrappers must match the JNI native method signature exactly: + // arg 0 is the JNIEnv*, arg 1 is the self handle, and the remaining args are + // the JNI ctor parameters. Whether those parameters are forwarded to a managed + // .ctor depends on the activation path chosen below. var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); int paramCount = 2 + jniParams.Count; @@ -1087,6 +1069,40 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy }), EncodeUcoConstructorLocals_JavaInterop); } else { + // For Java ctors that map to a known managed user-visible ctor, mirror + // TypeManager.Activate so that the user-visible managed ctor body runs + // when the peer is created from the Java side (i.e. so user-defined + // initialization in `MyType (...)` actually executes — equivalent to + // `cinfo.Invoke (newobj, parms)` in the reflection-based activator): + // + // var obj = (TargetType) RuntimeHelpers.GetUninitializedObject (typeof (TargetType)); + // ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self)); + // obj..ctor (marshalled_args...); + // + // The user-visible ctor's chain into Java.Lang.Object/IJavaPeerable is + // a no-op when the peer reference is already set (guarded by + // `if (PeerReference.IsValid) return;`), so this does not create a + // second Java peer. + // + // We only take this path when the scanner located a matching managed + // ctor. Types like `Java.Lang.Thread+RunnableImplementor` register a + // `()V` Java ctor via JCW codegen but only define parameterized + // managed ctors, so emitting a member ref to `..ctor ()` would resolve + // to a non-existent method at runtime — those fall through to the + // legacy activation-ctor `(IntPtr, JniHandleOwnership)` path below. + // + // Reference (`L...;`) JNI args are unmarshalled via + // `Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer, paramType)` + // and cast to the matching managed parameter type. Primitive JNI args + // (Z/B/C/S/I/J/F/D) are loaded directly (with a `byte → bool` conversion + // for `System.Boolean`); strings and arrays go through the `JNIEnv` helpers. + // All marshalling is delegated to . + if (uco.HasMatchingManagedCtor) { + handle = EmitUserVisibleCtorWrapper (uco, targetTypeRef, encodeSig); + AddUnmanagedCallersOnlyAttribute (handle); + return handle; + } + var ctorRef = AddActivationCtorRef ( activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); @@ -1123,6 +1139,73 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return handle; } + /// + /// Emits a UCO constructor wrapper that mirrors + /// by invoking the user-visible managed ctor on a peer materialized via + /// : + /// + /// var obj = (TargetType) RuntimeHelpers.GetUninitializedObject (typeof (TargetType)); + /// ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self)); + /// obj..ctor ( + /// (TParam0) Java.Lang.Object.GetObject (arg0, JniHandleOwnership.DoNotTransfer, typeof (TParam0)), + /// ...); + /// + /// Each JNI object argument is unmarshalled via the internal + /// Java.Lang.Object.GetObject (IntPtr, JniHandleOwnership, Type) helper + /// (reachable via [IgnoresAccessChecksTo("Mono.Android")]). + /// + MethodDefinitionHandle EmitUserVisibleCtorWrapper (UcoConstructorData uco, EntityHandle targetTypeRef, Action encodeSig) + { + var managedParamTypes = uco.ManagedParameterTypes; + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + var managedParamTypeRefs = new EntityHandle [managedParamTypes.Count]; + for (int i = 0; i < managedParamTypes.Count; i++) { + managedParamTypeRefs [i] = _pe.ResolveTypeRef (managedParamTypes [i]); + } + var userCtorRef = _pe.AddMemberRef (targetTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (managedParamTypes.Count, + rt => rt.Void (), + p => { + for (int i = 0; i < managedParamTypes.Count; i++) { + p.AddParameter ().Type ().Type (managedParamTypeRefs [i], false); + } + })); + // Argument marshalling reuses ExportMethodDispatchEmitter.LoadManagedArgument, + // which already handles primitives (with byte → bool conversion), strings, + // arrays, and object peers via Java.Lang.Object.GetObject. The emitter is + // only resolved when there are parameters to marshal so the parameterless + // `()V` path doesn't pull in the export-marshalling member refs. + var argLoader = managedParamTypes.Count > 0 ? GetExportMethodDispatchEmitter () : null; + return _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + // var obj = (TargetType) RuntimeHelpers.GetUninitializedObject (typeof (TargetType)); + enc.OpCode (ILOpCode.Ldtoken); + enc.Token (targetTypeRef); + enc.Call (_getTypeFromHandleRef); + enc.Call (_getUninitializedObjectRef); + enc.OpCode (ILOpCode.Castclass); + enc.Token (targetTypeRef); + + // ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self, Invalid)); + enc.OpCode (ILOpCode.Dup); + enc.LoadArgument (1); // self IntPtr + enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid + enc.OpCode (ILOpCode.Newobj); + enc.Token (_jniObjectReferenceCtorRef); + enc.OpCode (ILOpCode.Callvirt); + enc.Token (_iJavaPeerableSetPeerReferenceRef); + + for (int i = 0; i < managedParamTypes.Count; i++) { + argLoader!.LoadManagedArgument (enc, managedParamTypes [i], ExportParameterKindInfo.Unspecified, jniParams [i], 2 + i); + } + + enc.Call (userCtorRef); + }), + EncodeUcoConstructorLocals_Standard); + } + /// /// Emits the common try/catch/finally marshal-method wrapper pattern used by all /// non-generic UCO constructor bodies: @@ -1237,11 +1320,10 @@ void EncodeUcoConstructorLocals_JavaInterop (BlobBuilder blob) blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); } - void EmitRegisterNatives (JavaPeerProxyData proxy, + void EmitRegisterNatives (List registrations, Dictionary wrapperHandles) { // Filter to only registrations that have corresponding wrapper methods - var registrations = proxy.NativeRegistrations; var validRegs = new List<(NativeRegistrationData Reg, MethodDefinitionHandle Wrapper)> (registrations.Count); foreach (var reg in registrations) { if (wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index bfd2db7feac..3a6ac5a2c31 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -582,6 +582,8 @@ sealed record ExportInfo { public IReadOnlyList? ThrownNames { get; init; } public string? SuperArgumentsString { get; init; } + public IReadOnlyList ParameterKinds { get; init; } = []; + public ExportParameterKindInfo ReturnKind { get; init; } } class TypeAttributeInfo (string attributeName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ExportParameterKindInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ExportParameterKindInfo.cs new file mode 100644 index 00000000000..b44e69f7c72 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ExportParameterKindInfo.cs @@ -0,0 +1,14 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Identifies a special [ExportParameter] marshalling kind applied to +/// a parameter or return value of an [Export] method. +/// +public enum ExportParameterKindInfo +{ + Unspecified = 0, + InputStream = 1, + OutputStream = 2, + XmlPullParser = 3, + XmlResourceParser = 4, +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index ee285b42798..94c77a5d671 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -190,10 +190,39 @@ public sealed record MarshalMethodInfo /// /// The native callback method name, e.g., "n_onCreate". - /// This is the actual method the UCO wrapper delegates to. + /// This is the Java/JNI-visible native method name that the generated JCW calls. /// public required string NativeCallbackName { get; init; } + /// + /// Managed parameter types decoded from the method signature, including the + /// defining assembly for each type. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; + + /// + /// Per-parameter [ExportParameter] kinds for legacy callback marshalling. + /// + public IReadOnlyList ManagedParameterExportKinds { get; init; } = []; + + /// + /// Managed return type, including the defining assembly. + /// + public TypeRefData ManagedReturnType { get; init; } = new () { + ManagedTypeName = "System.Void", + AssemblyName = "System.Runtime", + }; + + /// + /// [ExportParameter] kind applied to the return value, if any. + /// + public ExportParameterKindInfo ManagedReturnExportKind { get; init; } + + /// + /// Whether the managed target method is static. + /// + public bool IsStatic { get; init; } + /// /// True if this is a constructor registration. /// @@ -262,6 +291,23 @@ public sealed record JavaConstructorInfo /// public required int ConstructorIndex { get; init; } + /// + /// For "()V" Java ctors: when the managed type defines a + /// matching parameterless instance ctor (`..ctor()`). When , + /// the UCO ctor codegen falls back to the legacy `(IntPtr, JniHandleOwnership)` + /// activation-ctor path so we don't emit a metadata reference to a non-existent + /// `..ctor()` (e.g., RunnableImplementor, which only has parameterized ctors). + /// + public bool HasMatchingManagedCtor { get; init; } + + /// + /// Managed parameter types of the matching user-visible ctor, captured by the + /// scanner when is . + /// Empty for `()V`. Used by the emitter to build the member ref signature for + /// the user ctor call and to marshal each JNI arg into its managed type. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; + /// /// For [Export] constructors: super constructor arguments string. /// Null for [Register] constructors. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index dda7460271c..8a26d1b3b0d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -262,7 +263,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A IsUnconditional = isUnconditional, CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, MarshalMethods = marshalMethods, - JavaConstructors = BuildJavaConstructors (marshalMethods), + JavaConstructors = BuildJavaConstructors (marshalMethods, typeDef, index), JavaFields = exportFields, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, @@ -293,8 +294,16 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - registeredMethodKeys.Add ($"{index.Reader.GetString (methodDef.Name)}({string.Join (",", sig.ParameterTypes)})"); + // Only [Register]-direct (and [JniConstructorSignature]) registrations + // should preempt Pass 3 base-override detection. [Export]/[ExportField] + // are orthogonal to a [Register]-driven override on the same method — + // e.g., `[Export("foo")] public override void OnCreate(...)` needs both + // the [Register]-driven override entry (Get*Handler connector) AND the + // [Export]-driven entry. Skip the dedup key for [Export]/[ExportField]. + if (exportInfo is null) { + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + registeredMethodKeys.Add ($"{index.Reader.GetString (methodDef.Name)}({string.Join (",", sig.ParameterTypes)})"); + } } // Pass 2: collect [Register] from properties (attribute is on the property, not the getter) @@ -669,14 +678,149 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, string? TryResolveJniObjectDescriptor (string managedType) { foreach (var index in assemblyCache.Values) { - if (index.TypesByFullName.TryGetValue (managedType, out var handle) && - index.RegisterInfoByType.TryGetValue (handle, out var registerInfo)) { - return $"L{registerInfo.JniName};"; + if (index.TypesByFullName.TryGetValue (managedType, out var handle)) { + if (index.RegisterInfoByType.TryGetValue (handle, out var registerInfo)) { + return $"L{registerInfo.JniName};"; + } + + // User peer types (extend a Java peer but lack [Register]) + // get a CRC64-based JNI name in ScanAssembly. Mirror that here + // so [Export]/[ExportField] signatures referring to such types + // emit the correct peer descriptor instead of falling back to + // java/lang/Object. + var typeDef = index.Reader.GetTypeDefinition (handle); + if (ExtendsJavaPeer (typeDef, index)) { + var (jniName, _) = ComputeAutoJniNames (typeDef, index); + return $"L{jniName};"; + } } } return null; } + /// + /// Resolves a `typeof(X)` argument captured as an assembly-qualified name + /// (e.g. "Java.IO.IOException, Mono.Android, ...") to its JNI internal + /// name (java/io/IOException). Returns null when the type cannot be + /// found among the loaded assemblies or has no [Register] attribute. + /// + string? ResolveTypeOfArgumentToJniName (string assemblyQualifiedName) + { + var commaIdx = assemblyQualifiedName.IndexOf (','); + var typeName = (commaIdx >= 0 ? assemblyQualifiedName.Substring (0, commaIdx) : assemblyQualifiedName).Trim (); + var descriptor = TryResolveJniObjectDescriptor (typeName); + if (descriptor is null || descriptor.Length < 3) { + return null; + } + // Strip leading 'L' and trailing ';' to get "java/io/IOException". + return descriptor.Substring (1, descriptor.Length - 2); + } + + /// + /// If resolves to an enum type, returns the + /// JNI descriptor of its underlying primitive ("I", "B", "S", "J"). Otherwise + /// returns null. Mirrors legacy CallbackCode behavior, where enum parameters + /// are passed via their underlying integer JNI ABI rather than as objects. + /// + string? TryResolveEnumUnderlyingDescriptor (string managedType, string? assemblyName = null) + { + var typeDef = TryFindEnumTypeDefinition (managedType, assemblyName); + if (typeDef is null) { + return null; + } + + return GetEnumUnderlyingPrimitiveDescriptor (typeDef.Value.typeDef, typeDef.Value.index); + } + + /// + /// Returns true if , or — for array types — + /// its element type, resolves to an enum. The IL emitter uses this to encode + /// the type as a valuetype rather than a class in signatures and member refs. + /// + bool IsEnumOrEnumArray (string managedType, string? assemblyName = null) + { + while (managedType.EndsWith ("[]", StringComparison.Ordinal)) { + managedType = managedType.Substring (0, managedType.Length - 2); + } + + return TryFindEnumTypeDefinition (managedType, assemblyName) is not null; + } + + (TypeDefinition typeDef, AssemblyIndex index)? TryFindEnumTypeDefinition (string managedType, string? assemblyName = null) + { + // Prefer the typed assembly hint so two assemblies with same-named types + // (one enum, one not) resolve deterministically — assemblyCache + // enumeration order is non-deterministic. + if (assemblyName is { Length: > 0 } && + assemblyCache.TryGetValue (assemblyName, out var hintedIndex) && + hintedIndex.TypesByFullName.TryGetValue (managedType, out var hintedHandle)) { + var hintedDef = hintedIndex.Reader.GetTypeDefinition (hintedHandle); + if (IsEnumType (hintedDef, hintedIndex)) { + return (hintedDef, hintedIndex); + } + // Hinted assembly had a same-named non-enum; keep scanning. + } + + foreach (var index in assemblyCache.Values) { + if (!index.TypesByFullName.TryGetValue (managedType, out var handle)) { + continue; + } + + var typeDef = index.Reader.GetTypeDefinition (handle); + if (IsEnumType (typeDef, index)) { + return (typeDef, index); + } + } + + return null; + } + + /// + /// Returns with set + /// when the managed type — or, for arrays, the element type — resolves to an + /// enum. Used to thread enum-ness from the scanner to the emitter so that + /// signatures and member refs encode the type as a valuetype. + /// + TypeRefData EnrichTypeRefWithEnumInfo (TypeRefData type) + { + if (type.IsEnum || string.IsNullOrEmpty (type.ManagedTypeName)) { + return type; + } + + return IsEnumOrEnumArray (type.ManagedTypeName, type.AssemblyName) ? type with { IsEnum = true } : type; + } + + static bool IsEnumType (TypeDefinition typeDef, AssemblyIndex index) + { + var baseType = typeDef.BaseType; + if (baseType.IsNil) { + return false; + } + + var baseFullName = baseType.Kind switch { + HandleKind.TypeReference => MetadataTypeNameResolver.GetTypeFromReference (index.Reader, (TypeReferenceHandle) baseType, rawTypeKind: 0), + HandleKind.TypeDefinition => MetadataTypeNameResolver.GetTypeFromDefinition (index.Reader, (TypeDefinitionHandle) baseType, rawTypeKind: 0), + _ => null, + }; + + return baseFullName == "System.Enum"; + } + + static string GetEnumUnderlyingPrimitiveDescriptor (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = index.Reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) { + continue; + } + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return TryGetPrimitiveJniDescriptor (sig) ?? "I"; + } + + return "I"; + } + /// /// Walks the base type hierarchy collecting constructors that have [Register] attributes. /// Stops after the first base type with DoNotGenerateAcw=true (matching legacy CecilImporter). @@ -776,8 +920,11 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, continue; } - // Found a matching base method — check if it has [Register] - if (TryGetMethodRegisterInfo (baseMethodDef, baseIndex, out var registerInfo, out _) && registerInfo is not null) { + // Found a matching base method — check if it has [Register]. + // [Export] / [ExportField] are AttributeUsage(Inherited=false), so a + // derived override must NOT inherit a base [Export] registration — + // only [Register]-driven entries propagate through inheritance. + if (TryGetMethodRegisterInfo (baseMethodDef, baseIndex, out var registerInfo, out var exportInfo) && registerInfo is not null && exportInfo is null) { return (registerInfo, baseTypeName, baseAssemblyName); } } @@ -881,7 +1028,7 @@ static bool HaveIdenticalParameterTypes (MethodDefinition method1, MethodDefinit return true; } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null, bool isInterfaceImplementation = false) + void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null, bool isInterfaceImplementation = false) { // Skip methods that are just the JNI name (type-level [Register]) if (registerInfo.Signature is null && registerInfo.Connector is null) { @@ -891,8 +1038,16 @@ static void AddMarshalMethod (List methods, RegisterInfo regi bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; bool isExport = exportInfo is not null; string managedName = index.Reader.GetString (methodDef.Name); + var managedSig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); string jniSignature = registerInfo.Signature ?? "()V"; + // Only decode TypeRefData signatures for [Export] methods — they need precise + // managed type + assembly metadata for direct dispatch IL generation. + var managedTypeSig = isExport + ? methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index) + : default; + var parameterKinds = exportInfo?.ParameterKinds ?? CreateDefaultExportKinds (managedSig.ParameterTypes.Length); + string declaringTypeName = ""; string declaringAssemblyName = ""; ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName); @@ -905,6 +1060,14 @@ static void AddMarshalMethod (List methods, RegisterInfo regi DeclaringTypeName = declaringTypeName, DeclaringAssemblyName = declaringAssemblyName, NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor), + ManagedParameterTypes = isExport ? new List (managedTypeSig.ParameterTypes.Select (EnrichTypeRefWithEnumInfo)) : [], + ManagedParameterExportKinds = parameterKinds, + ManagedReturnType = isExport ? EnrichTypeRefWithEnumInfo (managedTypeSig.ReturnType) : new TypeRefData { + ManagedTypeName = managedSig.ReturnType, + AssemblyName = "System.Runtime", + }, + ManagedReturnExportKind = exportInfo?.ReturnKind ?? ExportParameterKindInfo.Unspecified, + IsStatic = (methodDef.Attributes & MethodAttributes.Static) == MethodAttributes.Static, IsConstructor = isConstructor, IsExport = isExport, IsInterfaceImplementation = isInterfaceImplementation, @@ -1048,6 +1211,21 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, thrownNames.Add (s); } } + } else if (named.Name == "Throws" && named.Value is ImmutableArray> throwsTypes) { + // Throws is `Type[]` in source, but the metadata blob serializes each + // `typeof(X)` as a string (assembly-qualified type name) routed through + // our CustomAttributeTypeProvider's GetTypeFromSerializedName. Resolve + // each to its [Register]-driven JNI internal name so the runtime can + // emit `throws` clauses on the generated Java method. + thrownNames ??= new List (throwsTypes.Length); + foreach (var item in throwsTypes) { + if (item.Value is string aqn) { + var jni = ResolveTypeOfArgumentToJniName (aqn); + if (jni is not null) { + thrownNames.Add (jni); + } + } + } } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { superArguments = superArgs; } @@ -1059,24 +1237,100 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, 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); + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + var (parameterKinds, returnKind) = GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length); + var jniSig = BuildJniSignatureFromManaged (sig, parameterKinds, returnKind); return ( new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + new ExportInfo { + ThrownNames = thrownNames, + SuperArgumentsString = superArguments, + ParameterKinds = parameterKinds, + ReturnKind = returnKind, + } ); } - string BuildJniSignatureFromManaged (MethodSignature sig) + static List CreateDefaultExportKinds (int parameterCount) + { + var kinds = new List (parameterCount); + for (int i = 0; i < parameterCount; i++) { + kinds.Add (ExportParameterKindInfo.Unspecified); + } + return kinds; + } + + static (List parameterKinds, ExportParameterKindInfo returnKind) GetExportParameterKinds (MethodDefinition methodDef, AssemblyIndex index, int parameterCount) + { + var parameterKinds = CreateDefaultExportKinds (parameterCount); + var returnKind = ExportParameterKindInfo.Unspecified; + + foreach (var parameterHandle in methodDef.GetParameters ()) { + var parameter = index.Reader.GetParameter (parameterHandle); + var kind = GetExportParameterKind (parameter, index); + if (kind == ExportParameterKindInfo.Unspecified) { + continue; + } + + if (parameter.SequenceNumber == 0) { + returnKind = kind; + } else { + int parameterIndex = parameter.SequenceNumber - 1; + if (parameterIndex >= 0 && parameterIndex < parameterKinds.Count) { + parameterKinds [parameterIndex] = kind; + } + } + } + + return (parameterKinds, returnKind); + } + + static ExportParameterKindInfo GetExportParameterKind (Parameter parameter, AssemblyIndex index) + { + foreach (var caHandle in parameter.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + if (attrName != "ExportParameterAttribute") { + continue; + } + + var value = index.DecodeAttribute (ca); + if (value.FixedArguments.Length > 0 && TryConvertExportParameterKind (value.FixedArguments [0].Value, out var ctorKind)) { + return ctorKind; + } + + foreach (var named in value.NamedArguments) { + if (named.Name == "Kind" && TryConvertExportParameterKind (named.Value, out var namedKind)) { + return namedKind; + } + } + } + + return ExportParameterKindInfo.Unspecified; + } + + static bool TryConvertExportParameterKind (object? value, out ExportParameterKindInfo kind) + { + if (value is int i && Enum.IsDefined (typeof (ExportParameterKindInfo), i)) { + kind = (ExportParameterKindInfo) i; + return true; + } + + kind = ExportParameterKindInfo.Unspecified; + return false; + } + + string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnlyList parameterKinds, ExportParameterKindInfo returnKind) { var sb = new System.Text.StringBuilder (); sb.Append ('('); - foreach (var param in sig.ParameterTypes) { - sb.Append (ManagedTypeToJniDescriptor (param)); + for (int i = 0; i < sig.ParameterTypes.Length; i++) { + var exportKind = i < parameterKinds.Count ? parameterKinds [i] : ExportParameterKindInfo.Unspecified; + sb.Append (ManagedTypeToJniDescriptor (sig.ParameterTypes [i], exportKind)); } sb.Append (')'); - sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType, returnKind)); return sb.ToString (); } @@ -1088,8 +1342,8 @@ string BuildJniSignatureFromManaged (MethodSignature sig) (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var managedName = index.Reader.GetString (methodDef.Name); - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var jniSig = BuildJniSignatureFromManaged (sig); + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + var jniSig = BuildJniSignatureFromManaged (sig, CreateDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); return ( new RegisterInfo { JniName = managedName, Signature = jniSig, Connector = "__export__", DoNotGenerateAcw = false }, @@ -1102,23 +1356,56 @@ string BuildJniSignatureFromManaged (MethodSignature sig) /// via their [Register] attribute, falling back to "Ljava/lang/Object;" only /// for types that cannot be resolved (used by [Export] signature computation). /// - string ManagedTypeToJniDescriptor (string managedType) + string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindInfo exportKind = ExportParameterKindInfo.Unspecified) { - var primitive = TryGetPrimitiveJniDescriptor (managedType); + if (exportKind != ExportParameterKindInfo.Unspecified) { + return exportKind switch { + ExportParameterKindInfo.InputStream => "Ljava/io/InputStream;", + ExportParameterKindInfo.OutputStream => "Ljava/io/OutputStream;", + ExportParameterKindInfo.XmlPullParser => "Lorg/xmlpull/v1/XmlPullParser;", + ExportParameterKindInfo.XmlResourceParser => "Landroid/content/res/XmlResourceParser;", + _ => "Ljava/lang/Object;", + }; + } + + var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); if (primitive is not null) { return primitive; } - if (managedType.EndsWith ("[]")) { - return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + if (managedType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + return $"[{ManagedTypeToJniDescriptor (managedType with { ManagedTypeName = managedType.ManagedTypeName.Substring (0, managedType.ManagedTypeName.Length - 2) })}"; } // Try to resolve as a Java peer type with [Register] - var resolved = TryResolveJniObjectDescriptor (managedType); + var resolved = TryResolveJniObjectDescriptor (managedType.ManagedTypeName); if (resolved is not null) { return resolved; } + // Well-known interface types that legacy CallbackCode mapped explicitly + // to their canonical Java type. ICharSequence is in Mono.Android but is + // not annotated with [Register]; the non-generic collection interfaces + // live in System.Collections (no Java peer at all) and are wrapped at + // runtime by JavaList/JavaDictionary/JavaCollection. + var wellKnown = managedType.ManagedTypeName switch { + "Java.Lang.ICharSequence" => "Ljava/lang/CharSequence;", + "System.Collections.IList" => "Ljava/util/List;", + "System.Collections.IDictionary" => "Ljava/util/Map;", + "System.Collections.ICollection" => "Ljava/util/Collection;", + _ => null, + }; + if (wellKnown is not null) { + return wellKnown; + } + + // Enum parameters use their underlying primitive JNI ABI (matches legacy + // CallbackCode behavior). + var enumDescriptor = TryResolveEnumUnderlyingDescriptor (managedType.ManagedTypeName, managedType.AssemblyName); + if (enumDescriptor is not null) { + return enumDescriptor; + } + return "Ljava/lang/Object;"; } @@ -1541,7 +1828,7 @@ static string ExtractShortName (string fullName) return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } - static List BuildJavaConstructors (List marshalMethods) + static List BuildJavaConstructors (List marshalMethods, TypeDefinition typeDef, AssemblyIndex index) { var ctors = new List (); int ctorIndex = 0; @@ -1549,16 +1836,63 @@ static List BuildJavaConstructors (List if (!mm.IsConstructor) { continue; } + // Try to find a managed ctor whose signature matches the JNI ctor. + // Currently the trimmable user-ctor UCO codegen only supports ctors whose + // JNI args are all object references; primitive args fall back to the + // legacy activation-ctor `(IntPtr, JniHandleOwnership)` path. + var managedParams = TryFindMatchingManagedCtorParams (typeDef, mm.JniSignature, index); ctors.Add (new JavaConstructorInfo { JniSignature = mm.JniSignature, ConstructorIndex = ctorIndex, SuperArgumentsString = mm.SuperArgumentsString, + HasMatchingManagedCtor = managedParams != null, + ManagedParameterTypes = managedParams ?? [], }); ctorIndex++; } return ctors; } + /// + /// Attempts to find a managed instance constructor on + /// whose arity matches the supplied JNI signature, and returns its managed + /// parameter types. Returns when no constructor of the + /// requested arity exists. Type compatibility between the JNI param kinds and + /// the managed parameter types is not verified — the JCW marshal method is + /// the source of truth for what the Java side will pass. + /// + static IReadOnlyList? TryFindMatchingManagedCtorParams (TypeDefinition typeDef, string jniSignature, AssemblyIndex index) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (jniSignature); + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if ((methodDef.Attributes & MethodAttributes.Static) != 0) { + continue; + } + var name = index.Reader.GetString (methodDef.Name); + if (name != ".ctor") { + continue; + } + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: index); + if (sig.ParameterTypes.Length != jniParams.Count) { + continue; + } + // Skip ctors whose managed parameter signatures are not supported by the + // trimmable [Export]-style argument marshaller (generic instantiations, + // by-ref, pointers). Returning null here makes EmitUcoConstructor fall + // back to the legacy `(IntPtr, JniHandleOwnership)` activation ctor, + // which matches the legacy LLVM-IR behaviour for these shapes. + foreach (var p in sig.ParameterTypes) { + var paramTypeName = p.ManagedTypeName; + if (paramTypeName.IndexOf ('<') >= 0 || paramTypeName.EndsWith ("&", StringComparison.Ordinal) || paramTypeName.EndsWith ("*", StringComparison.Ordinal)) { + return null; + } + } + return [.. sig.ParameterTypes]; + } + return null; + } + /// /// Checks a single method for [ExportField] and adds a JavaFieldInfo if found. /// Called inline during Pass 1 to avoid a separate iteration. @@ -1584,8 +1918,8 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List signature) => "delegate*"; } + +sealed class TypeRefSignatureTypeProvider : ISignatureTypeProvider +{ + public static readonly TypeRefSignatureTypeProvider Instance = new (); + + public TypeRefData GetPrimitiveType (PrimitiveTypeCode typeCode) => new () { + ManagedTypeName = SignatureTypeProvider.Instance.GetPrimitiveType (typeCode), + AssemblyName = "System.Runtime", + }; + + public TypeRefData GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeRefFromDefinition (reader, handle, reader.GetString (reader.GetAssemblyDefinition ().Name), rawTypeKind); + + public TypeRefData GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeRefFromReference (reader, handle, reader.GetString (reader.GetAssemblyDefinition ().Name), rawTypeKind); + + public TypeRefData GetTypeFromSpecification (MetadataReader reader, AssemblyIndex genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var typeSpec = reader.GetTypeSpecification (handle); + return typeSpec.DecodeSignature (this, genericContext); + } + + public TypeRefData GetSZArrayType (TypeRefData elementType) => elementType with { + ManagedTypeName = $"{elementType.ManagedTypeName}[]", + }; + + public TypeRefData GetArrayType (TypeRefData elementType, ArrayShape shape) => elementType with { + ManagedTypeName = $"{elementType.ManagedTypeName}[{new string (',', shape.Rank - 1)}]", + }; + + public TypeRefData GetByReferenceType (TypeRefData elementType) => elementType with { + ManagedTypeName = $"{elementType.ManagedTypeName}&", + }; + + public TypeRefData GetPointerType (TypeRefData elementType) => elementType with { + ManagedTypeName = $"{elementType.ManagedTypeName}*", + }; + + public TypeRefData GetPinnedType (TypeRefData elementType) => elementType; + public TypeRefData GetModifiedType (TypeRefData modifier, TypeRefData unmodifiedType, bool isRequired) => unmodifiedType; + + public TypeRefData GetGenericInstantiation (TypeRefData genericType, ImmutableArray typeArguments) + { + return genericType with { + ManagedTypeName = $"{genericType.ManagedTypeName}<{string.Join (",", typeArguments.Select (t => t.ManagedTypeName))}>", + }; + } + + public TypeRefData GetGenericTypeParameter (AssemblyIndex genericContext, int index) => new () { + ManagedTypeName = $"!{index}", + AssemblyName = genericContext.AssemblyName, + }; + + public TypeRefData GetGenericMethodParameter (AssemblyIndex genericContext, int index) => new () { + ManagedTypeName = $"!!{index}", + AssemblyName = genericContext.AssemblyName, + }; + + public TypeRefData GetFunctionPointerType (MethodSignature signature) => new () { + ManagedTypeName = "delegate*", + AssemblyName = "System.Runtime", + }; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 090075d73bf..2c4f47c83c3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -59,7 +59,8 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } - RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + var preparedManifest = PrepareManifestForRooting (manifestTemplate, manifestConfig); + RootManifestReferencedTypes (allPeers, preparedManifest); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); @@ -76,7 +77,7 @@ public TrimmableTypeMapResult Execute ( } var manifest = manifestConfig is not null - ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) + ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, preparedManifest) : null; return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); @@ -284,8 +285,7 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen XName attName = androidNs + "name"; var packageName = (string?) root.Attribute ("package") ?? ""; - var componentNames = new HashSet (StringComparer.Ordinal); - var deferredRegistrationNames = new HashSet (StringComparer.Ordinal); + var componentEntries = new List<(string Name, bool DeferredRegistration, XElement Element)> (); foreach (var element in root.Descendants ()) { switch (element.Name.LocalName) { case "application": @@ -297,17 +297,13 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen var name = (string?) element.Attribute (attName); if (name is not null) { var resolvedName = ManifestNameResolver.Resolve (name, packageName); - componentNames.Add (resolvedName); - - if (element.Name.LocalName is "application" or "instrumentation") { - deferredRegistrationNames.Add (resolvedName); - } + componentEntries.Add ((resolvedName, element.Name.LocalName is "application" or "instrumentation", element)); } break; } } - if (componentNames.Count == 0) { + if (componentEntries.Count == 0) { return; } @@ -321,13 +317,20 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } - foreach (var name in componentNames) { + foreach (var (name, deferredRegistration, element) in componentEntries) { if (peersByDotName.TryGetValue (name, out var peers)) { - foreach (var peer in peers) { - if (deferredRegistrationNames.Contains (name)) { + string actualJavaName = JniSignatureHelper.JniNameToJavaName (peers [0].JavaName); + if (!string.Equals ((string?) element.Attribute (attName), actualJavaName, StringComparison.Ordinal)) { + element.SetAttributeValue (attName, actualJavaName); + } + + if (deferredRegistration) { + foreach (var peer in peers) { peer.CannotRegisterInStaticConstructor = true; } + } + foreach (var peer in peers) { if (!peer.IsUnconditional) { peer.IsUnconditional = true; logger.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index e499a05dcfa..b5e07fa744e 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -33,6 +33,11 @@ Value="true" Trim="true" /> + + + true + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 92aab238757..adc484dad40 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -179,8 +179,17 @@ public override bool RunTask () } }; + static bool ShouldSkipAssembly (ITaskItem assembly) + { + return assembly.GetMetadataOrDefault ("AndroidSkipAddToPackage", false); + } + if (SatelliteAssemblies != null) { foreach (ITaskItem assembly in SatelliteAssemblies) { + if (ShouldSkipAssembly (assembly)) { + continue; + } + updateNameWidth (assembly); updateAssemblyCount (assembly); } @@ -190,6 +199,10 @@ public override bool RunTask () int jnienv_initialize_method_token = -1; int jnienv_registerjninatives_method_token = -1; foreach (var assembly in ResolvedAssemblies) { + if (ShouldSkipAssembly (assembly)) { + continue; + } + updateNameWidth (assembly); updateAssemblyCount (assembly); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 563e987ee41..16ee6eb55db 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -1,15 +1,15 @@ using System; +using System.Collections.Generic; using System.IO; -using NUnit.Framework; -using Xamarin.ProjectTools; using System.Linq; using System.Text; -using System.Collections.Generic; using System.Xml.Linq; -using Xamarin.Tools.Zip; +using Microsoft.Build.Framework; +using NUnit.Framework; using Xamarin.Android.Tasks; using Xamarin.Android.Tools; -using Microsoft.Build.Framework; +using Xamarin.ProjectTools; +using Xamarin.Tools.Zip; namespace Xamarin.Android.Build.Tests { @@ -468,6 +468,49 @@ public void CheckMetadataSkipItemsAreProcessedCorrectly ([Values] AndroidRuntime } } + [Test] + [NonParallelizable] + public void MonoAndroidExportIsNotPackagedWithTrimmableTypeMap () + { + const AndroidRuntime runtime = AndroidRuntime.CoreCLR; + const bool isRelease = false; + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + proj.Sources.Add (new BuildItem.Source ("ContainsExportedMethods.cs") { + TextContent = () => @"using System; +using Java.Interop; + +namespace UnnamedProject { + class ContainsExportedMethods : Java.Lang.Object { + [Export] + public void Exported () + { + Console.WriteLine (""# ExportedCallbackInvoked""); + } + } +}" + }); + + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "build failed"); + + var apk = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, $"{proj.PackageName}-Signed.apk"); + var helper = new ArchiveAssemblyHelper (apk, useAssemblyStores: true); + var contents = helper.ListArchiveContents (); + + Assert.IsFalse ( + contents.Any (e => Path.GetFileName (e).Equals ("Mono.Android.Export.dll", StringComparison.Ordinal)), + $"APK file `{apk}` should not contain Mono.Android.Export.dll when the trimmable type map is enabled."); + } + } + [Test] public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [Values] AndroidRuntime runtime) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GeneratePackageManagerJavaTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GeneratePackageManagerJavaTests.cs index 9591b26a7e8..3d9e9cbfd1c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GeneratePackageManagerJavaTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GeneratePackageManagerJavaTests.cs @@ -1,15 +1,15 @@ #nullable disable -using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using Xamarin.Android.Tasks; +using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.Android.Build.Tasks; +using NUnit.Framework; +using Xamarin.Android.Tasks; using Xamarin.ProjectTools; namespace Xamarin.Android.Build.Tests @@ -104,5 +104,40 @@ public void CheckPackageManagerAssemblyOrder (string[] resolvedUserAssemblies, s txt = File.ReadAllText (Path.Combine (path, "env", "environment.x86.ll")); StringAssert.Contains ("XXXX", txt, "environment.x86.ll should contain 'XXXX'"); } + + [Test] + public void GenerateNativeApplicationConfigSkipsAssembliesExcludedFromPackage () + { + var path = Path.Combine (Root, "temp", nameof (GenerateNativeApplicationConfigSkipsAssembliesExcludedFromPackage)); + Directory.CreateDirectory (path); + + File.WriteAllText (Path.Combine (path, "myenv.txt"), @"MYENV=ZZZZ"); + + var metadata = new Dictionary (StringComparer.OrdinalIgnoreCase) { + { "Abi", "arm64-v8a" }, + }; + var skipped = new Dictionary (metadata, StringComparer.OrdinalIgnoreCase) { + { "AndroidSkipAddToPackage", "true" }, + }; + + var configTask = new GenerateNativeApplicationConfigSources { + BuildEngine = new MockBuildEngine (TestContext.Out), + ResolvedAssemblies = [ + new TaskItem ("linked/HelloAndroid.dll", metadata), + new TaskItem ("linked/Mono.Android.Export.dll", skipped), + ], + EnvironmentOutputDirectory = Path.Combine (path, "env"), + SupportedAbis = ["arm64-v8a"], + AndroidPackageName = "com.microsoft.net6.helloandroid", + EnablePreloadAssembliesDefault = false, + Environments = [new TaskItem (Path.Combine (path, "myenv.txt"))], + }; + + Assert.IsTrue (configTask.Execute (), "GenerateNativeApplicationConfigSources task should have executed."); + + var txt = File.ReadAllText (Path.Combine (path, "env", "environment.arm64-v8a.ll")); + StringAssert.Contains ("ZZZZ", txt, "environment.arm64-v8a.ll should contain the custom environment value."); + StringAssert.DoesNotContain ("Mono.Android.Export.dll", txt, "environment.arm64-v8a.ll should not list assemblies excluded from packaging."); + } } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs index 5014fe28a4c..118e2f650e5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -74,13 +74,28 @@ static string[]? AllUserTypesAssemblyPaths { static string NormalizeCrc64 (string javaName) { - if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { - int slash = javaName.IndexOf ('/'); - if (slash > 0) { - return "crc64.../" + javaName.Substring (slash + 1); - } - } - return javaName; + // Normalize crc64 hashes anywhere in the string — both the outer type + // name (JavaName) and any embedded type references inside JNI method + // signatures. Legacy and new scanners hash with different inputs (legacy + // hashes assembly+namespace, new scanner hashes namespace:assembly), so + // the absolute hash differs but should be deterministic per side. + return System.Text.RegularExpressions.Regex.Replace (javaName, @"crc64[0-9a-f]{16}", "crc64..."); + } + + static List NormalizeMethodGroups (List groups) + { + return groups + .Select (g => new TypeMethodGroup ( + g.ManagedName, + g.Methods + .Select (m => new MethodEntry ( + NormalizeCrc64 (m.JniName), + NormalizeCrc64 (m.JniSignature), + m.Connector is null ? null : NormalizeCrc64 (m.Connector) + )) + .ToList () + )) + .ToList (); } void AssertTypeMapMatch (List legacy, List newEntries) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index fcfe51cb1df..2db61b9ca8d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -132,9 +132,9 @@ public void ExactMarshalMethods_UserTypesFixture () var (_, newMethods) = ScannerRunner.RunNew (paths); var legacyNormalized = legacyMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); var newNormalized = newMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); AssertNoDiffs ("MISSING from new scanner", result.Missing); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs new file mode 100644 index 00000000000..964aabab5b0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerExportShapesTests.cs @@ -0,0 +1,293 @@ +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +/// +/// Integration coverage for the trimmable scanner's [Export] handling on +/// shapes that the legacy JCW emitter (CecilImporter.GetJniSignature) cannot +/// encode: enum-typed parameters / returns, ICharSequence, and non-generic +/// IList / IDictionary / ICollection. ScannerComparisonTests.RunLegacy falls +/// back to direct [Register] extraction for these types (yields no entries), +/// so legacy↔new comparison is intentionally skipped — these tests assert +/// the new scanner produces the right JNI signatures end-to-end. +/// +public class ScannerExportShapesTests +{ + static string UserTypesFixturePath { + get { + var testDir = Path.GetDirectoryName (typeof (ScannerExportShapesTests).Assembly.Location) + ?? throw new System.InvalidOperationException ("Could not determine test assembly directory."); + var path = Path.Combine (testDir, "UserTypesFixture.dll"); + Assert.True (File.Exists (path), $"UserTypesFixture.dll not found at '{path}'."); + return path; + } + } + + static MarshalMethodInfo[] GetMarshalMethods (string javaName) + { + var fixturePath = UserTypesFixturePath; + var dir = Path.GetDirectoryName (fixturePath)!; + + var paths = new System.Collections.Generic.List { fixturePath }; + var monoAndroid = Path.Combine (dir, "Mono.Android.dll"); + var javaInterop = Path.Combine (dir, "Java.Interop.dll"); + if (File.Exists (monoAndroid)) + paths.Add (monoAndroid); + if (File.Exists (javaInterop)) + paths.Add (javaInterop); + + using var scanner = new JavaPeerScanner (); + var peReaders = new System.Collections.Generic.List (); + try { + var assemblies = new System.Collections.Generic.List<(string Name, PEReader Reader)> (); + foreach (var p in paths) { + var pe = new PEReader (File.OpenRead (p)); + peReaders.Add (pe); + var md = pe.GetMetadataReader (); + assemblies.Add ((md.GetString (md.GetAssemblyDefinition ().Name), pe)); + } + + var peers = scanner.Scan (assemblies); + var peer = peers.FirstOrDefault (p => p.ManagedTypeName.EndsWith (javaName)); + Assert.NotNull (peer); + return peer!.MarshalMethods.ToArray (); + } finally { + foreach (var pe in peReaders) + pe.Dispose (); + } + } + + static void AssertHasExport (MarshalMethodInfo[] methods, string jniName, string jniSignature) + { + var match = methods.FirstOrDefault (m => m.JniName == jniName && m.JniSignature == jniSignature); + Assert.True (match != null, + $"Expected [Export] marshal method '{jniName}{jniSignature}' not found. " + + $"Discovered: {string.Join (", ", methods.Select (m => m.JniName + m.JniSignature))}"); + // [Export] methods carry no Connector — legacy uses __export__ at runtime, + // trimmable wires registration via UCO fnptr. [ExportField] methods do + // surface the "__export__" connector by design (matches legacy + // CecilImporter behaviour), so accept that case too. + Assert.True (match!.Connector is null || match.Connector == "__export__", + $"Unexpected connector '{match.Connector}' on {jniName}{jniSignature}."); + } + + [Fact] + public void EnumParam_AndReturn_MarshalAsUnderlyingPrimitive () + { + var methods = GetMarshalMethods ("ExportEnumShapes"); + + // SampleEnum (Int32) → I + AssertHasExport (methods, "echoEnum", "(I)I"); + // SampleByteEnum → B + AssertHasExport (methods, "echoByteEnum", "(B)B"); + // SampleLongEnum → J + AssertHasExport (methods, "echoLongEnum", "(J)J"); + } + + [Fact] + public void ICharSequenceParam_AndReturn_MarshalsAsCharSequence () + { + var methods = GetMarshalMethods ("ExportCharSequenceShapes"); + AssertHasExport (methods, "echoCharSequence", "(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;"); + } + + [Fact] + public void NonGenericCollections_MarshalAsExpectedJavaTypes () + { + var methods = GetMarshalMethods ("ExportCollectionShapes"); + + AssertHasExport (methods, "echoList", "(Ljava/util/List;)Ljava/util/List;"); + AssertHasExport (methods, "echoMap", "(Ljava/util/Map;)Ljava/util/Map;"); + AssertHasExport (methods, "echoCollection", "(Ljava/util/Collection;)Ljava/util/Collection;"); + } + + [Fact] + public void ExportField_RegistersGetterAsMarshalMethod () + { + var methods = GetMarshalMethods ("ExportFieldShapes"); + + // [ExportField] uses the managed method name as the JNI method name + // (legacy Mono.Android.Export does the same thing). The signatures + // below match the underlying CLR method shape. + // User-peer return type uses a CRC64-based package name; assert by prefix + // so the test isn't tied to the exact CRC64 hash of the assembly. + var getInstance = System.Array.Find (methods, m => m.JniName == "GetInstance"); + Assert.NotNull (getInstance); + Assert.EndsWith ("/ExportFieldShapes;", getInstance!.JniSignature); + Assert.StartsWith ("()L", getInstance.JniSignature); + Assert.DoesNotContain ("Ljava/lang/Object;", getInstance.JniSignature); + + AssertHasExport (methods, "GetValue", "()Ljava/lang/String;"); + AssertHasExport (methods, "GetCount", "()I"); + } + + [Fact] + public void ExportParameter_OverridesJavaTypeForStreamsAndXml () + { + var methods = GetMarshalMethods ("ExportParameterShapes"); + + // Stream → InputStream / OutputStream + AssertHasExport (methods, "openStream", "(Ljava/io/InputStream;)I"); + AssertHasExport (methods, "wrapStream", "(Ljava/io/OutputStream;)Ljava/io/OutputStream;"); + // XmlReader → XmlPullParser / XmlResourceParser + AssertHasExport (methods, "readXml", "(Lorg/xmlpull/v1/XmlPullParser;)Lorg/xmlpull/v1/XmlPullParser;"); + AssertHasExport (methods, "readResourceXml", "(Landroid/content/res/XmlResourceParser;)Landroid/content/res/XmlResourceParser;"); + } + + // === Phase A: dispatch & declaration shapes === + + [Fact] + public void StaticExport_RegistersStaticDispatch () + { + var methods = GetMarshalMethods ("StaticExportShapes"); + AssertHasExport (methods, "compute", "(I)I"); + AssertHasExport (methods, "hello", "()Ljava/lang/String;"); + } + + [Fact] + public void Export_WithThrowsClause_SurfacesDeclaredExceptions () + { + var methods = GetMarshalMethods ("ExportThrowsShapes"); + + var ioCall = System.Array.Find (methods, m => m.JniName == "ioCall"); + Assert.NotNull (ioCall); + Assert.NotNull (ioCall!.ThrownNames); + Assert.Contains ("java/io/IOException", ioCall.ThrownNames!); + + var multiThrow = System.Array.Find (methods, m => m.JniName == "multiThrow"); + Assert.NotNull (multiThrow); + Assert.NotNull (multiThrow!.ThrownNames); + Assert.Contains ("java/io/IOException", multiThrow.ThrownNames!); + Assert.Contains ("java/lang/IllegalStateException", multiThrow.ThrownNames!); + } + + [Fact] + public void MixedRegisterAndExport_BothPathsSurface () + { + var methods = GetMarshalMethods ("MixedRegisterAndExport"); + + // [Register]-driven Activity override carries a connector + var onCreate = System.Array.Find (methods, m => m.JniName == "onCreate"); + Assert.NotNull (onCreate); + Assert.False (onCreate!.Connector is null or "__export__", + $"OnCreate override should have a real Get*Handler connector, got '{onCreate.Connector}'."); + + // [Export]-driven new methods carry no connector (or "__export__") + AssertHasExport (methods, "doWork", "()V"); + AssertHasExport (methods, "compute", "(I)I"); + } + + [Fact] + public void VirtualExport_TopMostDeclarationRegisters () + { + var baseMethods = GetMarshalMethods ("VirtualExportBase"); + AssertHasExport (baseMethods, "ping", "()I"); + + var derivedMethods = GetMarshalMethods ("VirtualExportDerived"); + // Derived class doesn't re-declare [Export]; only the base [Export] applies, + // so the derived peer should NOT add a duplicate marshal-method entry of its + // own. (Legacy CecilImporter walks up the inheritance chain and registers + // the [Export] on the topmost declaring type.) + var derivedPing = System.Array.FindAll (derivedMethods, m => m.JniName == "ping"); + Assert.True (derivedPing.Length <= 1, + $"Derived peer should not duplicate base's [Export] entry, found {derivedPing.Length}."); + } + + [Fact] + public void Export_CustomJniName_NotIdentityMappedFromMethodName () + { + var methods = GetMarshalMethods ("ExportRenameShapes"); + + // JNI name comes from [Export("javaSideName")], not from "CSharpSideName". + Assert.Contains (methods, m => m.JniName == "javaSideName" && m.JniSignature == "()V"); + Assert.DoesNotContain (methods, m => m.JniName == "CSharpSideName"); + } + + // === Phase B: edge marshalling === + + [Fact] + public void Export_JavaLangObjectExplicitly_KeepsObjectDescriptor () + { + var methods = GetMarshalMethods ("ExportObjectShapes"); + AssertHasExport (methods, "any", "(Ljava/lang/Object;)Ljava/lang/Object;"); + } + + [Fact] + public void Export_ArrayOfUserPeerType_RecursesUserPeerResolver () + { + var methods = GetMarshalMethods ("ExportUserPeerArrayShapes"); + var echoArr = System.Array.Find (methods, m => m.JniName == "echoArr"); + Assert.NotNull (echoArr); + // Both parameter and return are arrays of the user-peer UserPeerForArray. + // CRC64 hash is environment-dependent; assert by suffix. + Assert.Matches (@"^\(\[Lcrc64[0-9a-f]{16}/UserPeerForArray;\)\[Lcrc64[0-9a-f]{16}/UserPeerForArray;$", echoArr!.JniSignature); + } + + [Fact] + public void Export_ProtectedAndPrivateVisibility_BothSurface () + { + var methods = GetMarshalMethods ("ExportVisibilityShapes"); + AssertHasExport (methods, "doProtected", "()V"); + AssertHasExport (methods, "doPrivate", "()V"); + } + + [Fact] + public void ExportField_ReturningPrimitive () + { + var methods = GetMarshalMethods ("ExportFieldPrimitiveShapes"); + // [ExportField] uses the managed method name as the JNI name (not the field name). + var getMaxValue = System.Array.Find (methods, m => m.JniName == "GetMaxValue"); + Assert.NotNull (getMaxValue); + Assert.Equal ("()I", getMaxValue!.JniSignature); + Assert.Equal ("__export__", getMaxValue.Connector); + } + + [Fact] + public void Export_OverloadsWithSameJavaName_RegisterDistinctly () + { + var methods = GetMarshalMethods ("ExportOverloadShapes"); + var calls = System.Array.FindAll (methods, m => m.JniName == "call"); + Assert.Equal (2, calls.Length); + Assert.Contains (calls, m => m.JniSignature == "(I)V"); + Assert.Contains (calls, m => m.JniSignature == "(Ljava/lang/String;)V"); + } + + // === Phase C: robustness === + + [Fact] + public void Export_GenericMethod_ScannerDoesNotCrash () + { + // Generic methods aren't legal Java targets for [Export], but the + // scanner must not crash. Either the method is skipped or it surfaces + // with some defined fallback — assert only that we get a non-null + // peer back without throwing. + var methods = GetMarshalMethods ("ExportGenericShapes"); + Assert.NotNull (methods); + } + + [Fact] + public void Export_OnRegisterOverride_RegisterPathWins () + { + var methods = GetMarshalMethods ("ExportOverridingRegisterShape"); + + // The Activity.OnCreate override carries [Register]-driven dispatch + // (real Get*Handler connector). Putting [Export] on top of an override + // of a [Register]'d base means BOTH entries are registered: the + // [Register]-driven override (so Activity.onCreate dispatch still works) + // AND the [Export]-driven new method (so Java callers can call the + // renamed method). Matches legacy CecilImporter behaviour. + var onCreate = System.Array.Find (methods, m => m.JniName == "onCreate"); + Assert.NotNull (onCreate); + Assert.False (onCreate!.Connector is null or "__export__", + $"OnCreate override should keep its [Register]-driven Get*Handler connector, got '{onCreate.Connector}'."); + + var onCreateExport = System.Array.Find (methods, m => m.JniName == "onCreateExport"); + Assert.NotNull (onCreateExport); + Assert.True (onCreateExport!.Connector is null or "__export__", + $"[Export]-driven entry should have no real connector, got '{onCreateExport.Connector}'."); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index d01d02746de..fcd8a0a477e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -178,7 +178,19 @@ static List ExtractMethodRegistrations (CecilTypeDefinition typeDef return ExtractDirectRegisterAttributes (typeDef); } - var wrapper = CecilImporter.CreateType (typeDef, cache); + Java.Interop.Tools.JavaCallableWrappers.CallableWrapperMembers.CallableWrapperType wrapper; + try { + wrapper = CecilImporter.CreateType (typeDef, cache); + } catch (ArgumentNullException) { + // Legacy JCW emitter (CecilImporter.GetJniSignature) cannot encode + // certain [Export] parameter / return types (enum, ICharSequence, + // non-generic collections). The trimmable scanner handles these, + // but legacy comparison can't be performed — yield direct + // [Register] attributes so the type is still represented in the + // legacy snapshot. This is the documented JCW emitter blocker + // (covered by ScannerExportShapesTests for the new scanner). + return ExtractDirectRegisterAttributes (typeDef); + } var methods = new List (); foreach (var m in wrapper.Methods) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 75586236a8e..e186d7e27f2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -155,4 +155,214 @@ public void DoWork () { } } + + // [Export] shapes that the legacy JCW emitter (CecilImporter.GetJniSignature) + // cannot encode but that the trimmable scanner is expected to handle. These + // types are excluded from legacy↔new comparison in ScannerComparisonTests + // and validated by ScannerExportShapesTests via the new scanner only. + public enum ExportSampleEnum { Zero, One, Two } + public enum ExportSampleByteEnum : byte { Red, Green, Blue } + public enum ExportSampleLongEnum : long { Zero = 0L, Big = long.MaxValue } + + public class ExportEnumShapes : Java.Lang.Object + { + [Export ("echoEnum")] + public ExportSampleEnum EchoEnum (ExportSampleEnum value) => value; + + [Export ("echoByteEnum")] + public ExportSampleByteEnum EchoByteEnum (ExportSampleByteEnum value) => value; + + [Export ("echoLongEnum")] + public ExportSampleLongEnum EchoLongEnum (ExportSampleLongEnum value) => value; + } + + public class ExportCharSequenceShapes : Java.Lang.Object + { + [Export ("echoCharSequence")] + public Java.Lang.ICharSequence? EchoCharSequence (Java.Lang.ICharSequence? value) => value; + } + + public class ExportCollectionShapes : Java.Lang.Object + { + [Export ("echoList")] + public System.Collections.IList? EchoList (System.Collections.IList? value) => value; + + [Export ("echoMap")] + public System.Collections.IDictionary? EchoMap (System.Collections.IDictionary? value) => value; + + [Export ("echoCollection")] + public System.Collections.ICollection? EchoCollection (System.Collections.ICollection? value) => value; + } + + // [ExportField] generates a Java field whose value is produced by a getter + // method. The scanner must surface the method-level registration so the UCO + // can dispatch to the getter. + public class ExportFieldShapes : Java.Lang.Object + { + protected ExportFieldShapes (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [ExportField ("STATIC_INSTANCE")] + public static ExportFieldShapes? GetInstance () => null; + + [ExportField ("VALUE")] + public string GetValue () => ""; + + [ExportField ("COUNT")] + public int GetCount () => 0; + } + + // [ExportParameter] overrides a Stream / XmlReader's Java type without + // relying on auto-resolution. Each kind must map to its specific JNI + // descriptor (java/io/InputStream, OutputStream, org/xmlpull/v1/XmlPullParser, + // android/content/res/XmlResourceParser). + public class ExportParameterShapes : Java.Lang.Object + { + [Export ("openStream")] + public int OpenStream ([ExportParameter (ExportParameterKind.InputStream)] System.IO.Stream? stream) + => stream is null ? 0 : 1; + + [return: ExportParameter (ExportParameterKind.OutputStream)] + [Export ("wrapStream")] + public System.IO.Stream? WrapStream ([ExportParameter (ExportParameterKind.OutputStream)] System.IO.Stream? stream) + => stream; + + [return: ExportParameter (ExportParameterKind.XmlPullParser)] + [Export ("readXml")] + public System.Xml.XmlReader? ReadXml ([ExportParameter (ExportParameterKind.XmlPullParser)] System.Xml.XmlReader? reader) + => reader; + + [return: ExportParameter (ExportParameterKind.XmlResourceParser)] + [Export ("readResourceXml")] + public System.Xml.XmlReader? ReadResourceXml ([ExportParameter (ExportParameterKind.XmlResourceParser)] System.Xml.XmlReader? reader) + => reader; + } + + // === Phase A: dispatch & declaration shapes === + + // A.1: static [Export] method — different dispatch path (no `this`). + public class StaticExportShapes : Java.Lang.Object + { + [Export ("compute")] + public static int Compute (int x) => x; + + [Export ("hello")] + public static string Hello () => "hi"; + } + + // A.2: [Export(Throws = ...)] — declared exceptions in JNI signature. + public class ExportThrowsShapes : Java.Lang.Object + { + [Export ("ioCall", Throws = new [] { typeof (Java.IO.IOException) })] + public void IoCall () { } + + [Export ("multiThrow", Throws = new [] { typeof (Java.IO.IOException), typeof (Java.Lang.IllegalStateException) })] + public int MultiThrow () => 0; + } + + // A.3: Mixed [Register] overrides + new [Export] methods on the same type. + [Register ("my/app/MixedRegisterAndExport")] + public class MixedRegisterAndExport : Activity + { + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + + [Export ("doWork")] + public void DoWork () { } + + [Export ("compute")] + public int Compute (int x) => x; + } + + // A.4: [Export] on a virtual method, derived class re-declaring without [Export]. + public class VirtualExportBase : Java.Lang.Object + { + [Export ("ping")] + public virtual int Ping () => 0; + } + + public class VirtualExportDerived : VirtualExportBase + { + public override int Ping () => 1; + } + + // A.5: [Export] with explicit JNI name differing from C# method name. + public class ExportRenameShapes : Java.Lang.Object + { + [Export ("javaSideName")] + public void CSharpSideName () { } + } + + // === Phase B: edge marshalling === + + // B.1: [Export] returning Java.Lang.Object explicitly (intentional unwrapped path). + public class ExportObjectShapes : Java.Lang.Object + { + [Export ("any")] + public Java.Lang.Object? Any (Java.Lang.Object? v) => v; + } + + // B.2: array of user-peer type — exercise [] recursion through the user-peer + // JNI resolver fix from a prior commit. + public class UserPeerForArray : Java.Lang.Object + { + protected UserPeerForArray (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + public class ExportUserPeerArrayShapes : Java.Lang.Object + { + [Export ("echoArr")] + public UserPeerForArray []? EchoArr (UserPeerForArray []? a) => a; + } + + // B.3: protected/private [Export] methods — visibility shouldn't gate registration. + public class ExportVisibilityShapes : Java.Lang.Object + { + [Export ("doProtected")] + protected void DoProtected () { } + + [Export ("doPrivate")] + void DoPrivate () { } + } + + // B.4: [ExportField] returning a primitive — focused single-shape assertion. + public class ExportFieldPrimitiveShapes : Java.Lang.Object + { + protected ExportFieldPrimitiveShapes (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [ExportField ("MAX_VALUE")] + public static int GetMaxValue () => 42; + } + + // B.5: [Export] overloads with same Java name, different signatures — no dedup. + public class ExportOverloadShapes : Java.Lang.Object + { + [Export ("call")] + public void Call (int x) { } + + [Export ("call")] + public void Call (string s) { } + } + + // === Phase C: robustness === + // C.1 (property) is gated by [AttributeUsage(Method|Constructor)] — skip. + + // C.2: generic method with [Export] — scanner shouldn't crash on T. + public class ExportGenericShapes : Java.Lang.Object + { + [Export ("g")] + public T Identity (T x) => x; + } + + // C.3: override of a [Register]'d base method also marked [Export]. + // Legacy: [Register]-driven dispatch wins (with connector); [Export] is a no-op. + public class ExportOverridingRegisterShape : Activity + { + [Export ("onCreateExport")] + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 44b5bff4f85..0acc7c3794a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -91,7 +91,7 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage return MakePeerWithActivation (jniName, managedName, asmName) with { DoNotGenerateAcw = false, JavaConstructors = new List { - new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V", HasMatchingManagedCtor = true }, }, MarshalMethods = new List { new MarshalMethodInfo { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 30d81218883..81fc6457f35 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -375,6 +375,19 @@ public void Generate_ExportWithThrows_HasThrowsClause () } + public class StaticExportMethods + { + + [Fact] + public void Generate_StaticExport_HasStaticKeyword () + { + var java = GenerateFixture ("my/app/StaticExportExample"); + AssertContainsLine ("public static java.lang.String computeLabel (int p0)", java); + AssertContainsLine ("public static native java.lang.String", java); + } + + } + public class MethodReturnTypesAndParams { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c9f070dc146..a91985d7f63 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -195,6 +195,39 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); } + [Fact] + public void Execute_ManifestReferencedTypeNames_AreNormalizedInGeneratedManifest () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet (), + manifestConfig: new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "21", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider"), + manifestTemplate: manifestTemplate); + + var androidName = (string?) result.Manifest?.Document.Root? + .Element ("application")? + .Element ("activity")? + .Attribute (System.Xml.Linq.XName.Get ("name", "http://schemas.android.com/apk/res/android")); + + Assert.Equal ("my.app.SimpleActivity", androidName); + } + + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); TrimmableTypeMapGenerator CreateGenerator (List warnings) => @@ -322,6 +355,73 @@ public void PropagateDeferredRegistrationToBaseClasses_PropagatesToBaseClassesOf Assert.True (basePeer.CannotRegisterInStaticConstructor, "Base peer should have deferred registration after propagation."); } + [Fact] + public void Execute_PropagatesDeferredRegistrationToBaseClasses () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" + + + + + """); + + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet (), + manifestConfig: new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "21", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider"), + manifestTemplate: manifestTemplate); + + var derivedPeer = result.AllPeers.FirstOrDefault ( + p => p.ManagedTypeShortName == "DerivedInstrumentation"); + var basePeer = derivedPeer?.BaseJavaName is not null + ? result.AllPeers.FirstOrDefault (p => p.JavaName == derivedPeer.BaseJavaName) + : null; + + if (derivedPeer is not null && basePeer is not null) { + Assert.True (derivedPeer.CannotRegisterInStaticConstructor, + "Instrumentation type should defer registerNatives."); + Assert.True (basePeer.CannotRegisterInStaticConstructor, + "Base class of instrumentation type should also defer registerNatives."); + } + // If test fixtures don't have a matching hierarchy, the test is skipped implicitly. + } + + [Fact] + public void RootManifestReferencedTypes_RewritesManifestApplicationToActualJavaName () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "crc64123456789abc/App", CompatJniName = "android/apptests/App", + ManagedTypeName = "Android.AppTests.App", ManagedTypeNamespace = "Android.AppTests", ManagedTypeShortName = "App", + AssemblyName = "Mono.Android.NET-Tests", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + var actualName = (string?) doc.Root? + .Element ("application")? + .Attribute (System.Xml.Linq.XName.Get ("name", "http://schemas.android.com/apk/res/android")); + + Assert.Equal ("crc64123456789abc.App", actualName); + Assert.True (peers [0].IsUnconditional); + Assert.True (peers [0].CannotRegisterInStaticConstructor); + } + [Fact] public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () { @@ -400,37 +500,42 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges () } [Fact] - public void MergeCrossAssemblyAliases_CrossAssemblyDuplicate_FirstAssemblyOwns () + public void MergeCrossAssemblyAliases_RegisterTakesPrecedenceOverJniTypeSignature () { - var firstPeer = new JavaPeerInfo { - JavaName = "com/example/Duplicate", CompatJniName = "com/example/Duplicate", - ManagedTypeName = "First.Duplicate", ManagedTypeNamespace = "First", ManagedTypeShortName = "Duplicate", - AssemblyName = "A.Binding", + // Java.Interop has JavaObject with [JniTypeSignature("java/lang/Object")] + var javaInteropPeer = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Interop.JavaObject", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaObject", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, }; - var secondPeer = new JavaPeerInfo { - JavaName = "com/example/Duplicate", CompatJniName = "com/example/Duplicate", - ManagedTypeName = "Second.Duplicate", ManagedTypeNamespace = "Second", ManagedTypeShortName = "Duplicate", - AssemblyName = "B.Binding", + // Mono.Android has Java.Lang.Object with [Register("java/lang/Object")] + var monoAndroidPeer = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Lang.Object", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Object", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, }; - var uniquePeer = new JavaPeerInfo { - JavaName = "com/example/Unique", CompatJniName = "com/example/Unique", - ManagedTypeName = "Second.Unique", ManagedTypeNamespace = "Second", ManagedTypeShortName = "Unique", - AssemblyName = "B.Binding", + // Another unique peer in Java.Interop that shouldn't be moved + var otherPeer = new JavaPeerInfo { + JavaName = "java/interop/SomeHelper", CompatJniName = "java/interop/SomeHelper", + ManagedTypeName = "Java.Interop.SomeHelper", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "SomeHelper", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, }; - var allPeers = new List { firstPeer, secondPeer, uniquePeer }; + var allPeers = new List { javaInteropPeer, monoAndroidPeer, otherPeer }; var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); - var firstGroup = result.Single (g => g.AssemblyName == "A.Binding"); - Assert.Equal (2, firstGroup.Peers.Count); - Assert.Contains (firstGroup.Peers, p => p.ManagedTypeName == "First.Duplicate"); - Assert.Contains (firstGroup.Peers, p => p.ManagedTypeName == "Second.Duplicate"); + // Both java/lang/Object peers should be in the Mono.Android group ([Register] wins) + var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); + Assert.Equal (2, monoAndroidGroup.Peers.Count); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Object"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaObject"); - var secondGroup = result.Single (g => g.AssemblyName == "B.Binding"); - Assert.Single (secondGroup.Peers); - Assert.Equal ("Second.Unique", secondGroup.Peers [0].ManagedTypeName); + // Java.Interop should only have the unique peer + var javaInteropGroup = result.Single (g => g.AssemblyName == "Java.Interop"); + Assert.Single (javaInteropGroup.Peers); + Assert.Equal ("Java.Interop.SomeHelper", javaInteropGroup.Peers [0].ManagedTypeName); } [Fact] @@ -476,6 +581,189 @@ public void MergeCrossAssemblyAliases_SameAssemblyAliases_NotMoved () Assert.Equal (2, result [0].Peers.Count); } + [Fact] + public void MergeCrossAssemblyAliases_SameManagedName_DifferentAssemblies_MergedCorrectly () + { + // Reproduces the java/lang/Throwable crash: two assemblies define Java.Lang.Throwable + // with the same JNI name, plus Java.Interop.JavaException also maps to the same JNI name. + // All three should be merged into the [Register]-owning assembly's group. + var javaInteropThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var monoAndroidThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + }; + + var javaException = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; + var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); + + // All java/lang/Throwable peers should be in the Mono.Android group ([Register] wins) + var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); + Assert.Equal (3, monoAndroidGroup.Peers.Count); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Mono.Android"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Java.Interop"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaException"); + + // Java.Interop group should be empty (all peers moved to Mono.Android) + Assert.DoesNotContain (result, g => g.AssemblyName == "Java.Interop"); + } + + [Fact] + public void MergeCrossAssemblyAliases_SameManagedName_ProducesCorrectAliasGroup () + { + // End-to-end: after merging, ModelBuilder must produce a 3-way alias group + // for java/lang/Throwable with indexed entries and a single base entry, + // ensuring the runtime dictionary only sees java/lang/Throwable once. + var javaInteropThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var monoAndroidThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + }; + + var javaException = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; + var merged = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); + + // All peers should be in the Mono.Android group + Assert.Single (merged); + var group = merged [0]; + Assert.Equal ("Mono.Android", group.AssemblyName); + Assert.Equal (3, group.Peers.Count); + + // Build the model — should produce a 3-way alias group + string typeMapAssemblyName = $"_{group.AssemblyName}.TypeMap"; + var model = ModelBuilder.Build (group.Peers, typeMapAssemblyName + ".dll", typeMapAssemblyName); + + // 3 indexed entries + 1 base entry = 4 + Assert.Equal (4, model.Entries.Count); + Assert.Equal ("java/lang/Throwable[0]", model.Entries [0].JniName); + Assert.Equal ("java/lang/Throwable[1]", model.Entries [1].JniName); + Assert.Equal ("java/lang/Throwable[2]", model.Entries [2].JniName); + Assert.Equal ("java/lang/Throwable", model.Entries [3].JniName); + + // Exactly 1 alias holder + Assert.Single (model.AliasHolders); + Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); + + // The base "java/lang/Throwable" entry points to the alias holder, not a type directly + var baseEntry = model.Entries [3]; + Assert.Contains ("_Aliases", baseEntry.ProxyTypeReference); + + // 3 associations (one per peer → alias holder) + Assert.Equal (3, model.Associations.Count); + + // The bare "java/lang/Throwable" key appears exactly once — no duplicates + Assert.Single (model.Entries, e => e.JniName == "java/lang/Throwable"); + } + + [Fact] + public void RootManifestReferencedTypes_ResolvesRelativeNames () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyService", CompatJniName = "com.example.MyService", + ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Dot-relative name '.MyActivity' should resolve to com.example.MyActivity."); + Assert.True (peers [1].IsUnconditional, "Simple name 'MyService' should resolve to com.example.MyService."); + } + + [Fact] + public void RootManifestReferencedTypes_MatchesCompatNames () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyActivity", CompatJniName = "my/app/MyActivity", + ManagedTypeName = "My.App.MyActivity", ManagedTypeNamespace = "My.App", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Relative manifest name should match CompatJniName when JavaName uses a CRC64 package."); + } + + [Fact] + public void RootManifestReferencedTypes_MatchesNestedTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/Outer$Inner", CompatJniName = "com.example.Outer$Inner", + ManagedTypeName = "MyApp.Outer.Inner", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "Inner", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Nested type 'Outer$Inner' should be matched using '$' separator."); + } + + static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index ec37cbff9af..104ba39a1a6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; @@ -792,9 +793,9 @@ public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesSByte () } [Fact] - public void Generate_UcoMethod_HasCatchRegionWithoutFinally () + public void Generate_ExportUcoMethod_HasCatchAndFinallyRegions () { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var peer = FindFixtureByJavaName ("my/app/ExportExample"); using var stream = GenerateAssembly (new [] { peer }, "UcoLegacyWrapperShape"); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); @@ -803,13 +804,13 @@ public void Generate_UcoMethod_HasCatchRegionWithoutFinally () .First (h => { var method = reader.GetMethodDefinition (h); var name = reader.GetString (method.Name); - return name.Contains ("onTouch") && name.Contains ("_uco_"); + return name.Contains ("myExportedMethod") && name.Contains ("_uco_"); }); var ucoMethod = reader.GetMethodDefinition (ucoMethodHandle); var body = pe.GetMethodBody (ucoMethod.RelativeVirtualAddress); Assert.NotNull (body); Assert.Contains (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Catch); - Assert.DoesNotContain (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Finally); + Assert.Contains (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Finally); } [Fact] @@ -943,6 +944,164 @@ public void Generate_AcwProxy_HasPrivateImplementationDetails () Assert.Contains ("", typeDefNames); } + [Fact] + public void Generate_ExportProxy_CallsManagedMethodDirectly () + { + var peers = ScanFixtures (); + var exportPeer = peers.First (p => p.JavaName == "my/app/ExportExample"); + + using var stream = GenerateAssembly (new [] { exportPeer }, "ExportDispatch"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("MyExportedMethod", memberNames); + Assert.DoesNotContain ("n_MyExportedMethod", memberNames); + } + + [Fact] + public void Generate_StaticExportProxy_CallsManagedMethodDirectly () + { + var peers = ScanFixtures (); + var exportPeer = peers.First (p => p.JavaName == "my/app/StaticExportExample"); + + using var stream = GenerateAssembly (new [] { exportPeer }, "StaticExportDispatch"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("ComputeLabel", memberNames); + Assert.DoesNotContain ("n_ComputeLabel", memberNames); + } + + [Fact] + public void Generate_ExportProxy_UsesStaticMarshallingHelpers () + { + var peers = ScanFixtures (); + var exportPeer = peers.First (p => p.JavaName == "my/app/ExportWithJavaBoundParams"); + + using var stream = GenerateAssembly (new [] { exportPeer }, "ExportMarshalling"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("GetObject", memberNames); + Assert.Contains ("NewString", memberNames); + Assert.Contains ("HandleClick", memberNames); + Assert.Contains ("ProcessView", memberNames); + Assert.Contains ("GetViewName", memberNames); + } + + [Fact] + public void Generate_ExportFieldProxy_UsesToLocalJniHandleForObjectReturn () + { + var peers = ScanFixtures (); + var exportFieldPeer = peers.First (p => p.JavaName == "my/app/ExportFieldExample"); + + using var stream = GenerateAssembly (new [] { exportFieldPeer }, "ExportFieldDispatch"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("ToLocalJniHandle", memberNames); + Assert.Contains ("GetInstance", memberNames); + } + + [Fact] + public void Generate_ExportProxy_SupportsArrayAndLegacyMarshallerHelpers () + { + var peers = ScanFixtures (); + var exportPeer = peers.First (p => p.JavaName == "my/app/ExportMarshallingShapes"); + + using var stream = GenerateAssembly (new [] { exportPeer }, "ExportLegacyMarshalling"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("FromJniHandle", memberNames); + Assert.Contains ("CopyArray", memberNames); + Assert.Contains ("NewArray", memberNames); + Assert.Contains ("WrapStream", memberNames); + Assert.Contains ("ReadXml", memberNames); + Assert.Contains ("ReadResourceXml", memberNames); + + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("XmlResourceParserReader", typeNames); + Assert.Contains ("XmlReaderResourceParser", typeNames); + } + + [Fact] + public void Generate_ExportProxy_UsesExactCrossAssemblyTypeReferences () + { + var peer = MakePeerWithActivation ("my/app/CrossAssemblyExport", "MyApp.CrossAssemblyExport", "App") with { + DoNotGenerateAcw = false, + MarshalMethods = new List { + new () { + JniName = "convert", + NativeCallbackName = "n_convert", + JniSignature = "(Lthird/party/Widget;)Lthird/party/Result;", + ManagedMethodName = "Convert", + ManagedParameterTypes = new [] { + new TypeRefData { ManagedTypeName = "ThirdParty.Widget", AssemblyName = "ThirdParty.Library" }, + }, + ManagedReturnType = new TypeRefData { ManagedTypeName = "ThirdParty.Result", AssemblyName = "ThirdParty.Library" }, + IsExport = true, + }, + }, + }; + + using var stream = GenerateAssembly (new [] { peer }, "CrossAssemblyExport"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var thirdPartyAsmRef = reader.AssemblyReferences + .First (h => reader.GetString (reader.GetAssemblyReference (h).Name) == "ThirdParty.Library"); + + var typeRefs = reader.TypeReferences + .Select (h => (Handle: h, Ref: reader.GetTypeReference (h))) + .ToList (); + + var widgetRef = typeRefs.First (t => reader.GetString (t.Ref.Name) == "Widget"); + var resultRef = typeRefs.First (t => reader.GetString (t.Ref.Name) == "Result"); + + Assert.Equal (thirdPartyAsmRef, widgetRef.Ref.ResolutionScope); + Assert.Equal (thirdPartyAsmRef, resultRef.Ref.ResolutionScope); + } + + [Theory] + [InlineData ("System.Int32&", "System.Void", "(I)V", "by-ref or pointer")] + [InlineData ("System.Int32*", "System.Void", "(I)V", "by-ref or pointer")] + [InlineData ("System.Int32", "System.Collections.Generic.List", "(I)Ljava/lang/Object;", "generic")] + public void Generate_ExportProxy_UnsupportedManagedShapesThrow (string parameterType, string returnType, string jniSignature, string expectedMessage) + { + var peer = MakePeerWithActivation ("my/app/UnsupportedExport", "MyApp.UnsupportedExport", "App") with { + DoNotGenerateAcw = false, + MarshalMethods = new List { + new () { + JniName = "badExport", + NativeCallbackName = "n_badExport", + JniSignature = jniSignature, + ManagedMethodName = "BadExport", + ManagedParameterTypes = new [] { + new TypeRefData { ManagedTypeName = parameterType, AssemblyName = "System.Runtime" }, + }, + ManagedReturnType = new TypeRefData { + ManagedTypeName = returnType, + AssemblyName = returnType.StartsWith ("System.Collections.Generic.", StringComparison.Ordinal) + ? "System.Collections" + : "System.Runtime", + }, + IsExport = true, + }, + }, + }; + + var ex = Assert.Throws (() => { + using var stream = GenerateAssembly (new [] { peer }, "UnsupportedExport"); + }); + Assert.Contains (expectedMessage, ex.Message); + } + [Fact] public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () { @@ -1287,4 +1446,486 @@ public void Generate_ProxyTypes_HaveSelfAppliedAttribute () } Assert.True (hasSelfApplied, "Proxy type should have a self-applied attribute (ctor is MethodDefinition)"); } + + [Fact] + public void Generate_UcoConstructor_Parameterless_InvokesUserVisibleCtorViaSetPeerReference () + { + // Regression test for ContainsExportedMethods (JnienvTest.ActivatedDirectObjectSubclassesShouldBeRegistered): + // for the parameterless `()V` UCO constructor wrapper, the emitter must mirror + // TypeManager.Activate (Mono.Android/Java.Interop/TypeManager.cs): + // + // 1. RuntimeHelpers.GetUninitializedObject(typeof(T)) + // 2. ((IJavaPeerable) obj).SetPeerReference(new JniObjectReference(self, Invalid)) + // 3. obj..ctor() // user-visible parameterless ctor + // + // The legacy implementation called the inherited activation ctor `(IntPtr, + // JniHandleOwnership)` instead, so user-visible ctor bodies (e.g. `Constructed = true`) + // never ran when the peer was created from the Java side. + var peer = MakeAcwPeer ("test/UcoCtorPeer", "Test.UcoCtorPeer", "TestAsm"); + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorParameterlessTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // SetPeerReference member ref must exist. + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("SetPeerReference", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); + + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + + var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => MetadataTokens.MemberReferenceHandle (i)) + .ToList (); + + // 1. The body must call SetPeerReference (the new behavior). + var setPeerHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "SetPeerReference"); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (setPeerHandle)), + "nctor_*_uco IL should call IJavaPeerable.SetPeerReference for parameterless ctor"); + + // 2. The body must call GetUninitializedObject (no `newobj` of the activation ctor). + var getUninitHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetUninitializedObject"); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getUninitHandle)), + "nctor_*_uco IL should call RuntimeHelpers.GetUninitializedObject for parameterless ctor"); + + // 3. The body must call the user-visible parameterless ctor on the target type — and + // NOT the (IntPtr, JniHandleOwnership) activation ctor. We disambiguate by signature. + var targetCtorRefs = memberRefHandles + .Where (h => { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor") + return false; + if (mref.Parent.Kind != HandleKind.TypeReference) + return false; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + return reader.GetString (typeRef.Name) == "UcoCtorPeer"; + }) + .ToList (); + Assert.NotEmpty (targetCtorRefs); + + var ctorSigDecoder = new MethodSignatureDecoder (); + MemberReferenceHandle? userCtorHandle = null; + MemberReferenceHandle? activationCtorHandle = null; + foreach (var h in targetCtorRefs) { + var mref = reader.GetMemberReference (h); + int paramCount = mref.DecodeMethodSignature (ctorSigDecoder, genericContext: null).RequiredParameterCount; + if (paramCount == 0) userCtorHandle = h; + else if (paramCount == 2) activationCtorHandle = h; + } + + Assert.NotNull (userCtorHandle); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtorHandle!.Value)), + "nctor_*_uco IL should call the user-visible parameterless ctor on the target type"); + if (activationCtorHandle.HasValue) { + Assert.False (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (activationCtorHandle.Value)), + "nctor_*_uco IL should NOT call the (IntPtr, JniHandleOwnership) activation ctor for parameterless `()V`"); + } + } + + [Fact] + public void Generate_UcoConstructor_Parameterless_NoMatchingManagedCtor_FallsBackToActivationCtor () + { + // Regression test for Java.Lang.Thread+RunnableImplementor: it registers a `()V` Java + // ctor via JCW codegen, but the managed type only defines parameterized ctors. Emitting + // a member ref to `..ctor()` would resolve to a non-existent method and crash the test + // app at runtime with `MissingMethodException : Method not found: 'Void RunnableImplementor..ctor()'`. + // In this case the codegen must fall back to the legacy `(IntPtr, JniHandleOwnership)` + // activation-ctor path (i.e. `newobj` of the activation ctor). + var peer = MakeAcwPeer ("test/UcoCtorNoParamlessPeer", "Test.UcoCtorNoParamlessPeer", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V", HasMatchingManagedCtor = false }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorNoParamlessTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + + var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => MetadataTokens.MemberReferenceHandle (i)) + .ToList (); + + // 1. The body must NOT call the user-visible parameterless ctor. + var ctorSigDecoder = new MethodSignatureDecoder (); + MemberReferenceHandle? userCtorHandle = null; + MemberReferenceHandle? activationCtorHandle = null; + foreach (var h in memberRefHandles) { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor") + continue; + if (mref.Parent.Kind != HandleKind.TypeReference) + continue; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + if (reader.GetString (typeRef.Name) != "UcoCtorNoParamlessPeer") + continue; + int paramCount = mref.DecodeMethodSignature (ctorSigDecoder, genericContext: null).RequiredParameterCount; + if (paramCount == 0) userCtorHandle = h; + else if (paramCount == 2) activationCtorHandle = h; + } + + Assert.Null (userCtorHandle); // member ref to `..ctor()` should not exist at all + + // 2. The body MUST reference the (IntPtr, JniHandleOwnership) activation ctor — either + // via `newobj` (IsOnLeafType=true) or `call` (IsOnLeafType=false). The exact opcode + // is an implementation detail of the legacy activation-ctor codegen. + Assert.NotNull (activationCtorHandle); + int activationToken = MetadataTokens.GetToken (activationCtorHandle!.Value); + Assert.True ( + ILContainsCallToken (ilBytes, activationToken) || ILContainsNewobjToken (ilBytes, activationToken), + "nctor_*_uco IL should reference the (IntPtr, JniHandleOwnership) activation ctor when no matching parameterless managed ctor exists"); + } + + [Fact] + public void Generate_UcoConstructor_ObjectRefParam_MarshalsViaJavaLangObjectGetObject () + { + // (Ljava/lang/Throwable;)V — verifies ref-arg marshalling delegates to + // Java.Lang.Object.GetObject (jniHandle, DoNotTransfer, paramType) + // and that the user-visible (Throwable) ctor is invoked. + var paramType = new TypeRefData { ManagedTypeName = "Java.Lang.Throwable", AssemblyName = "Mono.Android" }; + var peer = MakeAcwPeer ("test/UcoCtorObjArg", "Test.UcoCtorObjArg", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(Ljava/lang/Throwable;)V", + HasMatchingManagedCtor = true, + ManagedParameterTypes = new [] { paramType }, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorObjArg"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.Contains ("GetObject", GetMemberRefNames (reader)); + + var ilBytes = GetNctorUcoIL (pe, reader); + var memberRefHandles = AllMemberRefHandles (reader); + + var getObjectHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject"); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getObjectHandle)), + "nctor_*_uco IL should call Java.Lang.Object.GetObject for an object-ref ctor arg"); + + var userCtor = FindUserCtorRefByFirstParam (reader, "UcoCtorObjArg", paramCount: 1, firstParamTypeName: "Java.Lang.Throwable"); + Assert.NotNull (userCtor); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), + "nctor_*_uco IL should call the user-visible (Throwable) ctor"); + } + + [Fact] + public void Generate_UcoConstructor_PrimitiveIntParam_LoadsArgDirectly () + { + // (I)V — verifies primitive int args are loaded directly without GetObject. + var paramType = new TypeRefData { ManagedTypeName = "System.Int32", AssemblyName = "System.Runtime" }; + var peer = MakeAcwPeer ("test/UcoCtorIntArg", "Test.UcoCtorIntArg", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(I)V", + HasMatchingManagedCtor = true, + ManagedParameterTypes = new [] { paramType }, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorIntArg"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ilBytes = GetNctorUcoIL (pe, reader); + + // The IL must NOT call GetObject — primitive int is loaded directly via Ldarg. + var memberRefHandles = AllMemberRefHandles (reader); + var getObjectHandle = memberRefHandles.FirstOrDefault (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject"); + if (!getObjectHandle.IsNil) { + Assert.False (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getObjectHandle)), + "nctor_*_uco IL should NOT call GetObject for a primitive int ctor arg"); + } + + var userCtor = FindUserCtorRef (reader, "UcoCtorIntArg", new [] { "System.Int32" }); + Assert.NotNull (userCtor); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), + "nctor_*_uco IL should call the user-visible (int) ctor"); + } + + [Fact] + public void Generate_UcoConstructor_BooleanParam_EmitsByteToBoolConversion () + { + // (Z)V — verifies byte→bool conversion (Ldc.I4.0 + Cgt.Un) is emitted for + // System.Boolean params, matching ExportMethodDispatchEmitter's primitive marshalling. + var paramType = new TypeRefData { ManagedTypeName = "System.Boolean", AssemblyName = "System.Runtime" }; + var peer = MakeAcwPeer ("test/UcoCtorBoolArg", "Test.UcoCtorBoolArg", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(Z)V", + HasMatchingManagedCtor = true, + ManagedParameterTypes = new [] { paramType }, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorBoolArg"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ilBytes = GetNctorUcoIL (pe, reader); + + // Look for the bool conversion sequence: Ldc_I4_0 (0x16) ; Cgt_Un (0xFE 0x03) + bool foundBoolConv = false; + for (int i = 0; i < ilBytes.Length - 2; i++) { + if (ilBytes [i] == 0x16 && ilBytes [i + 1] == 0xFE && ilBytes [i + 2] == 0x03) { + foundBoolConv = true; + break; + } + } + Assert.True (foundBoolConv, "nctor_*_uco IL should emit Ldc.I4.0 + Cgt.Un to convert byte→bool for Boolean ctor arg"); + + var userCtor = FindUserCtorRef (reader, "UcoCtorBoolArg", new [] { "System.Boolean" }); + Assert.NotNull (userCtor); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), + "nctor_*_uco IL should call the user-visible (bool) ctor"); + } + + [Fact] + public void Generate_UcoConstructor_StringParam_MarshalsViaJniEnvGetString () + { + // (Ljava/lang/String;)V — verifies String args marshal via JNIEnv.GetString. + var paramType = new TypeRefData { ManagedTypeName = "System.String", AssemblyName = "System.Runtime" }; + var peer = MakeAcwPeer ("test/UcoCtorStrArg", "Test.UcoCtorStrArg", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(Ljava/lang/String;)V", + HasMatchingManagedCtor = true, + ManagedParameterTypes = new [] { paramType }, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorStrArg"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ilBytes = GetNctorUcoIL (pe, reader); + var memberRefHandles = AllMemberRefHandles (reader); + + // JNIEnv.GetString member ref must be present and called. + var getStringHandles = memberRefHandles.Where (h => { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != "GetString") + return false; + if (mref.Parent.Kind != HandleKind.TypeReference) + return false; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + return reader.GetString (typeRef.Name) == "JNIEnv"; + }).ToList (); + Assert.NotEmpty (getStringHandles); + Assert.Contains (getStringHandles, h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))); + + var userCtor = FindUserCtorRef (reader, "UcoCtorStrArg", new [] { "System.String" }); + Assert.NotNull (userCtor); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), + "nctor_*_uco IL should call the user-visible (string) ctor"); + } + + [Fact] + public void Generate_UcoConstructor_MixedSignature_MarshalsBothPrimitiveAndObjectArgs () + { + // (ILjava/lang/Throwable;)V — verifies int passes through and Throwable goes via GetObject. + var intParam = new TypeRefData { ManagedTypeName = "System.Int32", AssemblyName = "System.Runtime" }; + var throwableParam = new TypeRefData { ManagedTypeName = "Java.Lang.Throwable", AssemblyName = "Mono.Android" }; + var peer = MakeAcwPeer ("test/UcoCtorMixed", "Test.UcoCtorMixed", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(ILjava/lang/Throwable;)V", + HasMatchingManagedCtor = true, + ManagedParameterTypes = new [] { intParam, throwableParam }, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorMixed"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("GetObject", memberNames); + + var ilBytes = GetNctorUcoIL (pe, reader); + var memberRefHandles = AllMemberRefHandles (reader); + var getObjectHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetObject"); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (getObjectHandle)), + "nctor_*_uco IL should call GetObject for the Throwable arg in the mixed signature"); + + // User ctor: (Int32, Java.Lang.Throwable). Need a signature-discriminated lookup + // because the activation ctor (IntPtr, JniHandleOwnership) also has 2 params. + var userCtor = FindUserCtorRefByFirstParam (reader, "UcoCtorMixed", paramCount: 2, firstParamTypeName: "System.Int32"); + Assert.NotNull (userCtor); + Assert.True (ILContainsCallToken (ilBytes, MetadataTokens.GetToken (userCtor!.Value)), + "nctor_*_uco IL should call the user-visible (int, Throwable) ctor"); + } + + [Fact] + public void Generate_UcoConstructor_ParameterizedNoMatch_FallsBackToActivationCtor () + { + // HasMatchingManagedCtor=false on a parameterized signature — codegen must fall back + // to the legacy (IntPtr, JniHandleOwnership) activation-ctor path, NOT emit a member + // ref to a non-existent (Throwable) ctor. + var peer = MakeAcwPeer ("test/UcoCtorParamNoMatch", "Test.UcoCtorParamNoMatch", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = "(Ljava/lang/Throwable;)V", + HasMatchingManagedCtor = false, + }, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorParamNoMatch"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Activation ctor (IntPtr, JniHandleOwnership) on the target type — multiple + // equivalent member refs may be added (the metadata builder doesn't dedupe + // across emit phases), so verify the IL calls *some* 2-arg ctor on the type. + var activationCtorTokens = AllMemberRefHandles (reader) + .Where (h => { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor") + return false; + if (mref.Parent.Kind != HandleKind.TypeReference) + return false; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + if (reader.GetString (typeRef.Name) != "UcoCtorParamNoMatch") + return false; + return mref.DecodeMethodSignature (new MethodSignatureDecoder (), genericContext: null).RequiredParameterCount == 2; + }) + .Select (h => MetadataTokens.GetToken (h)) + .ToList (); + Assert.NotEmpty (activationCtorTokens); + + var ilBytes = GetNctorUcoIL (pe, reader); + Assert.Contains (activationCtorTokens, t => ILContainsCallToken (ilBytes, t) || ILContainsNewobjToken (ilBytes, t)); + } + + static byte[] GetNctorUcoIL (PEReader pe, MetadataReader reader) + { + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + return ilBytes!; + } + + static List AllMemberRefHandles (MetadataReader reader) => + Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => MetadataTokens.MemberReferenceHandle (i)) + .ToList (); + + static MemberReferenceHandle? FindUserCtorRef (MetadataReader reader, string typeShortName, IReadOnlyList paramTypeNames) + { + var decoder = new TypeNameSignatureDecoder (reader); + foreach (var h in AllMemberRefHandles (reader)) { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor") + continue; + if (mref.Parent.Kind != HandleKind.TypeReference) + continue; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + if (reader.GetString (typeRef.Name) != typeShortName) + continue; + var sig = mref.DecodeMethodSignature (decoder, genericContext: null); + if (sig.RequiredParameterCount != paramTypeNames.Count) + continue; + bool match = true; + for (int i = 0; i < paramTypeNames.Count; i++) { + if (sig.ParameterTypes [i] != paramTypeNames [i]) { + match = false; + break; + } + } + if (match) + return h; + } + return null; + } + + static MemberReferenceHandle? FindUserCtorRefByFirstParam (MetadataReader reader, string typeShortName, int paramCount, string firstParamTypeName) + { + var decoder = new TypeNameSignatureDecoder (reader); + foreach (var h in AllMemberRefHandles (reader)) { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor") + continue; + if (mref.Parent.Kind != HandleKind.TypeReference) + continue; + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + if (reader.GetString (typeRef.Name) != typeShortName) + continue; + var sig = mref.DecodeMethodSignature (decoder, genericContext: null); + if (sig.RequiredParameterCount != paramCount) + continue; + if (sig.ParameterTypes [0] == firstParamTypeName) + return h; + } + return null; + } + + // SignatureTypeProvider returning a stringified type name for primitives and typerefs. + sealed class TypeNameSignatureDecoder : ISignatureTypeProvider + { + readonly MetadataReader _reader; + public TypeNameSignatureDecoder (MetadataReader reader) => _reader = reader; + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => "System." + typeCode; + public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var tr = reader.GetTypeReference (handle); + var name = reader.GetString (tr.Name); + var ns = tr.Namespace.IsNil ? "" : reader.GetString (tr.Namespace); + return ns.Length == 0 ? name : ns + "." + name; + } + public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => ""; + public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => ""; + public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[*]"; + public string GetByReferenceType (string elementType) => elementType + "&"; + public string GetFunctionPointerType (MethodSignature signature) => ""; + public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) => genericType; + public string GetGenericMethodParameter (object? genericContext, int index) => ""; + public string GetGenericTypeParameter (object? genericContext, int index) => ""; + public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + public string GetPinnedType (string elementType) => elementType; + public string GetPointerType (string elementType) => elementType + "*"; + } + + // Minimal SignatureTypeProvider used only to count required parameters of a member ref. + sealed class MethodSignatureDecoder : ISignatureTypeProvider + { + public int GetArrayType (int elementType, ArrayShape shape) => 0; + public int GetByReferenceType (int elementType) => 0; + public int GetFunctionPointerType (MethodSignature signature) => 0; + public int GetGenericInstantiation (int genericType, ImmutableArray typeArguments) => 0; + public int GetGenericMethodParameter (object? genericContext, int index) => 0; + public int GetGenericTypeParameter (object? genericContext, int index) => 0; + public int GetModifiedType (int modifier, int unmodifiedType, bool isRequired) => 0; + public int GetPinnedType (int elementType) => 0; + public int GetPointerType (int elementType) => 0; + public int GetPrimitiveType (PrimitiveTypeCode typeCode) => 0; + public int GetSZArrayType (int elementType) => 0; + public int GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => 0; + public int GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => 0; + public int GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => 0; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 049bc74a654..fe4217ee12d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1330,6 +1330,58 @@ public void Fixture_TouchHandler_AllUcoMethods () Assert.True (proxy.UcoMethods.Count >= 2, "TouchHandler should have multiple UCO methods"); } + [Fact] + public void Fixture_ExportExample_UsesExportMethodDispatch () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + var exportUco = Assert.Single (proxy.UcoMethods); + var exportDispatch = exportUco.ExportMethodDispatch; + Assert.True (exportUco.UsesExportMethodDispatch); + Assert.NotNull (exportDispatch); + Assert.Equal ("MyExportedMethod", exportDispatch.ManagedMethodName); + } + + [Fact] + public void Fixture_StaticExportExample_UsesStaticExportMethodDispatch () + { + var peer = FindFixtureByJavaName ("my/app/StaticExportExample"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + var exportUco = Assert.Single (proxy.UcoMethods); + var exportDispatch = exportUco.ExportMethodDispatch; + Assert.True (exportUco.UsesExportMethodDispatch); + Assert.NotNull (exportDispatch); + Assert.True (exportDispatch.IsStatic); + Assert.Equal ("ComputeLabel", exportDispatch.ManagedMethodName); + } + + [Fact] + public void Fixture_ExportMarshallingShapes_PropagatesExactManagedTypeMetadata () + { + var peer = FindFixtureByJavaName ("my/app/ExportMarshallingShapes"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + + var xmlUco = proxy.UcoMethods.First (u => u.ExportMethodDispatch?.ManagedMethodName == "ReadXml"); + var xmlDispatch = xmlUco.ExportMethodDispatch; + Assert.NotNull (xmlDispatch); + Assert.Equal ("System.Xml.XmlReader", xmlDispatch.ParameterTypes [0].ManagedTypeName); + Assert.Equal ("System.Xml.ReaderWriter", xmlDispatch.ParameterTypes [0].AssemblyName); + Assert.Equal (ExportParameterKindInfo.XmlPullParser, xmlDispatch.ParameterKinds [0]); + Assert.Equal (ExportParameterKindInfo.XmlPullParser, xmlDispatch.ReturnKind); + + var resourceXmlUco = proxy.UcoMethods.First (u => u.ExportMethodDispatch?.ManagedMethodName == "ReadResourceXml"); + var resourceXmlDispatch = resourceXmlUco.ExportMethodDispatch; + Assert.NotNull (resourceXmlDispatch); + Assert.Equal (ExportParameterKindInfo.XmlResourceParser, resourceXmlDispatch.ParameterKinds [0]); + Assert.Equal (ExportParameterKindInfo.XmlResourceParser, resourceXmlDispatch.ReturnKind); + } + [Fact] public void Fixture_CustomView_HasTwoConstructorWrappers () { 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 659de452634..8e556b7bea2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -15,6 +15,7 @@ public partial class JavaPeerScannerTests [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")] + [InlineData ("my/app/StaticExportExample", "ComputeLabel", "computeLabel", "(I)Ljava/lang/String;")] public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) { var method = FindFixtureByJavaName (javaName) @@ -58,15 +59,105 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases () [InlineData ("processView", "(Landroid/view/View;)V")] [InlineData ("handleClick", "(Landroid/view/View;I)Z")] [InlineData ("getViewName", "(Landroid/view/View;)Ljava/lang/String;")] + [InlineData ("computeLabel", "(I)Ljava/lang/String;")] public void Scan_ExportMethod_ResolvesJavaBoundParameterTypes (string jniName, string expectedSig) { - var method = FindFixtureByJavaName ("my/app/ExportWithJavaBoundParams") + var peer = jniName == "computeLabel" + ? FindFixtureByJavaName ("my/app/StaticExportExample") + : FindFixtureByJavaName ("my/app/ExportWithJavaBoundParams"); + var method = peer .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); Assert.NotNull (method); Assert.Equal (expectedSig, method.JniSignature); Assert.Null (method.Connector); } + [Fact] + public void Scan_ExportMethod_CapturesStaticDispatchShape () + { + var method = FindFixtureByJavaName ("my/app/StaticExportExample") + .MarshalMethods.Single (m => m.JniName == "computeLabel"); + Assert.True (method.IsStatic); + Assert.Equal ("ComputeLabel", method.ManagedMethodName); + } + + [Theory] + [InlineData ("roundTripNames", "([Ljava/lang/String;)[Ljava/lang/String;")] + [InlineData ("openStream", "(Ljava/io/InputStream;)I")] + [InlineData ("wrapStream", "(Ljava/io/OutputStream;)Ljava/io/OutputStream;")] + [InlineData ("readXml", "(Lorg/xmlpull/v1/XmlPullParser;)Lorg/xmlpull/v1/XmlPullParser;")] + [InlineData ("readResourceXml", "(Landroid/content/res/XmlResourceParser;)Landroid/content/res/XmlResourceParser;")] + public void Scan_ExportMethod_SupportsLegacyMarshallerShapes (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportMarshallingShapes") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + } + + [Theory] + [InlineData ("echoEnum", "(I)I")] + [InlineData ("echoByteEnum", "(B)B")] + [InlineData ("echoLongEnum", "(J)J")] + public void Scan_ExportMethod_EnumParametersUseUnderlyingPrimitiveJniDescriptor (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportEnumShapes") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + } + + [Fact] + public void Scan_ExportMethod_EnumParametersFlagTypeRefAsEnum () + { + var method = FindFixtureByJavaName ("my/app/ExportEnumShapes") + .MarshalMethods.First (m => m.JniName == "echoEnum"); + Assert.True (method.ManagedParameterTypes [0].IsEnum, "enum parameter should be tagged IsEnum=true"); + Assert.True (method.ManagedReturnType.IsEnum, "enum return type should be tagged IsEnum=true"); + } + + [Theory] + [InlineData ("echoCharSequence", "(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;")] + public void Scan_ExportMethod_CharSequenceMapsToCanonicalJavaType (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportCharSequenceShapes") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + } + + [Theory] + [InlineData ("echoList", "(Ljava/util/List;)Ljava/util/List;")] + [InlineData ("echoMap", "(Ljava/util/Map;)Ljava/util/Map;")] + [InlineData ("echoCollection", "(Ljava/util/Collection;)Ljava/util/Collection;")] + public void Scan_ExportMethod_NonGenericCollectionsMapToCanonicalJavaTypes (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportCollectionShapes") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + } + + [Fact] + public void Scan_ExportMethod_CapturesPreciseManagedTypeMetadata () + { + var arrayMethod = FindFixtureByJavaName ("my/app/ExportMarshallingShapes") + .MarshalMethods.First (m => m.JniName == "roundTripNames"); + Assert.Equal ("System.String[]", arrayMethod.ManagedParameterTypes [0].ManagedTypeName); + Assert.Equal ("System.Runtime", arrayMethod.ManagedParameterTypes [0].AssemblyName); + + var xmlMethod = FindFixtureByJavaName ("my/app/ExportMarshallingShapes") + .MarshalMethods.First (m => m.JniName == "readXml"); + Assert.Equal (ExportParameterKindInfo.XmlPullParser, xmlMethod.ManagedParameterExportKinds [0]); + Assert.Equal (ExportParameterKindInfo.XmlPullParser, xmlMethod.ManagedReturnExportKind); + Assert.Equal ("System.Xml.ReaderWriter", xmlMethod.ManagedReturnType.AssemblyName); + + var resourceXmlMethod = FindFixtureByJavaName ("my/app/ExportMarshallingShapes") + .MarshalMethods.First (m => m.JniName == "readResourceXml"); + Assert.Equal (ExportParameterKindInfo.XmlResourceParser, resourceXmlMethod.ManagedParameterExportKinds [0]); + Assert.Equal (ExportParameterKindInfo.XmlResourceParser, resourceXmlMethod.ManagedReturnExportKind); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index ba579e4e9d1..bff0485029f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -188,6 +188,23 @@ public ExportAttribute () { } public ExportAttribute (string name) => Name = name; } + public enum ExportParameterKind + { + Unspecified = 0, + InputStream = 1, + OutputStream = 2, + XmlPullParser = 3, + XmlResourceParser = 4, + } + + [AttributeUsage (AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false)] + public sealed class ExportParameterAttribute : Attribute + { + public ExportParameterKind Kind { get; } + + public ExportParameterAttribute (ExportParameterKind kind) => Kind = kind; + } + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] public sealed class ExportFieldAttribute : Attribute { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 7e8111cfd24..0cf9d1e8e16 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Xml; using Android.App; using Android.Content; using Android.Runtime; @@ -26,6 +28,12 @@ public class Exception : Throwable { protected Exception (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + // Mirrors Mono.Android's Java.Lang.ICharSequence: an interface without a + // [Register] attribute. The trimmable typemap scanner / emitter must + // special-case it to map onto java/lang/CharSequence and dispatch via + // Android.Runtime.CharSequence.ToLocalJniHandle. + public interface ICharSequence { } } namespace Android.App @@ -312,6 +320,13 @@ public class ExportExample : Java.Lang.Object public void MyExportedMethod () { } } + [Register ("my/app/StaticExportExample")] + public class StaticExportExample : Java.Lang.Object + { + [Java.Interop.Export ("computeLabel")] + public static string ComputeLabel (int value) => value.ToString (); + } + /// /// Has [Export] methods with non-primitive Java-bound parameter types. /// The JCW should resolve parameter types via [Register] instead of falling back to Object. @@ -329,6 +344,87 @@ public void ProcessView (Android.Views.View view) { } public string GetViewName (Android.Views.View view) { return ""; } } + [Register ("my/app/ExportMarshallingShapes")] + public class ExportMarshallingShapes : Java.Lang.Object + { + [Java.Interop.Export ("roundTripNames")] + public string[]? RoundTripNames (string[]? names) => names; + + [Java.Interop.Export ("openStream")] + public int OpenStream ([Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] Stream? stream) + => stream is null ? 0 : 1; + + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.OutputStream)] + [Java.Interop.Export ("wrapStream")] + public Stream? WrapStream ([Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.OutputStream)] Stream? stream) + => stream; + + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.XmlPullParser)] + [Java.Interop.Export ("readXml")] + public XmlReader? ReadXml ([Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.XmlPullParser)] XmlReader? reader) + => reader; + + [return: Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.XmlResourceParser)] + [Java.Interop.Export ("readResourceXml")] + public XmlReader? ReadResourceXml ([Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.XmlResourceParser)] XmlReader? reader) + => reader; + } + + public enum SampleEnum { A, B, C } + + public enum SampleByteEnum : byte { Red, Green, Blue } + + public enum SampleLongEnum : long { Zero = 0L, Big = long.MaxValue } + + /// + /// Has [Export] methods that take and return enum-typed values. Enums must + /// marshal via their underlying primitive JNI ABI (matching legacy + /// Mono.Android.Export behaviour) — not as object peers. + /// + [Register ("my/app/ExportEnumShapes")] + public class ExportEnumShapes : Java.Lang.Object + { + [Java.Interop.Export ("echoEnum")] + public SampleEnum EchoEnum (SampleEnum value) => value; + + [Java.Interop.Export ("echoByteEnum")] + public SampleByteEnum EchoByteEnum (SampleByteEnum value) => value; + + [Java.Interop.Export ("echoLongEnum")] + public SampleLongEnum EchoLongEnum (SampleLongEnum value) => value; + } + + /// + /// Has [Export] methods that take and return ICharSequence values. Must + /// dispatch through Android.Runtime.CharSequence.ToLocalJniHandle (mirrors + /// legacy Mono.Android.Export behaviour) — not the generic IJavaObject + /// path used for other peers. + /// + [Register ("my/app/ExportCharSequenceShapes")] + public class ExportCharSequenceShapes : Java.Lang.Object + { + [Java.Interop.Export ("echoCharSequence")] + public Java.Lang.ICharSequence? EchoCharSequence (Java.Lang.ICharSequence? value) => value; + } + + /// + /// Has [Export] methods that take and return non-generic collection types + /// (IList, IDictionary, ICollection). Each must dispatch through the + /// matching JavaList/JavaDictionary/JavaCollection.ToLocalJniHandle helper. + /// + [Register ("my/app/ExportCollectionShapes")] + public class ExportCollectionShapes : Java.Lang.Object + { + [Java.Interop.Export ("echoList")] + public System.Collections.IList? EchoList (System.Collections.IList? value) => value; + + [Java.Interop.Export ("echoMap")] + public System.Collections.IDictionary? EchoMap (System.Collections.IDictionary? value) => value; + + [Java.Interop.Export ("echoCollection")] + public System.Collections.ICollection? EchoCollection (System.Collections.ICollection? value) => value; + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods. diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs new file mode 100644 index 00000000000..a2d84306f80 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -0,0 +1,262 @@ +using System; + +using Android.Runtime; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + // Device-level coverage for [Export] / [ExportField] marshalling. + // + // These tests drive the Java side of an [Export]-bearing peer via JNIEnv, + // then assert what C# observed (and vice versa). They run under both the + // legacy llvm-ir typemap (which is the contract) and the trimmable typemap + // (which must match it). See export-comparison.md for the gap analysis. + // + // Naming: each test is named Export___ so the + // runner output is greppable. + [TestFixture] + public class ExportTests + { + // --------------------------------------------------------------- + // Group A — parameter / return marshalling + // --------------------------------------------------------------- + + [Test, Category ("Export")] + public void Export_Method_Primitive_RoundTrip () + { + using var e = new ExportPrimitives (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "EchoInt", "(I)I"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for EchoInt not found"); + int r = JNIEnv.CallIntMethod (e.Handle, m, new JValue (21)); + Assert.AreEqual (43, r, "EchoInt(21) should be 43 (= 21*2 + 1)"); + } + + [Test, Category ("Export")] + public void Export_Method_Bool_RoundTrip () + { + using var e = new ExportPrimitives (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "EchoBool", "(Z)Z"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for EchoBool not found"); + Assert.IsFalse (JNIEnv.CallBooleanMethod (e.Handle, m, new JValue (true)), "EchoBool(true) should return false"); + Assert.IsTrue (JNIEnv.CallBooleanMethod (e.Handle, m, new JValue (false)), "EchoBool(false) should return true"); + } + + [Test, Category ("Export")] + public void Export_Method_String_RoundTrip () + { + using var e = new ExportPrimitives (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "EchoString", "(Ljava/lang/String;)Ljava/lang/String;"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for EchoString not found"); + IntPtr argHandle = JNIEnv.NewString ("world"); + try { + IntPtr resultHandle = JNIEnv.CallObjectMethod (e.Handle, m, new JValue (argHandle)); + try { + string result = JNIEnv.GetString (resultHandle, JniHandleOwnership.DoNotTransfer); + Assert.AreEqual ("", result); + } finally { + JNIEnv.DeleteLocalRef (resultHandle); + } + } finally { + JNIEnv.DeleteLocalRef (argHandle); + } + } + + [Test, Category ("Export")] + public void Export_Method_PeerArg_RoundTrip () + { + using var e = new ExportPrimitives (); + using var arg = new Java.Lang.Integer (42); + var m = JNIEnv.GetMethodID (e.Class.Handle, "GetClassName", "(Ljava/lang/Object;)Ljava/lang/String;"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for GetClassName not found"); + IntPtr resultHandle = JNIEnv.CallObjectMethod (e.Handle, m, new JValue (arg.Handle)); + try { + string result = JNIEnv.GetString (resultHandle, JniHandleOwnership.DoNotTransfer); + Assert.AreEqual ("java.lang.Integer", result); + } finally { + JNIEnv.DeleteLocalRef (resultHandle); + } + } + + [Test, Category ("Export")] + public void Export_Method_PeerArg_NullArg_HandledGracefully () + { + using var e = new ExportPrimitives (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "GetClassName", "(Ljava/lang/Object;)Ljava/lang/String;"); + IntPtr resultHandle = JNIEnv.CallObjectMethod (e.Handle, m, new JValue (IntPtr.Zero)); + try { + string result = JNIEnv.GetString (resultHandle, JniHandleOwnership.DoNotTransfer); + Assert.AreEqual ("", result); + } finally { + JNIEnv.DeleteLocalRef (resultHandle); + } + } + + [Test, Category ("Export")] + public void Export_Method_IntArray_RoundTrip_AndCopyBack () + { + using var e = new ExportPrimitives (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "DoubleArray", "([I)[I"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for DoubleArray not found"); + + var input = new int [] { 1, 2, 3 }; + IntPtr argHandle = JNIEnv.NewArray (input); + try { + IntPtr resultHandle = JNIEnv.CallObjectMethod (e.Handle, m, new JValue (argHandle)); + try { + var output = (int []) JNIEnv.GetArray (resultHandle, JniHandleOwnership.DoNotTransfer, typeof (int)); + Assert.AreEqual (new [] { 2, 4, 6 }, output, "return array should have doubled values"); + + // Copy-back: the input handle should also reflect the doubled values + var roundTrippedInput = (int []) JNIEnv.GetArray (argHandle, JniHandleOwnership.DoNotTransfer, typeof (int)); + Assert.AreEqual (new [] { 2, 4, 6 }, roundTrippedInput, "input array mutations should propagate back to JNI handle"); + } finally { + JNIEnv.DeleteLocalRef (resultHandle); + } + } finally { + JNIEnv.DeleteLocalRef (argHandle); + } + } + + // NOTE: A5/A6/A7 (enum, ICharSequence return, IList return) are + // deferred. The legacy Java callable wrapper emitter + // (CecilImporter.GetJniSignature) returns null for managed enum, + // non-bound IList, and certain ICharSequence shapes — the build + // fails before the runtime path can be exercised. Those tests + // belong with the codegen fix that teaches the JCW emitter to + // widen these types (mirrors §2 / §7 of export-comparison.md). + + [Test, Category ("Export")] + public void Export_Method_PeerArray_RoundTrip () + { + using var e = new ExportPrimitives (); + using var a = new Java.Lang.Integer (1); + using var b = new Java.Lang.Integer (2); + using var c = new Java.Lang.Integer (3); + + var m = JNIEnv.GetMethodID (e.Class.Handle, "Tail", "([Ljava/lang/Object;)[Ljava/lang/Object;"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for Tail not found"); + + IntPtr argHandle = JNIEnv.NewObjectArray (a, b, c); + try { + IntPtr resultHandle = JNIEnv.CallObjectMethod (e.Handle, m, new JValue (argHandle)); + try { + var result = (Java.Lang.Object []) JNIEnv.GetArray (resultHandle, JniHandleOwnership.DoNotTransfer, typeof (Java.Lang.Object)); + Assert.AreEqual (2, result.Length); + Assert.AreEqual ("2", result [0].ToString ()); + Assert.AreEqual ("3", result [1].ToString ()); + } finally { + JNIEnv.DeleteLocalRef (resultHandle); + } + } finally { + JNIEnv.DeleteLocalRef (argHandle); + } + } + + // --------------------------------------------------------------- + // Group B — exception routing + // --------------------------------------------------------------- + // The trimmable [Export] UCO wraps the dispatch in BeginMarshalMethod / + // OnUserUnhandledException / EndMarshalMethod so unhandled managed + // exceptions are stored as a pending exception on the JniTransition + // (matching the JavaInterop contract used by UCO ctors) instead of + // aborting the process. When the JNI call returns to managed code on + // the same thread, RaisePendingException re-raises the original + // exception — which can be either the underlying managed exception + // or a Java.Lang.Throwable depending on the runtime path. The + // invariant we assert here is "process did not abort and an exception + // surfaces with a recognizable message". See + // ExportMethodDispatchEmitter.EmitWrappedExportMethodDispatch. + + [Test, Category ("Export")] + public void Export_Method_Throws_PrimitiveReturn_SurfacesAsManagedException () + { + using var e = new ExportThrowing (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "Throwing", "()I"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for Throwing not found"); + + // The managed body throws InvalidOperationException("boom"). The wrapper + // must catch it and route it through OnUserUnhandledException so the + // process survives; the exception then re-surfaces on the calling + // thread when the JNI call returns to managed code. + var ex = Assert.Catch (() => JNIEnv.CallIntMethod (e.Handle, m)); + Assert.That (ex, Is.Not.Null, "expected an exception, got null"); + Assert.That (ex.Message, Contains.Substring ("boom"), "exception message should preserve 'boom'"); + } + + [Test, Category ("Export")] + public void Export_Method_Throws_ObjectReturn_SurfacesAsManagedException () + { + using var e = new ExportThrowing (); + var m = JNIEnv.GetMethodID (e.Class.Handle, "ThrowingString", "()Ljava/lang/String;"); + Assert.AreNotEqual (IntPtr.Zero, m, "JNI method id for ThrowingString not found"); + var ex = Assert.Catch (() => JNIEnv.CallObjectMethod (e.Handle, m)); + Assert.That (ex, Is.Not.Null, "expected an exception, got null"); + } + + // --------------------------------------------------------------- + // Group D — [ExportField] runtime visibility from Java + // --------------------------------------------------------------- + // NOTE: device-level [ExportField] tests are deferred. The JCW + // generator (legacy and trimmable) currently emits a static field + // initializer that calls the [ExportField] method as a non-static + // member (`public static int FOO = InitialFoo();`), which fails + // javac when the C# method is `static`, and is unreachable at + // runtime when the C# method is an instance member because there + // is no peer instance during class init. Add runtime [ExportField] + // coverage once the JCW emitter handles both shapes correctly. + } + + // --------------------------------------------------------------- + // Test fixtures (peer types) used by the tests above. + // + // Each fixture is a small Java.Lang.Object subclass with [Export] members + // designed to exercise one corner of the marshalling matrix. + // --------------------------------------------------------------- + + class ExportPrimitives : Java.Lang.Object + { + [Export] + public int EchoInt (int x) => x * 2 + 1; + + [Export] + public bool EchoBool (bool x) => !x; + + [Export] + public string EchoString (string x) => "<" + x + ">"; + + [Export] + public string GetClassName (Java.Lang.Object o) => o?.Class?.Name ?? ""; + + [Export] + public int [] DoubleArray (int [] xs) + { + for (int i = 0; i < xs.Length; i++) { + xs [i] *= 2; + } + return xs; + } + + [Export] + public Java.Lang.Object [] Tail (Java.Lang.Object [] xs) + { + if (xs.Length <= 1) { + return Array.Empty (); + } + var result = new Java.Lang.Object [xs.Length - 1]; + Array.Copy (xs, 1, result, 0, result.Length); + return result; + } + } + + class ExportThrowing : Java.Lang.Object + { + [Export] + public int Throwing () => throw new InvalidOperationException ("boom"); + + [Export] + public string ThrowingString () => throw new InvalidOperationException ("boom-string"); + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 0eacc4eddad..3b2dcab84be 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -256,7 +256,6 @@ public void SetField_PermitNullValues () } [Test, Category ("Export")] - [Category ("CoreCLRIgnore")] //TODO: https://github.com/dotnet/android/issues/10069 public void CreateTypeWithExportedMethods () { using (var e = new ContainsExportedMethods ()) { @@ -269,7 +268,6 @@ public void CreateTypeWithExportedMethods () } [Test, Category ("Export")] - [Category ("CoreCLRIgnore")] //TODO: https://github.com/dotnet/android/issues/10069 public void ActivatedDirectObjectSubclassesShouldBeRegistered () { if (Build.VERSION.SdkInt <= BuildVersionCodes.GingerbreadMr1) @@ -327,6 +325,65 @@ public void ActivatedDirectThrowableSubclassesShouldBeRegistered () Console.Error.WriteLine ($"# jonp: END ActivatedDirectThrowableSubclassesShouldBeRegistered!!!"); } + // Locks in the legacy llvm-ir typemap behavior for parameterized ctor activation. + // Java instantiation forwards JNI args to the user-visible managed ctor; trimmable + // typemap codegen must match this contract for non-()V signatures. + // + // NOTE: Legacy mono.android.TypeManager.Activate routes args through + // JNIEnv.GetObjectArray, which only supports IJavaObject-derived element types. + // Tests deliberately use Java.Lang.Throwable args (not System.String) to stay + // inside the supported legacy contract. + [Test] + public void ActivatedDirectThrowableSubclasses_ThrowableCtor_ShouldForwardArgs () + { + using (var klass = Java.Lang.Class.FromType (typeof (ThrowableCauseActivatedFromJava))) + using (var cause = new Java.Lang.Throwable ("a-cause")) { + var ctor = JNIEnv.GetMethodID (klass.Handle, "", "(Ljava/lang/Throwable;)V"); + + var o = JNIEnv.StartCreateInstance (klass.Handle, ctor, new JValue (cause.Handle)); + JNIEnv.FinishCreateInstance (o, klass.Handle, ctor, new JValue (cause.Handle)); + + GC.Collect (); + GC.WaitForPendingFinalizers (); + + var v = Java.Lang.Object.GetObject (o, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (v); + Assert.IsTrue (v.Constructed, "user-visible ctor body did not run"); + Assert.IsNotNull (v.ReceivedCause, "throwable arg not forwarded"); + Assert.AreEqual ("a-cause", v.ReceivedCause!.Message); + v.Dispose (); + } + } + + [Test] + public void ActivatedDirectThrowableSubclasses_MultipleCtors_ShouldDispatchToCorrectCtor () + { + using (var klass = Java.Lang.Class.FromType (typeof (MultiCtorActivatedFromJava))) { + // Default ctor + { + var ctor = JNIEnv.GetMethodID (klass.Handle, "", "()V"); + var o = JNIEnv.StartCreateInstance (klass.Handle, ctor); + JNIEnv.FinishCreateInstance (o, klass.Handle, ctor); + var v = Java.Lang.Object.GetObject (o, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (v); + Assert.AreEqual (0, v.CtorIndex, "()V dispatched to wrong ctor"); + v.Dispose (); + } + // (Throwable) ctor + using (var cause = new Java.Lang.Throwable ("only-cause")) { + var ctor = JNIEnv.GetMethodID (klass.Handle, "", "(Ljava/lang/Throwable;)V"); + var o = JNIEnv.StartCreateInstance (klass.Handle, ctor, new JValue (cause.Handle)); + JNIEnv.FinishCreateInstance (o, klass.Handle, ctor, new JValue (cause.Handle)); + var v = Java.Lang.Object.GetObject (o, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (v); + Assert.AreEqual (1, v.CtorIndex, "(Throwable) dispatched to wrong ctor"); + Assert.IsNotNull (v.ReceivedCause); + Assert.AreEqual ("only-cause", v.ReceivedCause!.Message); + v.Dispose (); + } + } + } + [Test] public void ConversionsAndThreadsAndInstanceMappingsOhMy () { @@ -535,6 +592,42 @@ public ThrowableActivatedFromJava () } } + // Throwable subclass with (Throwable) ctor — exercises single IJavaObject-derived + // ref-arg ctor activation. (System.String args are NOT supported by the legacy + // TypeManager.Activate path because JNIEnv.GetObjectArray routes Object[] elements + // through the IJavaObject converter.) + class ThrowableCauseActivatedFromJava : Java.Lang.Throwable { + + public bool Constructed; + public Java.Lang.Throwable? ReceivedCause; + + public ThrowableCauseActivatedFromJava (Java.Lang.Throwable cause) + : base (cause) + { + Constructed = true; + ReceivedCause = cause; + } + } + + // Throwable subclass with multiple registered ctors — exercises ctor dispatch. + class MultiCtorActivatedFromJava : Java.Lang.Throwable { + + public int CtorIndex = -1; + public Java.Lang.Throwable? ReceivedCause; + + public MultiCtorActivatedFromJava () + { + CtorIndex = 0; + } + + public MultiCtorActivatedFromJava (Java.Lang.Throwable cause) + : base (cause) + { + CtorIndex = 1; + ReceivedCause = cause; + } + } + class GenericHolder : Java.Lang.Object { public T Value {get; set;} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index f1575d1e45d..0e4d4333e64 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -29,7 +29,7 @@ NetworkInterfaces excluded: https://github.com/dotnet/runtime/issues/75155 --> - $(ExcludeCategories):CoreCLRIgnore:NTLM + $(ExcludeCategories):CoreCLRIgnore:NTLM $(ExcludeCategories):NativeAOTIgnore:SSL:NTLM:AndroidClientHandler:Export:NativeTypeMap @@ -40,9 +40,10 @@ false CoreCLRTrimmable - $(ExcludeCategories):NativeTypeMap:Export + $(ExcludeCategories):NativeTypeMap:TrimmableIgnore + @@ -74,9 +75,16 @@ - - - + + + + + + + + <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> @@ -102,6 +110,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index d69013f4063..f559ff27a9f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -30,21 +30,14 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // Tests from the external Java.Interop-Tests assembly that fail under the // trimmable typemap. These cannot use [Category("TrimmableIgnore")] because // we don't control that assembly — they must be excluded by name here. - ExcludedTestNames = new [] { + ExcludedTestNames = [ // net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK "Java.InteropTests.InvokeVirtualFromConstructorTests", - // net.dot.jni.internal.JavaProxyObject. calls - // net.dot.jni.ManagedPeer.registerNativeMembers, which the trimmable - // typemap path rejects (Native methods must be registered by JCW - // static initializer blocks). Fixing this requires a parallel - // Android-trimmable variant of JavaProxyObject.java that registers - // its native equals/hashCode/toString via mono.android.Runtime.register - // — an architectural change tracked separately from the JavaCast / JavaAs - // work in this PR. See https://github.com/dotnet/android/issues/11170. + // net.dot.jni.internal.JavaProxyObject Java class not in APK — fixture setup fails (16 tests) "Java.InteropTests.JavaObjectArray_object_ContractTest", - // Same root cause as above (JavaProxyObject static init). + // net.dot.jni.internal.JavaProxyObject Java class not in APK "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericObjectReferenceArgumentState", @@ -57,6 +50,11 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // root cause as the JavaProxyObject exclusions above. "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", + // IJavaInterfaceInvoker ctor trimmed / missing JavaPeerProxy for test types + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs", + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_Exceptions", + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull", + // JNI method remapping not supported in trimmable typemap "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName", "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod", @@ -66,12 +64,15 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // Throwable subclass registration "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", + // Typemap doesn't resolve most-derived type + "Java.LangTests.ObjectTest.GetObject_ReturnsMostDerivedType", + // Instance identity after JNI round-trip "Java.LangTests.ObjectTest.JnienvCreateInstance_RegistersMultipleInstances", // Global ref leak when inflating custom views "Xamarin.Android.RuntimeTests.CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs", - }; + ]; } } @@ -87,4 +88,4 @@ protected override IList GetTestAssemblies() }; } } -} +} \ No newline at end of file