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..4d03cd4b912 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs @@ -0,0 +1,616 @@ +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 679423576f2..5cea4065665 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -125,6 +125,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). /// @@ -149,7 +156,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". @@ -160,11 +167,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 { @@ -174,7 +189,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; } @@ -187,6 +202,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; } } /// @@ -211,6 +273,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 79570a14bda..a7d823c71ff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -270,6 +270,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash }, IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, + CannotRegisterInStaticConstructor = peer.CannotRegisterInStaticConstructor, }; if (peer.InvokerTypeName != null) { @@ -317,6 +318,14 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? 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++; } @@ -332,6 +341,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 417b1096f60..64b9ad67564 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -91,6 +91,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _iJavaPeerableSetPeerReferenceRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; MemberReferenceHandle _ucoAttrCtorRef; @@ -119,6 +120,8 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; + ExportMethodDispatchEmitter? _exportMethodDispatchEmitter; + /// /// Creates a new emitter. /// @@ -293,6 +296,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. @@ -430,6 +443,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) { @@ -524,7 +565,9 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary. + if (uco.HasMatchingManagedCtor) { + handle = EmitUserVisibleCtorWrapper (uco, targetTypeRef, encodeSig); + AddUnmanagedCallersOnlyAttribute (handle); + return handle; + } + var ctorRef = AddActivationCtorRef ( activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); @@ -996,6 +1074,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: diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 1db8dfd8309..cd084d8eb36 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -538,6 +538,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 bcc45d1b1c5..7c77c20eb96 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -183,10 +183,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. /// @@ -255,6 +284,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 2e09766a2fa..7fb3dcd1c35 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; @@ -245,7 +246,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, @@ -275,8 +276,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) @@ -651,14 +660,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). @@ -758,8 +902,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); } } @@ -863,7 +1010,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) { @@ -873,8 +1020,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); @@ -887,6 +1042,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, @@ -1030,6 +1193,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; } @@ -1041,24 +1219,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 (); } @@ -1070,8 +1324,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 }, @@ -1084,23 +1338,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;"; } @@ -1500,7 +1787,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; @@ -1508,16 +1795,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. @@ -1543,8 +1877,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 07d689854bb..edff0a10244 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -44,7 +44,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); @@ -61,7 +62,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); @@ -269,8 +270,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": @@ -282,17 +282,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; } @@ -306,13 +302,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 ec8ad004c88..ed2ff742555 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 @@ -26,6 +26,11 @@ Value="true" Trim="true" /> + + + true + @@ -40,6 +45,8 @@