From ed356813d2fa91a91f433f418733c5aa6168433c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:31:02 +0100 Subject: [PATCH 01/43] [TrimmableTypeMap] Data model and metadata type providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the TrimmableTypeMap scanner project (netstandard2.0) with: - JavaPeerInfo, MarshalMethodInfo, ActivationCtorInfo records — the core data model representing Java peer types discovered in assemblies - SignatureTypeProvider — decodes method signatures from metadata to extract parameter types for marshal method and activation constructor matching - CustomAttributeTypeProvider — decodes custom attribute arguments with lazy enum type caching and correct nested type resolution - CompilerFeaturePolyfills — netstandard2.0 shims for required/init - NullableExtensions — IsNullOrEmpty/IsNullOrWhiteSpace helpers - System.Reflection.Metadata 11.0.0-preview.1 package reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 1 + .../CompilerFeaturePolyfills.cs | 28 +++ ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 16 ++ .../Scanner/CustomAttributeTypeProvider.cs | 103 +++++++++++ .../Scanner/JavaPeerInfo.cs | 173 ++++++++++++++++++ .../Scanner/MetadataTypeNameResolver.cs | 39 ++++ .../Scanner/SignatureTypeProvider.cs | 66 +++++++ 7 files changed, 426 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs diff --git a/eng/Versions.props b/eng/Versions.props index d87b6a2fe34..bf1e2b58feb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -16,6 +16,7 @@ 11.0.100-preview.1.26076.102 0.11.5-preview.26076.102 9.0.4 + 11.0.0-preview.1.26104.118 36.1.30 $(MicrosoftNETSdkAndroidManifest100100PackageVersion) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs new file mode 100644 index 00000000000..6b5fc126876 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs @@ -0,0 +1,28 @@ +// Polyfills for C# language features on netstandard2.0 + +// Required for init-only setters +namespace System.Runtime.CompilerServices +{ + static class IsExternalInit { } + + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed class RequiredMemberAttribute : Attribute { } + + [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] + sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute (string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + public bool IsOptional { get; init; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage (AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + sealed class SetsRequiredMembersAttribute : Attribute { } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj new file mode 100644 index 00000000000..270911827f4 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -0,0 +1,16 @@ + + + + + $(TargetFrameworkNETStandard) + enable + Nullable + Microsoft.Android.Sdk.TrimmableTypeMap + + + + + + + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs new file mode 100644 index 00000000000..246d13f3760 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ICustomAttributeTypeProvider implementation for decoding +/// custom attribute values via System.Reflection.Metadata. +/// +sealed class CustomAttributeTypeProvider (MetadataReader reader) : ICustomAttributeTypeProvider +{ + Dictionary? enumTypeCache; + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); + + public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromDefinition (metadataReader, handle, rawTypeKind); + + public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromReference (metadataReader, handle, rawTypeKind); + + public string GetTypeFromSerializedName (string name) => name; + + public PrimitiveTypeCode GetUnderlyingEnumType (string type) + { + if (enumTypeCache == null) { + enumTypeCache = BuildEnumTypeCache (); + } + + if (enumTypeCache.TryGetValue (type, out var code)) { + return code; + } + + // Default to Int32 for enums defined in other assemblies + return PrimitiveTypeCode.Int32; + } + + Dictionary BuildEnumTypeCache () + { + var cache = new Dictionary (); + + foreach (var typeHandle in reader.TypeDefinitions) { + var typeDef = reader.GetTypeDefinition (typeHandle); + + // Only process enum types + if (!IsEnum (typeDef)) + continue; + + var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0); + var code = GetEnumUnderlyingTypeCode (typeDef); + cache [fullName] = code; + } + + return cache; + } + + bool IsEnum (TypeDefinition typeDef) + { + var baseType = typeDef.BaseType; + if (baseType.IsNil) + return false; + + string? baseFullName = baseType.Kind switch { + HandleKind.TypeReference => GetTypeFromReference (reader, (TypeReferenceHandle)baseType, rawTypeKind: 0), + HandleKind.TypeDefinition => GetTypeFromDefinition (reader, (TypeDefinitionHandle)baseType, rawTypeKind: 0), + _ => null, + }; + + return baseFullName == "System.Enum"; + } + + PrimitiveTypeCode GetEnumUnderlyingTypeCode (TypeDefinition typeDef) + { + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; + } + + return PrimitiveTypeCode.Int32; + } + + public string GetSystemType () => "System.Type"; + + public string GetSZArrayType (string elementType) => $"{elementType}[]"; + + public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs new file mode 100644 index 00000000000..85472f1b3ba --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Represents a Java peer type discovered during assembly scanning. +/// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). +/// Generators consume this data model — they never touch PEReader/MetadataReader. +/// +sealed record JavaPeerInfo +{ + /// + /// JNI type name, e.g., "android/app/Activity". + /// Extracted from the [Register] attribute. + /// + public required string JavaName { get; init; } + + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public required string CompatJniName { get; init; } + + /// + /// Full managed type name, e.g., "Android.App.Activity". + /// + public required string ManagedTypeName { get; init; } + + /// + /// Assembly name the type belongs to, e.g., "Mono.Android". + /// + public required string AssemblyName { get; init; } + + /// + /// JNI name of the base Java type, e.g., "android/app/Activity" for a type + /// that extends Activity. Null for java/lang/Object or types without a Java base. + /// Needed by JCW Java source generation ("extends" clause). + /// + public string? BaseJavaName { get; init; } + + /// + /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. + /// Needed by JCW Java source generation ("implements" clause). + /// + public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); + + public bool IsInterface { get; init; } + public bool IsAbstract { get; init; } + + /// + /// If true, this is a Managed Callable Wrapper (MCW) binding type. + /// No JCW or RegisterNatives will be generated for it. + /// + public bool DoNotGenerateAcw { get; init; } + + /// + /// Types with component attributes ([Activity], [Service], etc.), + /// custom views from layout XML, or manifest-declared components + /// are unconditionally preserved (not trimmable). + /// + public bool IsUnconditional { get; init; } + + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); + + /// + /// Information about the activation constructor for this type. + /// May reference a base type's constructor if the type doesn't define its own. + /// + public ActivationCtorInfo? ActivationCtor { get; init; } + + /// + /// For interfaces and abstract types, the name of the invoker type + /// used to instantiate instances from Java. + /// + public string? InvokerTypeName { get; init; } + + /// + /// True if this is an open generic type definition. + /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. + /// + public bool IsGenericDefinition { get; init; } +} + +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed record MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public required string JniName { get; init; } + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public required string JniSignature { get; init; } + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; init; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public required string ManagedMethodName { get; init; } + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; init; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; init; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; init; } +} + +/// +/// Describes how to call the activation constructor for a Java peer type. +/// +sealed record ActivationCtorInfo +{ + /// + /// The type that declares the activation constructor. + /// May be the type itself or a base type. + /// + public required string DeclaringTypeName { get; init; } + + /// + /// The assembly containing the declaring type. + /// + public required string DeclaringAssemblyName { get; init; } + + /// + /// The style of activation constructor found. + /// + public required ActivationCtorStyle Style { get; init; } +} + +enum ActivationCtorStyle +{ + /// + /// Xamarin.Android style: (IntPtr handle, JniHandleOwnership transfer) + /// + XamarinAndroid, + + /// + /// Java.Interop style: (ref JniObjectReference reference, JniObjectReferenceOptions options) + /// + JavaInterop, +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs new file mode 100644 index 00000000000..dcc3d5b7db9 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs @@ -0,0 +1,39 @@ +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Shared logic for resolving fully qualified type names from metadata handles. +/// Used by both and . +/// +static class MetadataTypeNameResolver +{ + public static string GetFullName (TypeDefinition typeDef, MetadataReader reader) + { + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + if (typeDef.IsNested) { + var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); + var parentName = GetFullName (declaringType, reader); + return $"{parentName}+{name}"; + } + return ns.Length > 0 ? $"{ns}.{name}" : name; + } + + public static string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + return GetFullName (reader.GetTypeDefinition (handle), reader); + } + + public static string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference (handle); + var name = reader.GetString (typeRef.Name); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return $"{parent}+{name}"; + } + var ns = reader.GetString (typeRef.Namespace); + return ns.Length > 0 ? $"{ns}.{name}" : name; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs new file mode 100644 index 00000000000..87ed078adf2 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Minimal ISignatureTypeProvider implementation for decoding method +/// signatures via System.Reflection.Metadata. +/// Returns fully qualified type name strings. +/// +sealed class SignatureTypeProvider : ISignatureTypeProvider +{ + public static readonly SignatureTypeProvider Instance = new (); + + public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch { + PrimitiveTypeCode.Void => "System.Void", + PrimitiveTypeCode.Boolean => "System.Boolean", + PrimitiveTypeCode.Char => "System.Char", + PrimitiveTypeCode.SByte => "System.SByte", + PrimitiveTypeCode.Byte => "System.Byte", + PrimitiveTypeCode.Int16 => "System.Int16", + PrimitiveTypeCode.UInt16 => "System.UInt16", + PrimitiveTypeCode.Int32 => "System.Int32", + PrimitiveTypeCode.UInt32 => "System.UInt32", + PrimitiveTypeCode.Int64 => "System.Int64", + PrimitiveTypeCode.UInt64 => "System.UInt64", + PrimitiveTypeCode.Single => "System.Single", + PrimitiveTypeCode.Double => "System.Double", + PrimitiveTypeCode.String => "System.String", + PrimitiveTypeCode.Object => "System.Object", + PrimitiveTypeCode.IntPtr => "System.IntPtr", + PrimitiveTypeCode.UIntPtr => "System.UIntPtr", + PrimitiveTypeCode.TypedReference => "System.TypedReference", + _ => typeCode.ToString (), + }; + + public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromDefinition (reader, handle, rawTypeKind); + + public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => MetadataTypeNameResolver.GetTypeFromReference (reader, handle, rawTypeKind); + + public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var typeSpec = reader.GetTypeSpecification (handle); + return typeSpec.DecodeSignature (this, genericContext); + } + + public string GetSZArrayType (string elementType) => $"{elementType}[]"; + public string GetArrayType (string elementType, ArrayShape shape) => $"{elementType}[{new string (',', shape.Rank - 1)}]"; + public string GetByReferenceType (string elementType) => $"{elementType}&"; + public string GetPointerType (string elementType) => $"{elementType}*"; + public string GetPinnedType (string elementType) => elementType; + public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + + public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) + { + return $"{genericType}<{string.Join (",", typeArguments)}>"; + } + + public string GetGenericTypeParameter (object? genericContext, int index) => $"!{index}"; + public string GetGenericMethodParameter (object? genericContext, int index) => $"!!{index}"; + + public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; +} From 9db2c36279c216a8c11f4c899058e504894090f7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:31:13 +0100 Subject: [PATCH 02/43] [TrimmableTypeMap] AssemblyIndex per-assembly metadata indexer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AssemblyIndex — the first phase of the scanner pipeline that reads a single assembly and indexes all Java peer metadata: - Discovers [Register], [Export] attributes on types and methods - Builds RegisterInfo/ExportInfo records from custom attribute blobs - Resolves TypeAttributeInfo for component attributes ([Activity], [Service], [BroadcastReceiver], [ContentProvider]) including their JNI name properties - Maps type definitions to their Java peer registration data for downstream consumption by JavaPeerScanner Key design: uses System.Reflection.Metadata directly (no Cecil) and produces immutable record types. The index is per-assembly so scanning can be parallelized across the app closure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs new file mode 100644 index 00000000000..8ef2217cb8e --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions, +/// all subsequent lookups are O(1) dictionary lookups. +/// +sealed class AssemblyIndex : IDisposable +{ + readonly PEReader peReader; + internal readonly CustomAttributeTypeProvider customAttributeTypeProvider; + + public MetadataReader Reader { get; } + public string AssemblyName { get; } + public string FilePath { get; } + + /// + /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. + /// + public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal); + + /// + /// Cached [Register] attribute data per type. + /// + public Dictionary RegisterInfoByType { get; } = new (); + + /// + /// All custom attribute data per type, pre-parsed for the attributes we care about. + /// + public Dictionary AttributesByType { get; } = new (); + + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath) + { + this.peReader = peReader; + this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); + Reader = reader; + AssemblyName = assemblyName; + FilePath = filePath; + } + + public static AssemblyIndex Create (string filePath) + { + var peReader = new PEReader (File.OpenRead (filePath)); + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); + index.Build (); + return index; + } + + void Build () + { + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader); + if (fullName.Length == 0) { + continue; + } + + TypesByFullName [fullName] = typeHandle; + + var (registerInfo, attrInfo) = ParseAttributes (typeDef); + + if (attrInfo is not null) { + AttributesByType [typeHandle] = attrInfo; + } + + if (registerInfo is not null) { + RegisterInfoByType [typeHandle] = registerInfo; + } + } + } + + (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) + { + RegisterInfo? registerInfo = null; + TypeAttributeInfo? attrInfo = null; + + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = Reader.GetCustomAttribute (caHandle); + var attrName = GetCustomAttributeName (ca, Reader); + + if (attrName is null) { + continue; + } + + if (attrName == "RegisterAttribute") { + registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); + } else if (attrName == "ExportAttribute") { + // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner + } else if (IsKnownComponentAttribute (attrName)) { + attrInfo ??= CreateTypeAttributeInfo (attrName); + var componentName = TryGetNameProperty (ca); + if (componentName is not null) { + attrInfo.JniName = componentName.Replace ('.', '/'); + } + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); + } + } + } + + return (registerInfo, attrInfo); + } + + static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { + "ActivityAttribute", + "ServiceAttribute", + "BroadcastReceiverAttribute", + "ContentProviderAttribute", + "ApplicationAttribute", + "InstrumentationAttribute", + }; + + static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) + { + return attrName == "ApplicationAttribute" + ? new ApplicationAttributeInfo () + : new TypeAttributeInfo (attrName); + } + + static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName); + + internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) + { + if (ca.Constructor.Kind == HandleKind.MemberReference) { + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeReference) { + var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent); + return reader.GetString (typeRef.Name); + } + } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) { + var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); + var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + return reader.GetString (declaringType.Name); + } + return null; + } + + internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider) + { + var value = ca.DecodeValue (provider); + + string jniName = ""; + string? signature = null; + string? connector = null; + bool doNotGenerateAcw = false; + + if (value.FixedArguments.Length > 0) { + jniName = (string?)value.FixedArguments [0].Value ?? ""; + } + if (value.FixedArguments.Length > 1) { + signature = (string?)value.FixedArguments [1].Value; + } + if (value.FixedArguments.Length > 2) { + connector = (string?)value.FixedArguments [2].Value; + } + + if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { + doNotGenerateAcw = doNotGenerateAcwValue; + } + + return new RegisterInfo { + JniName = jniName, + Signature = signature, + Connector = connector, + DoNotGenerateAcw = doNotGenerateAcw, + }; + } + + string? TryGetTypeProperty (CustomAttribute ca, string propertyName) + { + var value = ca.DecodeValue (customAttributeTypeProvider); + var typeName = TryGetNamedArgument (value, propertyName); + if (!string.IsNullOrEmpty (typeName)) { + return typeName; + } + return null; + } + + string? TryGetNameProperty (CustomAttribute ca) + { + var value = ca.DecodeValue (customAttributeTypeProvider); + + // Check named arguments first (e.g., [Activity(Name = "...")]) + var name = TryGetNamedArgument (value, "Name"); + if (!string.IsNullOrEmpty (name)) { + return name; + } + + // Fall back to first constructor argument (e.g., [CustomJniName("...")]) + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) { + return ctorName; + } + + return null; + } + + static T? TryGetNamedArgument (CustomAttributeValue value, string argumentName) where T : class + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is T typedValue) { + return typedValue; + } + } + return null; + } + + static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue) + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is bool boolValue) { + argumentValue = boolValue; + return true; + } + } + + argumentValue = false; + return false; + } + + public void Dispose () + { + peReader.Dispose (); + } +} + +/// +/// Parsed [Register] attribute data for a type or method. +/// +sealed record RegisterInfo +{ + public required string JniName { get; init; } + public string? Signature { get; init; } + public string? Connector { get; init; } + public bool DoNotGenerateAcw { get; init; } +} + +/// +/// Parsed [Export] attribute data for a method. +/// +sealed record ExportInfo +{ + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } +} + +class TypeAttributeInfo (string attributeName) +{ + public string AttributeName { get; } = attributeName; + public string? JniName { get; set; } +} + +sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") +{ + public string? BackupAgent { get; set; } + public string? ManageSpaceActivity { get; set; } +} From 0bd2fd63706aadc4444a92d472b9275bdfc021cf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:31:24 +0100 Subject: [PATCH 03/43] [TrimmableTypeMap] JavaPeerScanner execution logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add JavaPeerScanner — the second phase that consumes AssemblyIndex results and produces the final list of JavaPeerInfo for the app: - Walks all indexed types and resolves their Java peer registrations - Handles inheritance: traverses base types across assemblies to find the nearest registered Java peer ancestor - Detects activation constructors (IntPtr+JniHandleOwnership) and distinguishes between direct declarations and inherited ones - Collects marshal methods ([Register] on methods) and exported methods ([Export]) with their JNI signatures - Merges component attribute metadata ([Activity], etc.) and resolves JNI names from attribute properties - Flags types for unconditional preservation when component attributes with non-default JNI names are present The scanner is designed as a pure function: assemblies in → peer info out, with no side effects or global state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompilerFeaturePolyfills.cs | 9 +- .../Scanner/JavaPeerScanner.cs | 735 ++++++++++++++++++ 2 files changed, 737 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs index 6b5fc126876..c33ab5025c2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs @@ -9,14 +9,9 @@ static class IsExternalInit { } sealed class RequiredMemberAttribute : Attribute { } [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] - sealed class CompilerFeatureRequiredAttribute : Attribute + sealed class CompilerFeatureRequiredAttribute (string featureName) : Attribute { - public CompilerFeatureRequiredAttribute (string featureName) - { - FeatureName = featureName; - } - - public string FeatureName { get; } + public string FeatureName { get; } = featureName; public bool IsOptional { get; init; } } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs new file mode 100644 index 00000000000..bfe158f24cc --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -0,0 +1,735 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Scans assemblies for Java peer types using System.Reflection.Metadata. +/// Two-phase architecture: +/// Phase 1: Build per-assembly indices (fast, O(1) lookups) +/// Phase 2: Analyze types using cached indices +/// +sealed class JavaPeerScanner : IDisposable +{ + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); + readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + + /// + /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. + /// Checks the specified assembly (by name) in the assembly cache. + /// + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) + { + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && + resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { + return true; + } + handle = default; + resolvedIndex = null; + return false; + } + + /// + /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling + /// nested types whose ResolutionScope is another TypeReference. + /// + static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index) + { + var typeRef = index.Reader.GetTypeReference (handle); + var name = index.Reader.GetString (typeRef.Name); + var ns = index.Reader.GetString (typeRef.Namespace); + + var scope = typeRef.ResolutionScope; + switch (scope.Kind) { + case HandleKind.AssemblyReference: { + var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); + var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; + return (fullName, index.Reader.GetString (asmRef.Name)); + } + case HandleKind.TypeReference: { + // Nested type: recurse to get the declaring type's full name and assembly + var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); + return ($"{parentFullName}+{name}", assemblyName); + } + default: { + var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; + return (fullName, index.AssemblyName); + } + } + } + + /// + /// Looks up the [Register] JNI name for a type identified by name + assembly. + /// + string? ResolveRegisterJniName (string typeName, string assemblyName) + { + if (TryResolveType (typeName, assemblyName, out var handle, out var resolvedIndex) && + resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo)) { + return regInfo.JniName; + } + return null; + } + + /// + /// Phase 1: Build indices for all assemblies. + /// Phase 2: Scan all types and produce JavaPeerInfo records. + /// + public List Scan (IReadOnlyList assemblyPaths) + { + // Phase 1: Build indices for all assemblies + foreach (var path in assemblyPaths) { + var index = AssemblyIndex.Create (path); + assemblyCache [index.AssemblyName] = index; + } + + // Phase 2: Analyze types using cached indices + var resultsByManagedName = new Dictionary (StringComparer.Ordinal); + + foreach (var index in assemblyCache.Values) { + ScanAssembly (index, resultsByManagedName); + } + + // Phase 3: Force unconditional on types referenced by [Application] attributes + ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); + + return new List (resultsByManagedName.Values); + } + + /// + /// Types referenced by [Application(BackupAgent = typeof(X))] or + /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, + /// because the manifest will reference them even if nothing else does. + /// + static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) + { + foreach (var index in assemblyCache.Values) { + foreach (var attrInfo in index.AttributesByType.Values) { + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); + } + } + } + } + + static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) + { + if (managedTypeName is null) { + return; + } + + managedTypeName = managedTypeName.Trim (); + if (managedTypeName.Length == 0) { + return; + } + + // Try exact match first (handles both plain and assembly-qualified names) + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; + return; + } + + // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." + // Strip to just the type name for lookup + var commaIndex = managedTypeName.IndexOf (','); + if (commaIndex <= 0) { + return; + } + + var typeName = managedTypeName.Substring (0, commaIndex).Trim (); + if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { + resultsByManagedName [typeName] = peer with { IsUnconditional = true }; + } + } + + void ScanAssembly (AssemblyIndex index, Dictionary results) + { + foreach (var typeHandle in index.Reader.TypeDefinitions) { + var typeDef = index.Reader.GetTypeDefinition (typeHandle); + + // Skip module-level types + if (index.Reader.GetString (typeDef.Name) == "") { + continue; + } + + // Determine the JNI name and whether this is a known Java peer. + // Priority: + // 1. [Register] attribute → use JNI name from attribute + // 2. Component attribute Name property → convert dots to slashes + // 3. Extends a known Java peer → auto-compute JNI name via CRC64 + // 4. None of the above → not a Java peer, skip + string? jniName = null; + string? compatJniName = null; + bool doNotGenerateAcw = false; + + index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); + index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); + + if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + jniName = registerInfo.JniName; + compatJniName = jniName; + doNotGenerateAcw = registerInfo.DoNotGenerateAcw; + } else if (attrInfo?.JniName is not null) { + // User type with [Activity(Name = "...")] but no [Register] + jniName = attrInfo.JniName; + compatJniName = jniName; + } else { + // No explicit JNI name — check if this type extends a known Java peer. + // If so, auto-compute JNI name from the managed type name via CRC64. + if (ExtendsJavaPeer (typeDef, index)) { + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); + } else { + continue; + } + } + + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + + var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; + var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; + var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; + + var isUnconditional = attrInfo is not null; + string? invokerTypeName = null; + + // Resolve base Java type name + var baseJavaName = ResolveBaseJavaName (typeDef, index, results); + + // Resolve implemented Java interface names + var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); + + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + + // Resolve activation constructor + var activationCtor = ResolveActivationCtor (fullName, typeDef, index); + + // For interfaces/abstract types, try to find invoker type name + if (isInterface || isAbstract) { + invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); + } + + var peer = new JavaPeerInfo { + JavaName = jniName, + CompatJniName = compatJniName, + ManagedTypeName = fullName, + AssemblyName = index.AssemblyName, + BaseJavaName = baseJavaName, + ImplementedInterfaceJavaNames = implementedInterfaces, + IsInterface = isInterface, + IsAbstract = isAbstract, + DoNotGenerateAcw = doNotGenerateAcw, + IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, + ActivationCtor = activationCtor, + InvokerTypeName = invokerTypeName, + IsGenericDefinition = isGenericDefinition, + }; + + results [fullName] = peer; + } + } + + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister is null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature is null && registerInfo.Connector is null) { + return; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, + }); + } + + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is null) { + return null; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + // First try [Register] attribute + var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); + if (registerJniName is not null) { + return registerJniName; + } + + // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + if (results.TryGetValue (baseTypeName, out var basePeer)) { + return basePeer.JavaName; + } + + return null; + } + + List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index) + { + var result = new List (); + var interfaceImpls = typeDef.GetInterfaceImplementations (); + + foreach (var implHandle in interfaceImpls) { + var impl = index.Reader.GetInterfaceImplementation (implHandle); + var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); + if (ifaceJniName is not null) { + result.Add (ifaceJniName); + } + } + + return result; + } + + string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) + { + var resolved = ResolveEntityHandle (interfaceHandle, index); + return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + } + + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + { + exportInfo = null; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + return true; + } + + if (attrName == "ExportAttribute") { + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; + } + } + registerInfo = null; + return false; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + } + } + return null; + } + + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = ca.DecodeValue (index.customAttributeTypeProvider); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (string.IsNullOrEmpty (exportName)) { + exportName = index.Reader.GetString (methodDef.Name); + } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + } + return "Ljava/lang/Object;"; + } + } + + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) + { + var cacheKey = (typeName, index.AssemblyName); + if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { + return cached; + } + + // Check this type's constructors + var ownCtor = FindActivationCtorOnType (typeDef, index); + if (ownCtor is not null) { + var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; + activationCtorCache [cacheKey] = info; + return info; + } + + // Walk base type hierarchy + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is not null) { + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); + if (result is not null) { + activationCtorCache [cacheKey] = result; + } + return result; + } + } + + return null; + } + + static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var method = index.Reader.GetMethodDefinition (methodHandle); + var name = index.Reader.GetString (method.Name); + + if (name != ".ctor") { + continue; + } + + var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + + // XI style: (IntPtr, JniHandleOwnership) + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0] == "System.IntPtr" && + sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") { + return ActivationCtorStyle.XamarinAndroid; + } + + // JI style: (ref JniObjectReference, JniObjectReferenceOptions) + if (sig.ParameterTypes.Length == 2 && + (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") && + sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") { + return ActivationCtorStyle.JavaInterop; + } + } + + return null; + } + + /// + /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying + /// type's (fullName, assemblyName) by reading the raw signature blob. + /// + static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) + { + var typeSpec = index.Reader.GetTypeSpecification (specHandle); + var blobReader = index.Reader.GetBlobReader (typeSpec.Signature); + + // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args... + var elementType = blobReader.ReadByte (); + if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST + return null; + } + + var classOrValueType = blobReader.ReadByte (); + if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE + return null; + } + + // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec) + var codedToken = blobReader.ReadCompressedInteger (); + var tag = codedToken & 0x3; + var row = codedToken >> 2; + + switch (tag) { + case 0: { // TypeDef + var handle = MetadataTokens.TypeDefinitionHandle (row); + var baseDef = index.Reader.GetTypeDefinition (handle); + return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); + } + case 1: // TypeRef + return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); + default: + return null; + } + } + + /// + /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName). + /// Shared by base type resolution, interface resolution, and any handle-to-name lookup. + /// + (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) + { + switch (handle.Kind) { + case HandleKind.TypeDefinition: { + var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); + return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); + } + case HandleKind.TypeReference: + return ResolveTypeReference ((TypeReferenceHandle)handle, index); + case HandleKind.TypeSpecification: + return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index); + default: + return null; + } + } + + (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index) + { + return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); + } + + string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) + { + // First, check the [Register] attribute's connector arg (3rd arg). + // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] + // where the connector contains the assembly-qualified invoker type name. + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { + var connector = registerInfo.Connector; + // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." + // We want just the type name (before the first comma, if any) + var commaIndex = connector.IndexOf (','); + if (commaIndex > 0) { + return connector.Substring (0, commaIndex).Trim (); + } + if (connector.Length > 0) { + return connector; + } + } + + // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" + var invokerName = $"{typeName}Invoker"; + if (index.TypesByFullName.ContainsKey (invokerName)) { + return invokerName; + } + return null; + } + + public void Dispose () + { + foreach (var index in assemblyCache.Values) { + index.Dispose (); + } + assemblyCache.Clear (); + } + + readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + + /// + /// Check if a type extends a known Java peer (has [Register] or component attribute) + /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles. + /// + bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) + { + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var key = $"{index.AssemblyName}:{fullName}"; + + if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { + return cached; + } + + // Mark as false to prevent cycles, then compute + extendsJavaPeerCache [key] = false; + + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is null) { + return false; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { + return false; + } + + // Direct hit: base has [Register] or component attribute + if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) { + extendsJavaPeerCache [key] = true; + return true; + } + if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { + extendsJavaPeerCache [key] = true; + return true; + } + + // Recurse up the hierarchy + var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + var result = ExtendsJavaPeer (baseDef, baseIndex); + extendsJavaPeerCache [key] = result; + return result; + } + + /// + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. + /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. + /// Generic backticks are replaced with _. + /// + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + { + var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); + + if (parentJniName is not null) { + var name = $"{parentJniName}_{typeName}"; + return (name, name); + } + + var packageName = GetCrc64PackageName (ns, index.AssemblyName); + var jniName = $"{packageName}/{typeName}"; + + string compatName = ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + + return (jniName, compatName); + } + + /// + /// Builds the type name part (handling nesting) and returns either a parent's + /// registered JNI name or the outermost namespace. + /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types + /// and if a parent has [Register] or a component attribute JNI name, uses that + /// as prefix instead of computing CRC64 from the namespace. + /// + static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) + { + var firstName = index.Reader.GetString (typeDef.Name).Replace ('`', '_'); + + // Fast path: non-nested types (the vast majority) + if (!typeDef.IsNested) { + return (firstName, null, index.Reader.GetString (typeDef.Namespace)); + } + + // Nested type: walk up declaring types, collecting name parts + var nameParts = new List (4) { firstName }; + var current = typeDef; + string? parentJniName = null; + + do { + var parentHandle = current.GetDeclaringType (); + current = index.Reader.GetTypeDefinition (parentHandle); + + // Check if the parent has a registered JNI name + if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !string.IsNullOrEmpty (parentRegister.JniName)) { + parentJniName = parentRegister.JniName; + break; + } + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { + parentJniName = parentAttr.JniName; + break; + } + + nameParts.Add (index.Reader.GetString (current.Name).Replace ('`', '_')); + } while (current.IsNested); + + nameParts.Reverse (); + var typeName = string.Join ("_", nameParts); + var ns = index.Reader.GetString (current.Namespace); + + return (typeName, parentJniName, ns); + } + + static string GetCrc64PackageName (string ns, string assemblyName) + { + // Only Mono.Android preserves the namespace directly + if (assemblyName == "Mono.Android") { + return ns.ToLowerInvariant ().Replace ('.', '/'); + } + + var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); + var hash = System.IO.Hashing.Crc64.Hash (data); + return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + } +} From e7a3483f8677f3d4242a941ad64280351c29572d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:42:43 +0100 Subject: [PATCH 04/43] [TrimmableTypeMap] Test fixtures and foundational scanner tests Add test infrastructure for the Java peer scanner: - TestFixtures project with stub Mono.Android attributes ([Register], [Activity], [Service], etc.) and test types covering MCW bindings, user types, generics, nested types, interfaces, and component types - JavaPeerScannerTests with test helpers (ScanFixtures, FindByJavaName) and foundational assertions: type discovery, DoNotGenerateAcw flags, component unconditional marking, interface/abstract/generic metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 1 + ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 38 ++ .../Scanner/JavaPeerScannerTests.cs | 109 ++++++ .../TestFixtures/StubAttributes.cs | 119 ++++++ .../TestFixtures/TestFixtures.csproj | 13 + .../TestFixtures/TestTypes.cs | 341 ++++++++++++++++++ 6 files changed, 621 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 270911827f4..48a5f75728d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj new file mode 100644 index 00000000000..6370a77e680 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.Tests + + + + + + + + + + + + + + + + + + false + + + + + + + <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs new file mode 100644 index 00000000000..bc2b6195f22 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + JavaPeerInfo FindByJavaName (List peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + JavaPeerInfo FindByManagedName (List peers, string managedName) + { + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + [Fact] + public void Scan_FindsAllJavaPeerTypes () + { + var peers = ScanFixtures (); + Assert.NotEmpty (peers); + Assert.Contains (peers, p => p.JavaName == "java/lang/Object"); + Assert.Contains (peers, p => p.JavaName == "android/app/Activity"); + Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity"); + } + + [Theory] + [InlineData ("android/app/Activity", true)] + [InlineData ("android/widget/Button", true)] + [InlineData ("my/app/MainActivity", false)] + public void Scan_DoNotGenerateAcw (string javaName, bool expected) + { + var peers = ScanFixtures (); + Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw); + } + + [Theory] + [InlineData ("my/app/MainActivity", true)] + [InlineData ("my/app/MyService", true)] + [InlineData ("my/app/MyReceiver", true)] + [InlineData ("my/app/MyProvider", true)] + [InlineData ("my/app/MyApplication", true)] + [InlineData ("my/app/MyInstrumentation", true)] + [InlineData ("my/app/MyBackupAgent", true)] + [InlineData ("my/app/MyManageSpaceActivity", true)] + [InlineData ("my/app/MyHelper", false)] + [InlineData ("android/app/Activity", false)] + public void Scan_IsUnconditional (string javaName, bool expected) + { + var peers = ScanFixtures (); + Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional); + } + + [Fact] + public void Scan_TypeMetadata_IsCorrect () + { + var peers = ScanFixtures (); + Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract); + Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface); + Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw); + + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + } + + [Fact] + public void Scan_InvokerAndInterface_ShareJavaName () + { + var peers = ScanFixtures (); + var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickListenerPeers.Count); + Assert.Contains (clickListenerPeers, p => p.IsInterface); + Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + } + + [Fact] + public void Scan_AllTypes_HaveAssemblyName () + { + var peers = ScanFixtures (); + Assert.All (peers, peer => + Assert.False (string.IsNullOrEmpty (peer.AssemblyName), + $"Type {peer.ManagedTypeName} should have assembly name")); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs new file mode 100644 index 00000000000..36c7587eb28 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -0,0 +1,119 @@ +using System; + +namespace Java.Interop +{ + public interface IJniNameProviderAttribute + { + string Name { get; } + } +} + +namespace Android.Runtime +{ + [AttributeUsage ( + AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | + AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, + AllowMultiple = false)] + public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public string? Signature { get; set; } + public string? Connector { get; set; } + public bool DoNotGenerateAcw { get; set; } + public int ApiSince { get; set; } + + public RegisterAttribute (string name) => Name = name; + + public RegisterAttribute (string name, string signature, string connector) + { + Name = name; + Signature = signature; + Connector = connector; + } + } + + public enum JniHandleOwnership + { + DoNotTransfer = 0, + TransferLocalRef = 1, + TransferGlobalRef = 2, + } +} + +namespace Android.App +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class ActivityAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public bool MainLauncher { get; set; } + public string? Label { get; set; } + public string? Icon { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ServiceAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class InstrumentationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ApplicationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public Type? BackupAgent { get; set; } + public Type? ManageSpaceActivity { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } +} + +namespace Android.Content +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class BroadcastReceiverAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string []? Authorities { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + + public ContentProviderAttribute (string [] authorities) => Authorities = authorities; + } +} + +namespace Java.Interop +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportAttribute : Attribute + { + public string? Name { get; set; } + + public ExportAttribute () { } + public ExportAttribute (string name) => Name = name; + } +} + +namespace MyApp +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class CustomJniNameAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public CustomJniNameAttribute (string name) => Name = name; + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj new file mode 100644 index 00000000000..f7f4c72139b --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + Microsoft.Android.Sdk.TrimmableTypeMap.Tests.TestFixtures + + false + true + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs new file mode 100644 index 00000000000..35987f36f93 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -0,0 +1,341 @@ +using System; +using Android.App; +using Android.Content; +using Android.Runtime; + +namespace Java.Lang +{ + [Register ("java/lang/Object", DoNotGenerateAcw = true)] + public class Object + { + public Object () { } + protected Object (IntPtr handle, JniHandleOwnership transfer) { } + } + + [Register ("java/lang/Throwable", DoNotGenerateAcw = true)] + public class Throwable : Object + { + protected Throwable (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")] + public virtual string? Message { get; } + } + + [Register ("java/lang/Exception", DoNotGenerateAcw = true)] + public class Exception : Throwable + { + protected Exception (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.App +{ + [Register ("android/app/Activity", DoNotGenerateAcw = true)] + public class Activity : Java.Lang.Object + { + public Activity () { } + protected Activity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual void OnCreate (object? savedInstanceState) { } + + [Register ("onStart", "()V", "")] + protected virtual void OnStart () { } + } + + [Register ("android/app/Service", DoNotGenerateAcw = true)] + public class Service : Java.Lang.Object + { + protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.App.Backup +{ + [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] + public class BackupAgent : Java.Lang.Object + { + protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.Content +{ + [Register ("android/content/Context", DoNotGenerateAcw = true)] + public class Context : Java.Lang.Object + { + protected Context (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.Views +{ + [Register ("android/view/View", DoNotGenerateAcw = true)] + public class View : Java.Lang.Object + { + protected View (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] + public interface IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] + void OnClick (View v); + } + + [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] + internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + public void OnClick (View v) { } + } + + [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] + public interface IOnLongClickListener + { + [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] + bool OnLongClick (View v); + } +} + +namespace Android.Widget +{ + [Register ("android/widget/Button", DoNotGenerateAcw = true)] + public class Button : Android.Views.View + { + protected Button (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/widget/TextView", DoNotGenerateAcw = true)] + public class TextView : Android.Views.View + { + protected TextView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace MyApp +{ + [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] + public class MainActivity : Android.App.Activity + { + public MainActivity () { } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected override void OnCreate (object? savedInstanceState) => base.OnCreate (savedInstanceState); + } + + [Register ("my/app/MyHelper")] + public class MyHelper : Java.Lang.Object + { + [Register ("doSomething", "()V", "GetDoSomethingHandler")] + public virtual void DoSomething () { } + } + + [Service (Name = "my.app.MyService")] + public class MyService : Android.App.Service + { + protected MyService (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [BroadcastReceiver (Name = "my.app.MyReceiver")] + public class MyReceiver : Java.Lang.Object { } + + [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider")] + public class MyProvider : Java.Lang.Object { } + + [Register ("my/app/AbstractBase")] + public abstract class AbstractBase : Java.Lang.Object + { + protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("doWork", "()V", "")] + public abstract void DoWork (); + } + + [Register ("my/app/SimpleActivity")] + public class SimpleActivity : Android.App.Activity { } + + [Register ("my/app/ClickableView")] + public class ClickableView : Android.Views.View, Android.Views.IOnClickListener + { + protected ClickableView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + } + + [Register ("my/app/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("", "()V", "")] + public CustomView () : base (default!, default) { } + + [Register ("", "(Landroid/content/Context;)V", "")] + public CustomView (Context context) : base (default!, default) { } + } + + [Register ("my/app/Outer")] + public class Outer : Java.Lang.Object + { + [Register ("my/app/Outer$Inner")] + public class Inner : Java.Lang.Object + { + protected Inner (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + } + + [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")] + public interface ICallback + { + [Register ("my/app/ICallback$Result")] + public class Result : Java.Lang.Object + { + protected Result (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + } + + [Register ("my/app/TouchHandler")] + public class TouchHandler : Java.Lang.Object + { + [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")] + public virtual bool OnTouch (Android.Views.View v, int action) => false; + + [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")] + public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) { } + + [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")] + public virtual void OnScroll (int x, float y, long timestamp, double velocity) { } + + [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")] + public virtual string? GetText () => null; + + [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")] + public virtual void SetItems (string[]? items) { } + } + + [Register ("my/app/ExportExample")] + public class ExportExample : Java.Lang.Object + { + [Java.Interop.Export ("myExportedMethod")] + public void MyExportedMethod () { } + } + + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] + public class MyApplication : Java.Lang.Object { } + + [Instrumentation (Name = "my.app.MyInstrumentation")] + public class MyInstrumentation : Java.Lang.Object { } + + [Register ("my/app/MyBackupAgent")] + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/MyManageSpaceActivity")] + public class MyManageSpaceActivity : Android.App.Activity + { + protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + public class UnregisteredHelper : Java.Lang.Object { } + + [Register ("my/app/MyButton")] + public class MyButton : Android.Widget.Button + { + protected MyButton (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/MultiInterfaceView")] + public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener + { + protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + + [Register ("onLongClick", "(Landroid/view/View;)Z", "")] + public bool OnLongClick (Android.Views.View v) => false; + } + + [CustomJniName ("com.example.CustomWidget")] + public class CustomWidget : Java.Lang.Object { } + + [Activity (Name = "my.app.BaseActivityNoRegister")] + public class BaseActivityNoRegister : Android.App.Activity { } + + public class DerivedFromComponentBase : BaseActivityNoRegister { } + + [Register ("my/app/RegisteredParent")] + public class RegisteredParent : Java.Lang.Object + { + public class UnregisteredChild : Java.Lang.Object { } + } + + [Register ("my/app/DeepOuter")] + public class DeepOuter : Java.Lang.Object + { + public class Middle : Java.Lang.Object + { + public class DeepInner : Java.Lang.Object { } + } + } + + public class PlainActivitySubclass : Android.App.Activity { } + + [Activity (Label = "Unnamed")] + public class UnnamedActivity : Android.App.Activity { } + + public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + } + + public class UnregisteredExporter : Java.Lang.Object + { + [Java.Interop.Export ("doExportedWork")] + public void DoExportedWork () { } + } +} + +namespace MyApp.Generic +{ + [Register ("my/app/GenericHolder")] + public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + { + [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] + public virtual T? GetItem () => default; + } + + [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] + public class GenericBase : Java.Lang.Object where T : class + { + protected GenericBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/ConcreteFromGeneric")] + public class ConcreteFromGeneric : GenericBase + { + protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/IGenericCallback", "", "")] + public interface IGenericCallback { } + + [Register ("my/app/GenericCallbackImpl")] + public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback + { + protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +[Register ("my/app/GlobalType")] +public class GlobalType : Java.Lang.Object +{ + protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } +} + +public class GlobalUnregisteredType : Java.Lang.Object { } From f63a8314f33ce40d7e5bdf58d42e1153d69882d0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:42:50 +0100 Subject: [PATCH 05/43] [TrimmableTypeMap] Scanner behavior and contract tests Test marshal method collection, JNI signature decoding, activation constructor resolution, base type chain walking, interface resolution, compat JNI names, and component attribute metadata merging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScannerTests.Behavior.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs new file mode 100644 index 00000000000..81aabe9ad74 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var peers = ScanFixtures (); + var method = FindByJavaName (peers, javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var peers = ScanFixtures (); + + var ctors = FindByJavaName (peers, "my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindByJavaName (peers, "android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); + } + + [Theory] + [InlineData ("android/app/Activity", "Android.App.Activity")] + [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] + [InlineData ("my/app/MyButton", "MyApp.MyButton")] + public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType) + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, javaName); + Assert.NotNull (peer.ActivationCtor); + Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName); + } + + [Theory] + [InlineData ("java/lang/Object", null)] + [InlineData ("android/app/Activity", "java/lang/Object")] + [InlineData ("my/app/MainActivity", "android/app/Activity")] + [InlineData ("java/lang/Throwable", "java/lang/Object")] + [InlineData ("java/lang/Exception", "java/lang/Throwable")] + [InlineData ("my/app/MyButton", "android/widget/Button")] + public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase) + { + var peers = ScanFixtures (); + Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + + Assert.Contains ("android/view/View$OnClickListener", + FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames); + Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); + } + + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + var peers = ScanFixtures (); + Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + Assert.Equal ("com/example/CustomWidget", + FindByManagedName (peers, "MyApp.CustomWidget").JavaName); + } + + [Theory] + [InlineData ("my/app/Outer$Inner", "MyApp.Outer+Inner")] + [InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")] + public void Scan_NestedType_IsDiscovered (string javaName, string managedName) + { + var peers = ScanFixtures (); + Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName); + } +} From 22d57eca268780577333afafc3211b8d2bf21dc8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 16:42:57 +0100 Subject: [PATCH 06/43] [TrimmableTypeMap] Scanner edge-case regression tests Cover generic base/interface type specification resolution, component- only base detection, unregistered nested type naming, deep nesting, empty namespace types, plain subclass CRC64 naming, and unregistered types with interfaces or [Export] methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs new file mode 100644 index 00000000000..ec368ed6a44 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Fact] + public void Scan_GenericTypes_ResolveViaTypeSpecification () + { + var peers = ScanFixtures (); + Assert.Equal ("my/app/GenericBase", + FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName); + Assert.Contains ("my/app/IGenericCallback", + FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () + { + var peers = ScanFixtures (); + + var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + Assert.True (baseType.IsUnconditional); + Assert.Equal ("android/app/Activity", baseType.BaseJavaName); + + var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase"); + Assert.StartsWith ("crc64", derived.JavaName); + } + + [Theory] + [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "my/app/RegisteredParent_UnregisteredChild")] + [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")] + public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName) + { + var peers = ScanFixtures (); + Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_EmptyNamespace_Handled () + { + var peers = ScanFixtures (); + Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); + } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnnamedActivity")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) + { + var peers = ScanFixtures (); + Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var peers = ScanFixtures (); + var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } +} From 142c6c78a347121685bc68a06131f825ac0c8425 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 17:44:39 +0100 Subject: [PATCH 07/43] [TrimmableTypeMap] Wire unit tests into solution and CI Add TrimmableTypeMap, TrimmableTypeMap.Tests, and TestFixtures projects to Xamarin.Android.sln. Add CI step to run scanner unit tests and publish results on the Windows build pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.Android.sln | 20 +++++++++++++++++++ .../yaml-templates/build-windows-steps.yaml | 15 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index d1edbe95c97..d5554ea849a 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -59,6 +59,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.ProjectTools", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Build.Tests", "src\Xamarin.Android.Build.Tasks\Tests\Xamarin.Android.Build.Tests\Xamarin.Android.Build.Tests.csproj", "{53E4ABF0-1085-45F9-B964-DCAE4B819998}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap", "src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj", "{507759AE-93DF-411B-8645-31F680319F5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.Tests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj", "{F9CD012E-67AC-4A4E-B2A7-252387F91256}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}" @@ -231,6 +237,18 @@ Global {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Debug|AnyCPU.Build.0 = Debug|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.ActiveCfg = Release|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.Build.0 = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.Build.0 = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.Build.0 = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU @@ -398,6 +416,8 @@ Global {645E1718-C8C4-4C23-8A49-5A37E4ECF7ED} = {04E3E11E-B47D-4599-8AFC-50515A95E715} {2DD1EE75-6D8D-4653-A800-0A24367F7F38} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058} {7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058} {8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 6d3d6738ad2..48544d84d7a 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -77,6 +77,21 @@ steps: testRunTitle: Microsoft.Android.Sdk.Analysis.Tests continueOnError: true +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj + arguments: -c $(XA.Build.Configuration) --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.Tests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.Tests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests + - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: From bd35cdc5a64aa7bd69bb1d15eef4d7480fb00355 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 00:55:49 +0100 Subject: [PATCH 08/43] [TrimmableTypeMap] Integration parity test slice --- ...k.TrimmableTypeMap.IntegrationTests.csproj | 56 + .../MockBuildEngine.cs | 22 + .../ScannerComparisonTests.cs | 1086 +++++++++++++++++ .../UserTypesFixture/UserTypes.cs | 176 +++ .../UserTypesFixture/UserTypesFixture.csproj | 45 + 5 files changed, 1385 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj new file mode 100644 index 00000000000..35c76b21bbc --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj @@ -0,0 +1,56 @@ + + + + $(DotNetTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests + true + ..\..\product.snk + ..\..\bin\Test$(Configuration) + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" /> + + + <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';') + <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0]) + + + + $(_MonoAndroidRefAssembly) + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs new file mode 100644 index 00000000000..d6c8c19d1eb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +/// +/// Minimal IBuildEngine implementation for use with TaskLoggingHelper in tests. +/// +sealed class MockBuildEngine : IBuildEngine +{ + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => ""; + + public bool BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + public void LogCustomEvent (CustomBuildEventArgs e) { } + public void LogErrorEvent (BuildErrorEventArgs e) { } + public void LogMessageEvent (BuildMessageEventArgs e) { } + public void LogWarningEvent (BuildWarningEventArgs e) { } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs new file mode 100644 index 00000000000..8ee29061de0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -0,0 +1,1086 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.TypeNameMappings; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public class ScannerComparisonTests +{ + readonly ITestOutputHelper output; + + public ScannerComparisonTests (ITestOutputHelper output) + { + this.output = output; + } + + record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); + + record MethodEntry (string JniName, string JniSignature, string? Connector); + + record TypeMethodGroup (string ManagedName, List Methods); + + static (List entries, Dictionary> methodsByJavaName) RunLegacyScanner (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + // Extract method-level [Register] attributes from each TypeDefinition. + // Use the raw javaTypes list to get ALL types — multiple managed types + // can map to the same JNI name (aliases). + var methodsByJavaName = new Dictionary> (); + foreach (var typeDef in javaTypes) { + var javaName = GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + // Cecil uses '/' for nested types, SRM uses '+' (CLR format) — normalize + var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + var methods = ExtractMethodRegistrations (typeDef); + + if (!methodsByJavaName.TryGetValue (javaName, out var groups)) { + groups = new List (); + methodsByJavaName [javaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + methods.OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + // Some types appear in dataSets.JavaToManaged (the typemap) but not in + // javaTypes (the raw list). Include them with empty method lists so the + // comparison covers all types known to the legacy scanner. + foreach (var entry in dataSets.JavaToManaged) { + if (methodsByJavaName.ContainsKey (entry.JavaName)) { + continue; + } + + methodsByJavaName [entry.JavaName] = new List { + new TypeMethodGroup (entry.ManagedName, new List ()) + }; + } + + return (entries, methodsByJavaName); + } + + static string? GetCecilJavaName (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return null; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count > 0) { + return ((string) attr.ConstructorArguments [0].Value).Replace ('.', '/'); + } + } + + return null; + } + + static List ExtractMethodRegistrations (TypeDefinition typeDef) + { + var methods = new List (); + + // Collect [Register] from methods directly + foreach (var method in typeDef.Methods) { + if (!method.HasCustomAttributes) { + continue; + } + + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count < 2) { + continue; + } + + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } + } + + // Collect [Register] from properties (attribute is on the property, not the getter/setter) + if (typeDef.HasProperties) { + foreach (var prop in typeDef.Properties) { + if (!prop.HasCustomAttributes) { + continue; + } + + foreach (var attr in prop.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count < 2) { + continue; + } + + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } + } + } + + return methods; + } + + static (List entries, Dictionary> methodsByJavaName) RunNewScanner (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var allPeers = scanner.Scan (assemblyPaths); + var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); + + var entries = peers + .Select (p => new TypeMapEntry ( + p.JavaName, + $"{p.ManagedTypeName}, {p.AssemblyName}", + p.IsInterface || p.IsGenericDefinition + )) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var methodsByJavaName = new Dictionary> (); + foreach (var peer in peers) { + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + if (!methodsByJavaName.TryGetValue (peer.JavaName, out var groups)) { + groups = new List (); + methodsByJavaName [peer.JavaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + peer.MarshalMethods + .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector)) + .OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + return (entries, methodsByJavaName); + } + + [Fact] + public void ExactTypeMap_MonoAndroid () + { + var (legacy, _) = RunLegacyScanner (MonoAndroidAssemblyPath); + var (newEntries, _) = RunNewScanner (AllAssemblyPaths); + output.WriteLine ($"Legacy: {legacy.Count} entries, New: {newEntries.Count} entries"); + AssertTypeMapMatch (legacy, newEntries); + } + + [Fact] + public void ExactMarshalMethods_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (_, legacyMethods) = RunLegacyScanner (assemblyPath); + var (_, newMethods) = RunNewScanner (AllAssemblyPaths); + + var legacyTypeCount = legacyMethods.Values.Sum (g => g.Count); + var newTypeCount = newMethods.Values.Sum (g => g.Count); + var legacyMethodCount = legacyMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); + var newMethodCount = newMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); + output.WriteLine ($"Legacy: {legacyTypeCount} type groups across {legacyMethods.Count} JNI names, {legacyMethodCount} total methods"); + output.WriteLine ($"New: {newTypeCount} type groups across {newMethods.Count} JNI names, {newMethodCount} total methods"); + + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.UnionWith (newMethods.Keys); + + var missingTypes = new List (); + var extraTypes = new List (); + var missingMethods = new List (); + var extraMethods = new List (); + var connectorMismatches = new List (); + + foreach (var javaName in allJavaNames.OrderBy (n => n)) { + var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); + var inNew = newMethods.TryGetValue (javaName, out var newGroups); + + if (inLegacy && !inNew) { + foreach (var g in legacyGroups!) { + missingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + if (!inLegacy && inNew) { + foreach (var g in newGroups!) { + extraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + // Both scanners found this JNI name — compare managed types within it + var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) { + missingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); + } + + foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) { + extraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); + } + + // For managed types present in both, compare their method sets + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + var legacyMethodList = legacyByManaged [managedName]; + var newMethodList = newByManaged [managedName]; + + var legacySet = new HashSet<(string name, string sig)> ( + legacyMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + var newSet = new HashSet<(string name, string sig)> ( + newMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + + foreach (var m in legacySet.Except (newSet)) { + missingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + foreach (var m in newSet.Except (legacySet)) { + extraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + // For methods in both, compare connector strings + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lc = legacyByKey [key].Connector ?? ""; + var nc = newByKey [key].Connector ?? ""; + if (lc != nc) { + connectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); + } + } + } + } + + LogDiffs ("MANAGED TYPES MISSING from new scanner", missingTypes); + LogDiffs ("MANAGED TYPES EXTRA in new scanner", extraTypes); + LogDiffs ("METHODS MISSING from new scanner", missingMethods); + LogDiffs ("METHODS EXTRA in new scanner", extraMethods); + LogDiffs ("CONNECTOR MISMATCHES", connectorMismatches); + + Assert.Empty (missingTypes); + Assert.Empty (extraTypes); + Assert.Empty (missingMethods); + Assert.Empty (extraMethods); + Assert.Empty (connectorMismatches); + } + + [Fact] + public void ScannerDiagnostics_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { assemblyPath }); + + var interfaces = peers.Count (p => p.IsInterface); + var abstracts = peers.Count (p => p.IsAbstract); + var generics = peers.Count (p => p.IsGenericDefinition); + var withMethods = peers.Count (p => p.MarshalMethods.Count > 0); + var totalMethods = peers.Sum (p => p.MarshalMethods.Count); + var withConstructors = peers.Count (p => p.MarshalMethods.Any (m => m.IsConstructor)); + var withBase = peers.Count (p => p.BaseJavaName != null); + var withInterfaces = peers.Count (p => p.ImplementedInterfaceJavaNames.Count > 0); + + output.WriteLine ($"Total types: {peers.Count}"); + output.WriteLine ($"Interfaces: {interfaces}"); + output.WriteLine ($"Abstract classes: {abstracts}"); + output.WriteLine ($"Generic defs: {generics}"); + output.WriteLine ($"With marshal methods: {withMethods} ({totalMethods} total methods)"); + output.WriteLine ($"With constructors: {withConstructors}"); + output.WriteLine ($"With base Java: {withBase}"); + output.WriteLine ($"With interfaces: {withInterfaces}"); + + // Mono.Android.dll should have thousands of types + Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); + Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); + Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); + } + + [Fact] + public void ExactBaseJavaNames_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.BaseJavaName != newInfo.BaseJavaName) { + // Legacy ToJniName can't resolve bases for open generic types (returns null). + // Our scanner resolves them correctly. Accept this known difference. + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { + continue; + } + + // Invokers share JNI names with their base class. Legacy ToJniName + // self-reference filter discards the base (baseJni == javaName), but + // our scanner correctly resolves it. Accept legacy=null, new=valid + // for DoNotGenerateAcw types. + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { + continue; + } + + // Legacy ToJniName(System.Object) returns "java/lang/Object" as a fallback, + // making Java.Lang.Object/Throwable appear to have themselves as base. + // Our scanner correctly returns null. Accept legacy=self, new=null. + if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && + legacy.BaseJavaName == legacy.JavaName) { + continue; + } + + mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); + } + } + + output.WriteLine ($"Compared BaseJavaName for {compared} types"); + + LogDiffs ("BASE JAVA NAME MISMATCHES", mismatches); + + Assert.Empty (mismatches); + } + + [Fact] + public void ExactImplementedInterfaces_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingInterfaces = new List (); + var extraInterfaces = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); + + foreach (var iface in legacySet.Except (newSet)) { + missingInterfaces.Add ($"{managedName}: missing '{iface}'"); + } + + foreach (var iface in newSet.Except (legacySet)) { + extraInterfaces.Add ($"{managedName}: extra '{iface}'"); + } + } + + output.WriteLine ($"Compared ImplementedInterfaces for {compared} types"); + + LogDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); + LogDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); + + Assert.Empty (missingInterfaces); + Assert.Empty (extraInterfaces); + } + + [Fact] + public void ExactActivationCtors_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var presenceMismatches = new List (); + var declaringTypeMismatches = new List (); + var styleMismatches = new List (); + int compared = 0; + int withActivationCtor = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { + presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); + continue; + } + + if (!legacy.HasActivationCtor) { + continue; + } + + withActivationCtor++; + + if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { + declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); + } + + if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { + styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); + } + } + + output.WriteLine ($"Compared ActivationCtor for {compared} types ({withActivationCtor} have activation ctors)"); + + LogDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); + LogDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); + LogDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); + + Assert.Empty (presenceMismatches); + Assert.Empty (declaringTypeMismatches); + Assert.Empty (styleMismatches); + } + + [Fact] + public void ExactJavaConstructors_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingCtors = new List (); + var extraCtors = new List (); + int compared = 0; + int totalCtors = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); + totalCtors += newSet.Count; + + foreach (var sig in legacySet.Except (newSet)) { + missingCtors.Add ($"{managedName}: missing '{sig}'"); + } + + foreach (var sig in newSet.Except (legacySet)) { + extraCtors.Add ($"{managedName}: extra '{sig}'"); + } + } + + output.WriteLine ($"Compared JavaConstructors for {compared} types ({totalCtors} total constructors)"); + + LogDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); + LogDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); + + Assert.Empty (missingCtors); + Assert.Empty (extraCtors); + } + + [Fact] + public void ExactTypeFlags_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var interfaceMismatches = new List (); + var abstractMismatches = new List (); + var genericMismatches = new List (); + var acwMismatches = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.IsInterface != newInfo.IsInterface) { + interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); + } + + if (legacy.IsAbstract != newInfo.IsAbstract) { + abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); + } + + if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { + genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); + } + + if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { + acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); + } + } + + output.WriteLine ($"Compared type flags for {compared} types"); + + LogDiffs ("IsInterface MISMATCHES", interfaceMismatches); + LogDiffs ("IsAbstract MISMATCHES", abstractMismatches); + LogDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); + LogDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); + + Assert.Empty (interfaceMismatches); + Assert.Empty (abstractMismatches); + Assert.Empty (genericMismatches); + Assert.Empty (acwMismatches); + } + + + record TypeComparisonData ( + string ManagedName, + string JavaName, + string? BaseJavaName, + IReadOnlyList ImplementedInterfaces, + bool HasActivationCtor, + string? ActivationCtorDeclaringType, + string? ActivationCtorStyle, + IReadOnlyList JavaConstructorSignatures, + bool IsInterface, + bool IsAbstract, + bool IsGenericDefinition, + bool DoNotGenerateAcw + ); + + static (Dictionary perType, List entries) BuildLegacyTypeData (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var typeDef in javaTypes) { + var javaName = GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + // Cecil uses '/' for nested types, SRM uses '+' — normalize + var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + + // Base Java name + string? baseJavaName = null; + var baseType = typeDef.GetBaseType (cache); + if (baseType != null) { + var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache); + // Filter self-references: ToJniName can return the type's own JNI name + // (e.g., Java.Lang.Object → System.Object → "java/lang/Object"). + if (baseJni != null && baseJni != javaName) { + baseJavaName = baseJni; + } + } + + // Implemented interfaces (only Java peer interfaces with [Register]) + var implementedInterfaces = new List (); + if (typeDef.HasInterfaces) { + foreach (var ifaceImpl in typeDef.Interfaces) { + var ifaceDef = cache.Resolve (ifaceImpl.InterfaceType); + if (ifaceDef == null) { + continue; + } + var ifaceRegs = CecilExtensions.GetTypeRegistrationAttributes (ifaceDef); + var ifaceReg = ifaceRegs.FirstOrDefault (); + if (ifaceReg != null) { + implementedInterfaces.Add (ifaceReg.Name.Replace ('.', '/')); + } + } + } + implementedInterfaces.Sort (StringComparer.Ordinal); + + // Activation constructor + bool hasActivationCtor = false; + string? activationCtorDeclaringType = null; + string? activationCtorStyle = null; + FindLegacyActivationCtor (typeDef, cache, out hasActivationCtor, out activationCtorDeclaringType, out activationCtorStyle); + + // Java constructors: [Register("", sig, ...)] on .ctor methods + var javaCtorSignatures = new List (); + foreach (var method in typeDef.Methods) { + if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) { + continue; + } + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.ConstructorArguments.Count >= 2) { + var regName = (string) attr.ConstructorArguments [0].Value; + if (regName == "" || regName == ".ctor") { + javaCtorSignatures.Add ((string) attr.ConstructorArguments [1].Value); + } + } + } + } + javaCtorSignatures.Sort (StringComparer.Ordinal); + + // Type flags + var isInterface = typeDef.IsInterface; + var isAbstract = typeDef.IsAbstract && !typeDef.IsInterface; + var isGenericDefinition = typeDef.HasGenericParameters; + var doNotGenerateAcw = GetCecilDoNotGenerateAcw (typeDef); + + perType [managedName] = new TypeComparisonData ( + managedName, + javaName, + baseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + isInterface, + isAbstract, + isGenericDefinition, + doNotGenerateAcw + ); + } + + return (perType, entries); + } + + static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, + out bool found, out string? declaringType, out string? style) + { + found = false; + declaringType = null; + style = null; + + // Walk from current type up through base types + TypeDefinition? current = typeDef; + while (current != null) { + foreach (var method in current.Methods) { + if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) { + continue; + } + + var p0 = method.Parameters [0].ParameterType.FullName; + var p1 = method.Parameters [1].ParameterType.FullName; + + if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") { + found = true; + declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + style = "XamarinAndroid"; + return; + } + + if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") && + p1 == "Java.Interop.JniObjectReferenceOptions") { + found = true; + declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + style = "JavaInterop"; + return; + } + } + + current = current.GetBaseType (cache); + } + } + + static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return false; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.HasProperties) { + foreach (var prop in attr.Properties.Where (p => p.Name == "DoNotGenerateAcw")) { + if (prop.Argument.Value is bool val) { + return val; + } + } + } + // [Register] found but DoNotGenerateAcw not set — defaults to false + return false; + } + + return false; + } + + static Dictionary BuildNewTypeData (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (assemblyPaths); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var peer in peers) { + // Only include types from the primary assembly + if (peer.AssemblyName != primaryAssemblyName) { + continue; + } + + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + // Map ActivationCtor + bool hasActivationCtor = peer.ActivationCtor != null; + string? activationCtorDeclaringType = null; + string? activationCtorStyle = null; + if (peer.ActivationCtor != null) { + activationCtorDeclaringType = $"{peer.ActivationCtor.DeclaringTypeName}, {peer.ActivationCtor.DeclaringAssemblyName}"; + activationCtorStyle = peer.ActivationCtor.Style.ToString (); + } + + // Java constructor signatures (sorted) — derived from constructor marshal methods + var javaCtorSignatures = peer.MarshalMethods + .Where (m => m.IsConstructor) + .Select (m => m.JniSignature) + .OrderBy (s => s, StringComparer.Ordinal) + .ToList (); + + // Implemented interfaces (sorted) + var implementedInterfaces = peer.ImplementedInterfaceJavaNames + .OrderBy (i => i, StringComparer.Ordinal) + .ToList (); + + perType [managedName] = new TypeComparisonData ( + managedName, + peer.JavaName, + peer.BaseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + peer.IsInterface, + peer.IsAbstract && !peer.IsInterface, // Match legacy: isAbstract excludes interfaces + peer.IsGenericDefinition, + peer.DoNotGenerateAcw + ); + } + + return perType; + } + + static string MonoAndroidAssemblyPath { + get { + // Compile-time check: this ensures the Mono.Android reference is properly configured. + // It's never actually evaluated at runtime — it just validates the build setup. + _ = nameof (Java.Lang.Object); + + // At runtime, find the Mono.Android.dll copy in the test output directory. + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var path = Path.Combine (testDir, "Mono.Android.dll"); + + if (!File.Exists (path)) { + throw new InvalidOperationException ( + $"Mono.Android.dll not found at '{path}'. " + + "Ensure Mono.Android is built (bin/Debug/lib/packs/Microsoft.Android.Ref.*)."); + } + + return path; + } + } + + static string[] AllAssemblyPaths { + get { + var monoAndroidPath = MonoAndroidAssemblyPath; + var dir = Path.GetDirectoryName (monoAndroidPath)!; + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + if (!File.Exists (javaInteropPath)) { + return new [] { monoAndroidPath }; + } + + return new [] { monoAndroidPath, javaInteropPath }; + } + } + + 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; + } + + static string? UserTypesFixturePath { + get { + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var path = Path.Combine (testDir, "UserTypesFixture.dll"); + return File.Exists (path) ? path : null; + } + } + + static string[]? AllUserTypesAssemblyPaths { + get { + var fixturePath = UserTypesFixturePath; + if (fixturePath == null) { + return null; + } + + var dir = Path.GetDirectoryName (fixturePath)!; + var monoAndroidPath = Path.Combine (dir, "Mono.Android.dll"); + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + var paths = new List { fixturePath }; + if (File.Exists (monoAndroidPath)) { + paths.Add (monoAndroidPath); + } + if (File.Exists (javaInteropPath)) { + paths.Add (javaInteropPath); + } + return paths.ToArray (); + } + } + + [Fact] + public void ExactTypeMap_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var fixturePath = paths! [0]; + var (legacy, _) = RunLegacyScanner (fixturePath); + var (newEntries, _) = RunNewScanner (paths); + + output.WriteLine ($"UserTypesFixture: Legacy={legacy.Count} entries, New={newEntries.Count} entries"); + + // Normalize CRC64 hashes — the two scanners use different polynomials + var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); + var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); + + AssertTypeMapMatch (legacyNormalized, newNormalized); + } + + [Fact] + public void ExactMarshalMethods_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var fixturePath = paths! [0]; + var (_, legacyMethods) = RunLegacyScanner (fixturePath); + var (_, newMethods) = RunNewScanner (paths); + + // Normalize CRC64 hashes in method group keys + var legacyNormalized = legacyMethods + .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); + var newNormalized = newMethods + .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); + + output.WriteLine ($"UserTypesFixture: Legacy={legacyNormalized.Count} types with methods, New={newNormalized.Count}"); + + // Only compare types that the legacy scanner found (it skips user types without [Register]) + var missing = new List (); + var methodMismatches = new List (); + + foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n)) { + if (!newNormalized.TryGetValue (javaName, out var newGroups)) { + missing.Add (javaName); + continue; + } + + var legacyGroups = legacyNormalized [javaName]; + + foreach (var legacyGroup in legacyGroups) { + var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); + if (newGroup == null) { + missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); + continue; + } + + // Legacy test helper only extracts [Register] methods, not [Export] methods. + // When legacy has 0 methods (from the typemap fallback path) but new has some, + // the new scanner is correct — it handles [Export] too. Skip comparison. + if (legacyGroup.Methods.Count == 0) { + continue; + } + + if (legacyGroup.Methods.Count != newGroup.Methods.Count) { + methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); + continue; + } + + for (int i = 0; i < legacyGroup.Methods.Count; i++) { + var lm = legacyGroup.Methods [i]; + var nm = newGroup.Methods [i]; + if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { + methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); + } + } + } + } + + LogDiffs ("MISSING from new scanner", missing); + LogDiffs ("METHOD MISMATCHES", methodMismatches); + + Assert.Empty (missing); + Assert.Empty (methodMismatches); + } + + void AssertTypeMapMatch (List legacy, List newEntries) + { + var legacyMap = legacy.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + var newMap = newEntries.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + + var allJavaNames = new HashSet (legacyMap.Keys); + allJavaNames.UnionWith (newMap.Keys); + + var missing = new List (); + var extra = new List (); + var managedNameMismatches = new List (); + var skipMismatches = new List (); + + foreach (var javaName in allJavaNames.OrderBy (n => n)) { + var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries); + var inNew = newMap.TryGetValue (javaName, out var newEntriesForName); + + if (inLegacy && !inNew) { + foreach (var e in legacyEntries!) + missing.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + if (!inLegacy && inNew) { + foreach (var e in newEntriesForName!) + extra.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + var le = legacyEntries!.OrderBy (e => e.ManagedName).First (); + var ne = newEntriesForName!.OrderBy (e => e.ManagedName).First (); + + if (le.ManagedName != ne.ManagedName) + managedNameMismatches.Add ($"{javaName}: legacy='{le.ManagedName}' new='{ne.ManagedName}'"); + + if (le.SkipInJavaToManaged != ne.SkipInJavaToManaged) + skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}"); + } + + LogDiffs ("MISSING", missing); + LogDiffs ("EXTRA", extra); + LogDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); + LogDiffs ("SKIP FLAG MISMATCHES", skipMismatches); + + Assert.Empty (missing); + Assert.Empty (extra); + Assert.Empty (managedNameMismatches); + Assert.Empty (skipMismatches); + } + + void LogDiffs (string label, List items) + { + if (items.Count == 0) return; + output.WriteLine ($"\n--- {label} ({items.Count}) ---"); + foreach (var item in items) output.WriteLine ($" {item}"); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs new file mode 100644 index 00000000000..291137278cb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -0,0 +1,176 @@ +// User-type test fixture assembly that references REAL Mono.Android. +// Exercises edge cases that MCW binding assemblies don't have: +// - User types extending Java peers without [Register] +// - Component attributes ([Activity], [Service], etc.) +// - [Export] methods +// - Nested user types +// - Generic user types + +using System; +using System.Runtime.Versioning; +using Android.App; +using Android.Content; +using Android.Runtime; +using Java.Interop; + +[assembly: SupportedOSPlatform ("android21.0")] + +// --- User Activity with explicit Name --- + +namespace UserApp +{ + [Activity (Name = "com.example.userapp.MainActivity", MainLauncher = true, Label = "User App")] + public class MainActivity : Activity + { + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + } + + // Activity WITHOUT explicit Name — should get CRC64-based JNI name + [Activity (Label = "Settings")] + public class SettingsActivity : Activity + { + } + + // Simple Activity subclass — no attributes at all, just extends a Java peer + public class PlainActivity : Activity + { + } +} + +// --- Services --- + +namespace UserApp.Services +{ + [Service (Name = "com.example.userapp.MyBackgroundService")] + public class MyBackgroundService : Android.App.Service + { + public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null; + } + + // Service without explicit Name + [Service] + public class UnnamedService : Android.App.Service + { + public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null; + } +} + +// --- BroadcastReceiver --- + +namespace UserApp.Receivers +{ + [BroadcastReceiver (Name = "com.example.userapp.BootReceiver", Exported = false)] + public class BootReceiver : BroadcastReceiver + { + public override void OnReceive (Context? context, Intent? intent) + { + } + } +} + +// --- Application with BackupAgent --- + +namespace UserApp +{ + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + public override void OnBackup (Android.OS.ParcelFileDescriptor? oldState, + Android.App.Backup.BackupDataOutput? data, + Android.OS.ParcelFileDescriptor? newState) + { + } + + public override void OnRestore (Android.App.Backup.BackupDataInput? data, + int appVersionCode, + Android.OS.ParcelFileDescriptor? newState) + { + } + } + + [Application (Name = "com.example.userapp.MyApp", BackupAgent = typeof (MyBackupAgent))] + public class MyApp : Application + { + public MyApp (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// --- Nested types --- + +namespace UserApp.Nested +{ + [Register ("com/example/userapp/OuterClass")] + public class OuterClass : Java.Lang.Object + { + // Nested class inheriting from Java peer — no [Register] + public class InnerHelper : Java.Lang.Object + { + } + + // Deeply nested + public class MiddleClass : Java.Lang.Object + { + public class DeepHelper : Java.Lang.Object + { + } + } + } +} + +// --- Plain Java.Lang.Object subclasses (no attributes) --- + +namespace UserApp.Models +{ + // These should all get CRC64-based JNI names + public class UserModel : Java.Lang.Object + { + } + + public class DataManager : Java.Lang.Object + { + } +} + +// --- Explicit [Register] on user type --- + +namespace UserApp +{ + [Register ("com/example/userapp/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// --- Interface implementation --- + +namespace UserApp.Listeners +{ + public class MyClickListener : Java.Lang.Object, Android.Views.View.IOnClickListener + { + public void OnClick (Android.Views.View? v) + { + } + } +} + +// --- [Export] method --- + +namespace UserApp +{ + public class ExportedMethodHolder : Java.Lang.Object + { + [Export ("doWork")] + public void DoWork () + { + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj new file mode 100644 index 00000000000..bba3496f276 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj @@ -0,0 +1,45 @@ + + + + + $(DotNetTargetFramework) + latest + enable + false + Library + true + ..\..\..\product.snk + + ..\..\..\bin\Test$(Configuration)\ + + + + + + + + + + <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" /> + <_JavaInteropRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Java.Interop.dll" /> + + + <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';') + <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0]) + <_JavaInteropRefAssembly>@(_JavaInteropRefCandidate, ';') + <_JavaInteropRefAssembly>$(_JavaInteropRefAssembly.Split(';')[0]) + + + + $(_MonoAndroidRefAssembly) + + + + + $(_JavaInteropRefAssembly) + + + + + From e92938ef096d42c685adddbae31f56a590decbd1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 17:46:09 +0100 Subject: [PATCH 09/43] [TrimmableTypeMap] Wire integration tests into solution and CI Add IntegrationTests and UserTypesFixture projects to Xamarin.Android.sln. Add CI step to run integration tests and publish results. Add InternalsVisibleTo for the integration test assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.Android.sln | 14 ++++++++++++++ .../yaml-templates/build-windows-steps.yaml | 15 +++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + 3 files changed, 30 insertions(+) diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index d5554ea849a..48ee13d6a66 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -65,6 +65,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.Trimm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj", "{A14CB0A1-7A05-4F27-88B2-383798CE1DEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserTypesFixture", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\UserTypesFixture\UserTypesFixture.csproj", "{2498F8A0-AA04-40EF-8691-59BBD2396B4D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}" @@ -249,6 +253,14 @@ Global {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.Build.0 = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.Build.0 = Release|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU @@ -418,6 +430,8 @@ Global {53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {2498F8A0-AA04-40EF-8691-59BBD2396B4D} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058} {7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058} {8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 48544d84d7a..f7cd09ce3b3 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -92,6 +92,21 @@ steps: testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.dll + arguments: --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-integration-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-integration-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests + - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: diff --git a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs index 0bd860a35e2..e66435ccc40 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs @@ -20,3 +20,4 @@ [assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("MSBuildDeviceIntegration, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] From f525db4f1f55c49ce6b99b3dd2cc062f278099b2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 19:21:00 +0100 Subject: [PATCH 10/43] [TypeMap] Add JCW and TypeMap assembly generators with IR model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements generators for dotnet/android#10799: - JcwJavaSourceGenerator: generates .java files for ACW types from JavaPeerInfo - TypeMapModelBuilder: transforms JavaPeerInfo → TypeMapAssemblyModel (IR/AST) - TypeMapAssemblyEmitter: mechanical SRM-based PE emitter from IR model - TypeMapAssemblyGenerator: high-level API composing builder + emitter - JniSignatureHelper: JNI signature parsing and CLR type encoding Key design: - 1 typemap assembly per input assembly for better caching - IR model separates 'what to generate' from 'how to serialize to IL' - Model builder tests are the primary unit tests (148 total, all passing) Generated assemblies contain: - [assembly: TypeMap] attributes per JNI type - Proxy types (JavaPeerProxy subclasses) with CreateInstance, TargetType - [UnmanagedCallersOnly] UCO wrappers for marshal methods/constructors - RegisterNatives with function pointer registration - IgnoresAccessChecksToAttribute for cross-assembly calls --- .../Generator/JcwJavaSourceGenerator.cs | 319 ++++++++++ .../Generator/JniSignatureHelper.cs | 84 +++ .../Generator/Model/TypeMapAssemblyModel.cs | 137 +++++ .../Generator/TypeMapAssemblyEmitter.cs | 569 ++++++++++++++++++ .../Generator/TypeMapAssemblyGenerator.cs | 25 + .../Generator/TypeMapModelBuilder.cs | 173 ++++++ .../Generator/JcwJavaSourceGeneratorTests.cs | 293 +++++++++ .../TypeMapAssemblyGeneratorTests.cs | 447 ++++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 480 +++++++++++++++ 9 files changed, 2527 insertions(+) create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs create mode 100644 tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs create mode 100644 tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs create mode 100644 tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs new file mode 100644 index 00000000000..df2a57f1963 --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// Generates JCW (Java Callable Wrapper) .java source files from scanned records. +/// Only processes ACW types (where is false). +/// +sealed class JcwJavaSourceGenerator +{ + /// + /// Generates .java source files for all ACW types and writes them to the output directory. + /// Returns the list of generated file paths. + /// + public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory) + { + if (types is null) { + throw new ArgumentNullException (nameof (types)); + } + if (outputDirectory is null) { + throw new ArgumentNullException (nameof (outputDirectory)); + } + + var generatedFiles = new List (); + + foreach (var type in types) { + if (type.DoNotGenerateAcw) { + continue; + } + + string filePath = GetOutputFilePath (type, outputDirectory); + string? dir = Path.GetDirectoryName (filePath); + if (dir != null) { + Directory.CreateDirectory (dir); + } + + using var writer = new StreamWriter (filePath); + Generate (type, writer); + generatedFiles.Add (filePath); + } + + return generatedFiles; + } + + /// + /// Generates a single .java source file for the given type. + /// + internal void Generate (JavaPeerInfo type, TextWriter writer) + { + WritePackageDeclaration (type, writer); + WriteClassDeclaration (type, writer); + WriteStaticInitializer (type, writer); + WriteConstructors (type, writer); + WriteMethods (type, writer); + WriteClassClose (writer); + } + + static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory) + { + // JNI name uses '/' as separator and '$' for nested types + // e.g., "com/example/MainActivity" → "com/example/MainActivity.java" + // Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention) + string relativePath = type.JavaName + ".java"; + return Path.Combine (outputDirectory, relativePath); + } + + static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer) + { + string? package = GetJavaPackageName (type.JavaName); + if (package != null) { + writer.Write ("package "); + writer.Write (package); + writer.WriteLine (';'); + writer.WriteLine (); + } + } + + static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) + { + writer.Write ("public "); + if (type.IsAbstract && !type.IsInterface) { + writer.Write ("abstract "); + } + writer.Write ("class "); + writer.WriteLine (GetJavaSimpleName (type.JavaName)); + + // extends clause + string? baseJavaType = type.BaseJavaName != null ? JniNameToJavaName (type.BaseJavaName) : null; + if (baseJavaType != null) { + writer.Write ("\textends "); + writer.WriteLine (baseJavaType); + } + + // implements clause — always includes IGCUserPeer, plus any implemented interfaces + writer.Write ("\timplements"); + writer.Write ("\n\t\tmono.android.IGCUserPeer"); + + foreach (var iface in type.ImplementedInterfaceJavaNames) { + writer.Write (",\n\t\t"); + writer.Write (JniNameToJavaName (iface)); + } + + writer.WriteLine (); + writer.WriteLine ('{'); + } + + static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) + { + writer.Write ("\tstatic {\n"); + writer.Write ("\t\tmono.android.Runtime.registerNatives ("); + writer.Write (GetJavaSimpleName (type.JavaName)); + writer.Write (".class);\n"); + writer.Write ("\t}\n"); + writer.WriteLine (); + } + + static void WriteConstructors (JavaPeerInfo type, TextWriter writer) + { + string simpleClassName = GetJavaSimpleName (type.JavaName); + + foreach (var ctor in type.JavaConstructors) { + // Constructor signature + writer.Write ("\tpublic "); + writer.Write (simpleClassName); + writer.Write (" ("); + WriteParameterList (ctor.Parameters, writer); + writer.WriteLine (')'); + writer.WriteLine ("\t{"); + + // super() call with parameters + writer.Write ("\t\tsuper ("); + WriteArgumentList (ctor.Parameters, writer); + writer.WriteLine (");"); + + // Activation guard: only activate if this is the exact class + writer.Write ("\t\tif (getClass () == "); + writer.Write (simpleClassName); + writer.Write (".class) "); + writer.Write ("nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteArgumentList (ctor.Parameters, writer); + writer.WriteLine (");"); + + writer.WriteLine ("\t}"); + writer.WriteLine (); + } + + // Write native constructor declarations + foreach (var ctor in type.JavaConstructors) { + writer.Write ("\tprivate native void nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteParameterList (ctor.Parameters, writer); + writer.WriteLine (");"); + } + + if (type.JavaConstructors.Count > 0) { + writer.WriteLine (); + } + } + + static void WriteMethods (JavaPeerInfo type, TextWriter writer) + { + foreach (var method in type.MarshalMethods) { + if (method.IsConstructor) { + continue; + } + + string javaReturnType = JniTypeToJava (method.JniReturnType); + bool isVoid = method.JniReturnType == "V"; + + // Public override wrapper + writer.Write ("\t@Override\n"); + writer.Write ("\tpublic "); + writer.Write (javaReturnType); + writer.Write (' '); + writer.Write (method.JniName); + writer.Write (" ("); + WriteParameterList (method.Parameters, writer); + writer.Write (")\n"); + + // throws clause for [Export] methods + if (method.ThrownNames != null && method.ThrownNames.Count > 0) { + writer.Write ("\t\tthrows "); + for (int i = 0; i < method.ThrownNames.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (method.ThrownNames [i]); + } + writer.Write ('\n'); + } + + writer.Write ("\t{\n"); + + // Delegate to native method + writer.Write ("\t\t"); + if (!isVoid) { + writer.Write ("return "); + } + writer.Write (method.NativeCallbackName); + writer.Write (" ("); + WriteArgumentList (method.Parameters, writer); + writer.Write (");\n"); + + writer.Write ("\t}\n"); + + // Native method declaration + writer.Write ("\tpublic native "); + writer.Write (javaReturnType); + writer.Write (' '); + writer.Write (method.NativeCallbackName); + writer.Write (" ("); + WriteParameterList (method.Parameters, writer); + writer.Write (");\n"); + + writer.WriteLine (); + } + } + + static void WriteClassClose (TextWriter writer) + { + writer.WriteLine ('}'); + } + + static void WriteParameterList (IReadOnlyList parameters, TextWriter writer) + { + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (JniTypeToJava (parameters [i].JniType)); + writer.Write (" p"); + writer.Write (i); + } + } + + static void WriteArgumentList (IReadOnlyList parameters, TextWriter writer) + { + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write ('p'); + writer.Write (i); + } + } + + /// + /// Converts a JNI type name to a Java source type name. + /// e.g., "android/app/Activity" → "android.app.Activity" + /// + internal static string JniNameToJavaName (string jniName) + { + return jniName.Replace ('/', '.'); + } + + /// + /// Extracts the Java package name from a JNI type name. + /// e.g., "com/example/MainActivity" → "com.example" + /// Returns null for types without a package. + /// + internal static string? GetJavaPackageName (string jniName) + { + int lastSlash = jniName.LastIndexOf ('/'); + if (lastSlash < 0) { + return null; + } + return jniName.Substring (0, lastSlash).Replace ('/', '.'); + } + + /// + /// Extracts the simple Java class name from a JNI type name. + /// e.g., "com/example/MainActivity" → "MainActivity" + /// e.g., "com/example/Outer$Inner" → "Outer$Inner" (preserves nesting separator) + /// + internal static string GetJavaSimpleName (string jniName) + { + int lastSlash = jniName.LastIndexOf ('/'); + return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName; + } + + /// + /// Converts a JNI type descriptor to a Java source type. + /// e.g., "V" → "void", "I" → "int", "Landroid/os/Bundle;" → "android.os.Bundle" + /// + internal static string JniTypeToJava (string jniType) + { + if (jniType.Length == 1) { + return jniType [0] switch { + 'V' => "void", + 'Z' => "boolean", + 'B' => "byte", + 'C' => "char", + 'S' => "short", + 'I' => "int", + 'J' => "long", + 'F' => "float", + 'D' => "double", + _ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"), + }; + } + + // Array types: "[I" → "int[]", "[Ljava/lang/String;" → "java.lang.String[]" + if (jniType [0] == '[') { + return JniTypeToJava (jniType.Substring (1)) + "[]"; + } + + // Object types: "Landroid/os/Bundle;" → "android.os.Bundle" + if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') { + return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2)); + } + + throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}"); + } +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs new file mode 100644 index 00000000000..b0ce11bdd0d --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Build.TypeMap; + +/// JNI primitive type kinds used for mapping JNI signatures → CLR types. +enum JniParamKind +{ + Void, // V + Boolean, // Z → sbyte + Byte, // B → sbyte + Char, // C → char + Short, // S → short + Int, // I → int + Long, // J → long + Float, // F → float + Double, // D → double + Object, // L...; or [ → IntPtr +} + +/// Helpers for parsing JNI method signatures. +static class JniSignatureHelper +{ + /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V". + public static List ParseParameterTypes (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + result.Add (ParseSingleType (jniSignature, ref i)); + } + return result; + } + + /// Parses the return type from a JNI method signature. + public static JniParamKind ParseReturnType (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return ParseSingleType (jniSignature, ref i); + } + + static JniParamKind ParseSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': i++; return JniParamKind.Void; + case 'Z': i++; return JniParamKind.Boolean; + case 'B': i++; return JniParamKind.Byte; + case 'C': i++; return JniParamKind.Char; + case 'S': i++; return JniParamKind.Short; + case 'I': i++; return JniParamKind.Int; + case 'J': i++; return JniParamKind.Long; + case 'F': i++; return JniParamKind.Float; + case 'D': i++; return JniParamKind.Double; + case 'L': + i = sig.IndexOf (';', i) + 1; + return JniParamKind.Object; + case '[': + i++; + ParseSingleType (sig, ref i); // skip element type + return JniParamKind.Object; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } + + /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. + public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) + { + switch (kind) { + case JniParamKind.Boolean: encoder.SByte (); break; + case JniParamKind.Byte: encoder.SByte (); break; + case JniParamKind.Char: encoder.Char (); break; + case JniParamKind.Short: encoder.Int16 (); break; + case JniParamKind.Int: encoder.Int32 (); break; + case JniParamKind.Long: encoder.Int64 (); break; + case JniParamKind.Float: encoder.Single (); break; + case JniParamKind.Double: encoder.Double (); break; + case JniParamKind.Object: encoder.IntPtr (); break; + default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type"); + } + } +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs new file mode 100644 index 00000000000..65255a0057c --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// Intermediate representation of a single TypeMap output assembly. +/// This is the "AST" that describes what to emit — the PE emitter translates it 1:1 into IL. +/// Built by , consumed by . +/// +sealed class TypeMapAssemblyModel +{ + /// Assembly name (e.g., "_MyApp.TypeMap"). + public string AssemblyName { get; set; } = ""; + + /// Module file name (e.g., "_MyApp.TypeMap.dll"). + public string ModuleName { get; set; } = ""; + + /// TypeMap entries — one per unique JNI name. + public List Entries { get; } = new (); + + /// Proxy types to emit in the assembly. + public List ProxyTypes { get; } = new (); + + /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. + public List IgnoresAccessChecksTo { get; } = new () { "Mono.Android", "Java.Interop" }; +} + +/// +/// One [assembly: TypeMap("jni/name", typeof(TargetOrProxy))] entry. +/// +sealed class TypeMapEntryModel +{ + /// JNI type name, e.g., "android/app/Activity". + public string JniName { get; set; } = ""; + + /// + /// Assembly-qualified type reference for the attribute's Type argument. + /// Either points to a generated proxy or to the original managed type. + /// + public string TypeReference { get; set; } = ""; +} + +/// +/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy). +/// +sealed class ProxyTypeModel +{ + /// Simple type name, e.g., "java_lang_Object_Proxy". + public string TypeName { get; set; } = ""; + + /// Namespace for all proxy types. + public string Namespace { get; set; } = "_TypeMap.Proxies"; + + /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property). + public TypeRefModel TargetType { get; set; } = new (); + + /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. + public TypeRefModel? InvokerType { get; set; } + + /// Whether this proxy has a CreateInstance that can actually create instances (has activation ctor). + public bool HasActivation { get; set; } + + /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers). + public bool IsAcw { get; set; } + + /// Implements IAndroidCallableWrapper when IsAcw is true. + public bool ImplementsIAndroidCallableWrapper => IsAcw; + + /// UCO method wrappers for marshal methods (non-constructor). + public List UcoMethods { get; } = new (); + + /// UCO constructor wrappers. + public List UcoConstructors { get; } = new (); + + /// RegisterNatives registrations (method name, JNI signature, wrapper name). + public List NativeRegistrations { get; } = new (); +} + +/// +/// A cross-assembly type reference (assembly name + full managed type name). +/// +sealed class TypeRefModel +{ + /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". + public string ManagedTypeName { get; set; } = ""; + + /// Assembly containing the type, e.g., "Mono.Android". + public string AssemblyName { get; set; } = ""; +} + +/// +/// An [UnmanagedCallersOnly] static wrapper for a marshal method. +/// Body: load all args → call n_* callback → ret. +/// +sealed class UcoMethodModel +{ + /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0". + public string WrapperName { get; set; } = ""; + + /// Name of the n_* callback to call, e.g., "n_OnCreate". + public string CallbackMethodName { get; set; } = ""; + + /// Type containing the callback method. + public TypeRefModel CallbackType { get; set; } = new (); + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. + public string JniSignature { get; set; } = ""; +} + +/// +/// An [UnmanagedCallersOnly] static wrapper for a constructor callback. +/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). +/// +sealed class UcoConstructorModel +{ + /// Name of the generated wrapper, e.g., "nctor_0_uco". + public string WrapperName { get; set; } = ""; + + /// Target type to pass to ActivateInstance. + public TypeRefModel TargetType { get; set; } = new (); +} + +/// +/// One JNI native method registration in RegisterNatives. +/// +sealed class NativeRegistrationModel +{ + /// JNI method name to register, e.g., "n_onCreate" or "nctor_0". + public string JniMethodName { get; set; } = ""; + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + public string JniSignature { get; set; } = ""; + + /// Name of the UCO wrapper method whose function pointer to register. + public string WrapperMethodName { get; set; } = ""; +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs new file mode 100644 index 00000000000..48bacd9665b --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// Emits a TypeMap PE assembly from a . +/// This is a mechanical translation — all decision logic lives in . +/// +sealed class TypeMapAssemblyEmitter +{ + readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); + + AssemblyReferenceHandle _systemRuntimeRef; + AssemblyReferenceHandle _monoAndroidRef; + AssemblyReferenceHandle _javaInteropRef; + AssemblyReferenceHandle _systemRuntimeInteropServicesRef; + + TypeReferenceHandle _javaPeerProxyRef; + TypeReferenceHandle _iJavaPeerableRef; + TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _iAndroidCallableWrapperRef; + TypeReferenceHandle _systemTypeRef; + TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _stringRef; + TypeReferenceHandle _jniTypeRef; + TypeReferenceHandle _trimmableNativeRegistrationRef; + + MemberReferenceHandle _baseCtorRef; + MemberReferenceHandle _getTypeFromHandleRef; + MemberReferenceHandle _createManagedPeerRef; + MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _registerMethodRef; + MemberReferenceHandle _ucoAttrCtorRef; + MemberReferenceHandle _typeMapAttrCtorRef; + + /// + /// Emits a PE assembly from the given model and writes it to . + /// + public void Emit (TypeMapAssemblyModel model, string outputPath) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + _asmRefCache.Clear (); + + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + var metadata = new MetadataBuilder (); + var ilBuilder = new BlobBuilder (); + + EmitAssemblyAndModule (metadata, model); + EmitAssemblyReferences (metadata); + EmitTypeReferences (metadata); + EmitMemberReferences (metadata); + EmitModuleType (metadata); + + // Track wrapper method names → handles for RegisterNatives + var wrapperHandles = new Dictionary (); + + foreach (var proxy in model.ProxyTypes) { + EmitProxyType (metadata, ilBuilder, proxy, wrapperHandles); + } + + foreach (var entry in model.Entries) { + EmitTypeMapAttribute (metadata, entry); + } + + EmitIgnoresAccessChecksToAttribute (metadata, ilBuilder, model.IgnoresAccessChecksTo); + WritePE (metadata, ilBuilder, outputPath); + } + + // ---- Assembly / Module ---- + + void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyModel model) + { + metadata.AddAssembly ( + metadata.GetOrAddString (model.AssemblyName), + new Version (1, 0, 0, 0), + culture: default, + publicKey: default, + flags: 0, + hashAlgorithm: AssemblyHashAlgorithm.None); + + metadata.AddModule ( + generation: 0, + metadata.GetOrAddString (model.ModuleName), + metadata.GetOrAddGuid (Guid.NewGuid ()), + encId: default, + encBaseId: default); + } + + void EmitAssemblyReferences (MetadataBuilder metadata) + { + _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", new Version (11, 0, 0, 0)); + _monoAndroidRef = AddAssemblyRef (metadata, "Mono.Android", new Version (0, 0, 0, 0), + publicKeyOrToken: new byte [] { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }); + _javaInteropRef = AddAssemblyRef (metadata, "Java.Interop", new Version (0, 0, 0, 0)); + _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", new Version (11, 0, 0, 0)); + } + + void EmitTypeReferences (MetadataBuilder metadata) + { + _javaPeerProxyRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); + _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); + _jniHandleOwnershipRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _iAndroidCallableWrapperRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper")); + _systemTypeRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + _runtimeTypeHandleRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); + _stringRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); + _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); + _trimmableNativeRegistrationRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); + } + + void EmitMemberReferences (MetadataBuilder metadata) + { + _baseCtorRef = AddMemberRef (metadata, _javaPeerProxyRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + _getTypeFromHandleRef = AddMemberRef (metadata, _systemTypeRef, "GetTypeFromHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Type (_systemTypeRef, false), + p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true))); + + _createManagedPeerRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "CreateManagedPeer", + 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); + })); + + _activateInstanceRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "ActivateInstance", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + _registerMethodRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "RegisterMethod", + sig => sig.MethodSignature ().Parameters (4, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_jniTypeRef, false); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().IntPtr (); + })); + + var ucoAttrTypeRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute")); + _ucoAttrCtorRef = AddMemberRef (metadata, ucoAttrTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + EmitTypeMapAttributeCtorRef (metadata); + } + + void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) + { + var typeMapAttrOpenRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAttribute`1")); + var javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeMapAttrOpenRef)); + genericInstBlob.WriteCompressedInteger (1); + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + + _typeMapAttrCtorRef = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_stringRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + + void EmitModuleType (MetadataBuilder metadata) + { + metadata.AddTypeDefinition ( + default, default, + metadata.GetOrAddString (""), + default, + MetadataTokens.FieldDefinitionHandle (1), + MetadataTokens.MethodDefinitionHandle (1)); + } + + // ---- Proxy types ---- + + void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyTypeModel proxy, + Dictionary wrapperHandles) + { + var typeDefHandle = metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (proxy.Namespace), + metadata.GetOrAddString (proxy.TypeName), + _javaPeerProxyRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + if (proxy.ImplementsIAndroidCallableWrapper) { + metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef); + } + + // .ctor + EmitBody (metadata, ilBuilder, ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_0); + encoder.Call (_baseCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + // CreateInstance + EmitCreateInstance (metadata, ilBuilder, proxy); + + // get_TargetType + EmitTypeGetter (metadata, ilBuilder, "get_TargetType", proxy.TargetType, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + + // get_InvokerType + if (proxy.InvokerType != null) { + EmitTypeGetter (metadata, ilBuilder, "get_InvokerType", proxy.InvokerType, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + } + + // UCO wrappers + foreach (var uco in proxy.UcoMethods) { + var handle = EmitUcoMethod (metadata, ilBuilder, uco); + wrapperHandles [uco.WrapperName] = handle; + } + + foreach (var uco in proxy.UcoConstructors) { + var handle = EmitUcoConstructor (metadata, ilBuilder, uco); + wrapperHandles [uco.WrapperName] = handle; + } + + // RegisterNatives + if (proxy.IsAcw) { + EmitRegisterNatives (metadata, ilBuilder, proxy.NativeRegistrations, wrapperHandles); + } + } + + void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyTypeModel proxy) + { + if (!proxy.HasActivation) { + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + return; + } + + var userTypeRef = ResolveTypeRef (metadata, proxy.TargetType); + + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (userTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_createManagedPeerRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + void EmitTypeGetter (MetadataBuilder metadata, BlobBuilder ilBuilder, string methodName, + TypeRefModel typeRef, MethodAttributes attrs) + { + var handle = ResolveTypeRef (metadata, typeRef); + + EmitBody (metadata, ilBuilder, methodName, attrs, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().Type (_systemTypeRef, false), + p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (handle); + encoder.Call (_getTypeFromHandleRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + // ---- UCO wrappers ---- + + MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoMethodModel uco) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature); + int paramCount = 2 + jniParams.Count; + bool isVoid = returnKind == JniParamKind.Void; + + // Callback method reference + var callbackTypeHandle = ResolveTypeRef (metadata, uco.CallbackType); + var callbackRef = AddMemberRef (metadata, callbackTypeHandle, uco.CallbackMethodName, + sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + })); + + var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + 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]); + }), + encoder => { + for (int p = 0; p < paramCount; p++) + encoder.LoadArgument (p); + encoder.Call (callbackRef); + encoder.OpCode (ILOpCode.Ret); + }); + + AddUnmanagedCallersOnlyAttribute (metadata, handle); + return handle; + } + + MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoConstructorModel uco) + { + var userTypeRef = ResolveTypeRef (metadata, uco.TargetType); + + var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + }), + encoder => { + encoder.LoadArgument (1); // self + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (userTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_activateInstanceRef); + encoder.OpCode (ILOpCode.Ret); + }); + + AddUnmanagedCallersOnlyAttribute (metadata, handle); + return handle; + } + + // ---- RegisterNatives ---- + + void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, + List registrations, Dictionary wrapperHandles) + { + EmitBody (metadata, ilBuilder, "RegisterNatives", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.NewSlot | MethodAttributes.Final, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniTypeRef, false)), + encoder => { + foreach (var reg in registrations) { + if (!wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { + continue; + } + encoder.LoadArgument (1); + encoder.LoadString (metadata.GetOrAddUserString (reg.JniMethodName)); + encoder.LoadString (metadata.GetOrAddUserString (reg.JniSignature)); + encoder.OpCode (ILOpCode.Ldftn); + encoder.Token (wrapperHandle); + encoder.Call (_registerMethodRef); + } + encoder.OpCode (ILOpCode.Ret); + }); + } + + // ---- TypeMap attributes ---- + + void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapEntryModel entry) + { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (0x0001); // Prolog + attrBlob.WriteSerializedString (entry.JniName); + attrBlob.WriteSerializedString (entry.TypeReference); + attrBlob.WriteUInt16 (0x0000); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef, metadata.GetOrAddBlob (attrBlob)); + } + + // ---- IgnoresAccessChecksTo ---- + + void EmitIgnoresAccessChecksToAttribute (MetadataBuilder metadata, BlobBuilder ilBuilder, List assemblyNames) + { + var attributeTypeRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); + + int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var baseAttrCtorRef = AddMemberRef (metadata, attributeTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + var ctorDef = EmitBody (metadata, ilBuilder, ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ()), + encoder => { + encoder.LoadArgument (0); + encoder.Call (baseAttrCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), + metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"), + attributeTypeRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + foreach (var asmName in assemblyNames) { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (1); + attrBlob.WriteSerializedString (asmName); + attrBlob.WriteUInt16 (0); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, metadata.GetOrAddBlob (attrBlob)); + } + } + + // ---- Plumbing helpers ---- + + AssemblyReferenceHandle AddAssemblyRef (MetadataBuilder metadata, string name, Version version, + byte []? publicKeyOrToken = null) + { + var handle = metadata.AddAssemblyReference ( + metadata.GetOrAddString (name), version, default, + publicKeyOrToken != null ? metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default); + _asmRefCache [name] = handle; + return handle; + } + + AssemblyReferenceHandle FindOrAddAssemblyReference (MetadataBuilder metadata, string assemblyName) + { + if (_asmRefCache.TryGetValue (assemblyName, out var handle)) { + return handle; + } + return AddAssemblyRef (metadata, assemblyName, new Version (0, 0, 0, 0)); + } + + static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, + Action encodeSig) + { + var blob = new BlobBuilder (); + encodeSig (new BlobEncoder (blob)); + return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob)); + } + + EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefModel typeRef) + { + var asmRef = FindOrAddAssemblyReference (metadata, typeRef.AssemblyName); + return MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName); + } + + TypeReferenceHandle MakeTypeRefForManagedName (MetadataBuilder metadata, EntityHandle scope, string managedTypeName) + { + int plusIndex = managedTypeName.IndexOf ('+'); + if (plusIndex >= 0) { + var outerRef = MakeTypeRefForManagedName (metadata, scope, managedTypeName.Substring (0, plusIndex)); + return MakeTypeRefForManagedName (metadata, outerRef, managedTypeName.Substring (plusIndex + 1)); + } + int lastDot = managedTypeName.LastIndexOf ('.'); + var ns = lastDot >= 0 ? managedTypeName.Substring (0, lastDot) : ""; + var name = lastDot >= 0 ? managedTypeName.Substring (lastDot + 1) : managedTypeName; + return metadata.AddTypeReference (scope, metadata.GetOrAddString (ns), metadata.GetOrAddString (name)); + } + + void AddUnmanagedCallersOnlyAttribute (MetadataBuilder metadata, MethodDefinitionHandle handle) + { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (1); + attrBlob.WriteUInt16 (0); + metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, metadata.GetOrAddBlob (attrBlob)); + } + + /// Emits a method body and definition in one call. + MethodDefinitionHandle EmitBody (MetadataBuilder metadata, BlobBuilder ilBuilder, + string name, MethodAttributes attrs, + Action encodeSig, Action emitIL) + { + var sigBlob = new BlobBuilder (); + encodeSig (new BlobEncoder (sigBlob)); + + var codeBuilder = new BlobBuilder (); + var encoder = new InstructionEncoder (codeBuilder); + emitIL (encoder); + + while (ilBuilder.Count % 4 != 0) { + ilBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); + int bodyOffset = bodyEncoder.AddMethodBody (encoder); + + return metadata.AddMethodDefinition ( + attrs, MethodImplAttributes.IL, + metadata.GetOrAddString (name), + metadata.GetOrAddBlob (sigBlob), + bodyOffset, default); + } + + static void WritePE (MetadataBuilder metadata, BlobBuilder ilBuilder, string outputPath) + { + var peBuilder = new ManagedPEBuilder ( + new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (metadata), + ilBuilder); + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + using var fs = File.Create (outputPath); + peBlob.WriteContentTo (fs); + } +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..264916b8d34 --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// High-level API: builds the model from peers, then emits the PE assembly. +/// Composes + . +/// +sealed class TypeMapAssemblyGenerator +{ + /// + /// Generates a TypeMap PE assembly from the given Java peer info records. + /// + /// Scanned Java peer types. + /// Path where the output .dll will be written. + /// Optional explicit assembly name. Derived from outputPath if null. + public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + var builder = new TypeMapModelBuilder (); + var model = builder.Build (peers, outputPath, assemblyName); + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + } +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs new file mode 100644 index 00000000000..4e3a1636850 --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// Builds a from scanned records. +/// All decision logic (deduplication, ACW detection, callback resolution, proxy naming) lives here. +/// The output model is a plain data structure that the PE emitter translates 1:1. +/// +sealed class TypeMapModelBuilder +{ + /// + /// Builds a TypeMap assembly model for the given peers. + /// + /// Scanned Java peer types (typically from a single input assembly). + /// Output .dll path — used to derive assembly/module names if not specified. + /// Explicit assembly name. If null, derived from . + public TypeMapAssemblyModel Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + if (peers is null) { + throw new ArgumentNullException (nameof (peers)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); + string moduleName = Path.GetFileName (outputPath); + + var model = new TypeMapAssemblyModel { + AssemblyName = assemblyName, + ModuleName = moduleName, + }; + + var seenJniNames = new HashSet (StringComparer.Ordinal); + + foreach (var peer in peers) { + if (!seenJniNames.Add (peer.JavaName)) { + continue; + } + + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; + bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; + + ProxyTypeModel? proxy = null; + if (hasProxy) { + proxy = BuildProxyType (peer, isAcw); + model.ProxyTypes.Add (proxy); + } + + model.Entries.Add (BuildEntry (peer, proxy, assemblyName)); + } + + return model; + } + + static ProxyTypeModel BuildProxyType (JavaPeerInfo peer, bool isAcw) + { + var proxyTypeName = peer.JavaName.Replace ('/', '_').Replace ('$', '_') + "_Proxy"; + + var proxy = new ProxyTypeModel { + TypeName = proxyTypeName, + TargetType = new TypeRefModel { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + HasActivation = peer.ActivationCtor != null, + IsAcw = isAcw, + }; + + if (peer.InvokerTypeName != null) { + proxy.InvokerType = new TypeRefModel { + ManagedTypeName = peer.InvokerTypeName, + AssemblyName = peer.AssemblyName, + }; + } + + if (isAcw) { + BuildUcoMethods (peer, proxy); + BuildUcoConstructors (peer, proxy); + BuildNativeRegistrations (proxy); + } + + return proxy; + } + + static void BuildUcoMethods (JavaPeerInfo peer, ProxyTypeModel proxy) + { + int ucoIndex = 0; + for (int i = 0; i < peer.MarshalMethods.Count; i++) { + var mm = peer.MarshalMethods [i]; + if (mm.IsConstructor) { + continue; + } + + proxy.UcoMethods.Add (new UcoMethodModel { + WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", + CallbackMethodName = mm.NativeCallbackName, + CallbackType = new TypeRefModel { + ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, + AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + }); + ucoIndex++; + } + } + + static void BuildUcoConstructors (JavaPeerInfo peer, ProxyTypeModel proxy) + { + if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) { + return; + } + + foreach (var ctor in peer.JavaConstructors) { + proxy.UcoConstructors.Add (new UcoConstructorModel { + WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", + TargetType = new TypeRefModel { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + }); + } + } + + static void BuildNativeRegistrations (ProxyTypeModel proxy) + { + foreach (var uco in proxy.UcoMethods) { + // The JNI method name registered is the n_* callback name (e.g., "n_onCreate") + // but we need the Java-side native method name which matches the callback name + proxy.NativeRegistrations.Add (new NativeRegistrationModel { + JniMethodName = uco.CallbackMethodName, + JniSignature = uco.JniSignature, + WrapperMethodName = uco.WrapperName, + }); + } + + foreach (var uco in proxy.UcoConstructors) { + // Constructor wrapper name is "nctor_N_uco", JNI name is "nctor_N" + string jniName = uco.WrapperName; + int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); + if (ucoSuffix >= 0) { + jniName = jniName.Substring (0, ucoSuffix); + } + + proxy.NativeRegistrations.Add (new NativeRegistrationModel { + JniMethodName = jniName, + // Constructor UCO wrappers have a fixed (IntPtr, IntPtr) signature — the JNI + // signature for registration is the Java constructor's JNI signature. + // For now, use "()V" as placeholder — the actual ctor signature is resolved at emit time. + JniSignature = "()V", + WrapperMethodName = uco.WrapperName, + }); + } + } + + static TypeMapEntryModel BuildEntry (JavaPeerInfo peer, ProxyTypeModel? proxy, string outputAssemblyName) + { + string typeRef; + if (proxy != null) { + typeRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; + } else { + typeRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + return new TypeMapEntryModel { + JniName = peer.JavaName, + TypeReference = typeRef, + }; + } +} diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs new file mode 100644 index 00000000000..1ca26f0596d --- /dev/null +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Build.TypeMap.Tests; + +public class JcwJavaSourceGeneratorTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (JcwJavaSourceGeneratorTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + JavaPeerInfo FindByJavaName (List peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + string GenerateToString (JavaPeerInfo type) + { + var generator = new JcwJavaSourceGenerator (); + using var writer = new StringWriter (); + generator.Generate (type, writer); + return writer.ToString (); + } + + // ---- JNI name conversion tests ---- + + [Theory] + [InlineData ("android/app/Activity", "android.app.Activity")] + [InlineData ("java/lang/Object", "java.lang.Object")] + [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")] + public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.JniNameToJavaName (jniName)); + } + + [Theory] + [InlineData ("com/example/MainActivity", "com.example")] + [InlineData ("java/lang/Object", "java.lang")] + [InlineData ("TopLevelClass", null)] + public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaPackageName (jniName)); + } + + [Theory] + [InlineData ("com/example/MainActivity", "MainActivity")] + [InlineData ("com/example/Outer$Inner", "Outer$Inner")] + [InlineData ("TopLevelClass", "TopLevelClass")] + public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (jniName)); + } + + [Theory] + [InlineData ("V", "void")] + [InlineData ("Z", "boolean")] + [InlineData ("B", "byte")] + [InlineData ("I", "int")] + [InlineData ("J", "long")] + [InlineData ("F", "float")] + [InlineData ("D", "double")] + [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")] + [InlineData ("[I", "int[]")] + [InlineData ("[Ljava/lang/String;", "java.lang.String[]")] + public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.JniTypeToJava (jniType)); + } + + // ---- Filtering tests ---- + + [Fact] + public void Generate_SkipsMcwTypes () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + var files = generator.Generate (peers, outputDir); + // MCW types like java/lang/Object, android/app/Activity should NOT be generated + Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java")); + Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java")); + // User ACW types should be generated + Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java")); + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } + } + } + + // ---- Package declaration tests ---- + + [Fact] + public void Generate_MainActivity_HasPackageDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.StartsWith ("package my.app;\n", java); + } + + // ---- Class declaration tests ---- + + [Fact] + public void Generate_MainActivity_HasClassDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("public class MainActivity\n", java); + Assert.Contains ("\textends android.app.Activity\n", java); + Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java); + } + + [Fact] + public void Generate_AbstractType_HasAbstractModifier () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + var java = GenerateToString (abstractBase); + Assert.Contains ("public abstract class AbstractBase\n", java); + } + + [Fact] + public void Generate_TypeWithInterfaces_HasImplementsClause () + { + var peers = ScanFixtures (); + var multiView = FindByJavaName (peers, "my/app/MultiInterfaceView"); + var java = GenerateToString (multiView); + Assert.Contains ("\timplements\n", java); + Assert.Contains ("\t\tmono.android.IGCUserPeer", java); + Assert.Contains ("android.view.View$OnClickListener", java); + Assert.Contains ("android.view.View$OnLongClickListener", java); + } + + // ---- Static initializer tests ---- + + [Fact] + public void Generate_AcwType_HasRegisterNativesStaticBlock () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("static {\n", java); + Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); + } + + // ---- Constructor tests ---- + + [Fact] + public void Generate_CustomView_HasConstructors () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + + // Default constructor + Assert.Contains ("public CustomView ()\n", java); + Assert.Contains ("super ();\n", java); + Assert.Contains ("nctor_0 ();\n", java); + + // Context constructor + Assert.Contains ("public CustomView (android.content.Context p0)\n", java); + Assert.Contains ("super (p0);\n", java); + Assert.Contains ("nctor_1 (p0);\n", java); + } + + [Fact] + public void Generate_CustomView_HasNativeConstructorDeclarations () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + Assert.Contains ("private native void nctor_0 ();\n", java); + Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java); + } + + [Fact] + public void Generate_Constructor_HasActivationGuard () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java); + } + + // ---- Method tests ---- + + [Fact] + public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("@Override\n", java); + Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); + Assert.Contains ("n_OnCreate (p0);\n", java); + Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + } + + [Fact] + public void Generate_MethodWithReturnValue_HasReturnStatement () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java); + Assert.Contains ("return n_OnTouch (p0, p1);\n", java); + } + + [Fact] + public void Generate_MethodWithMultipleParams_HasAllParameters () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java); + } + + [Fact] + public void Generate_MethodWithObjectReturnType_HasCorrectType () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public java.lang.String getText ()\n", java); + Assert.Contains ("return n_GetText ();\n", java); + } + + [Fact] + public void Generate_MethodWithArrayParam_HasCorrectType () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java); + } + + // ---- Nested type tests ---- + + [Fact] + public void Generate_NestedType_HasCorrectPackageAndClassName () + { + var peers = ScanFixtures (); + var inner = FindByJavaName (peers, "my/app/Outer$Inner"); + var java = GenerateToString (inner); + Assert.Contains ("package my.app;\n", java); + Assert.Contains ("public class Outer$Inner\n", java); + } + + // ---- Output file path tests ---- + + [Fact] + public void Generate_CreatesCorrectFileStructure () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + var files = generator.Generate (peers, outputDir); + Assert.NotEmpty (files); + + // All files should be under the output directory + foreach (var file in files) { + Assert.StartsWith (outputDir, file); + Assert.True (File.Exists (file), $"Generated file should exist: {file}"); + Assert.EndsWith (".java", file); + } + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } + } + } +} diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs new file mode 100644 index 00000000000..57f08f83616 --- /dev/null +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,447 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Build.TypeMap.Tests; + +public class TypeMapAssemblyGeneratorTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (TypeMapAssemblyGeneratorTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) + { + var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", + (assemblyName ?? "TestTypeMap") + ".dll"); + var generator = new TypeMapAssemblyGenerator (); + generator.Generate (peers, outputPath, assemblyName); + return outputPath; + } + + (PEReader pe, MetadataReader reader) OpenAssembly (string path) + { + var pe = new PEReader (File.OpenRead (path)); + return (pe, pe.GetMetadataReader ()); + } + + // ---- Basic assembly structure tests ---- + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_AssemblyHasCorrectName () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, "MyTestTypeMap"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("MyTestTypeMap", reader.GetString (asmDef.Name)); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_HasModuleType () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var types = reader.TypeDefinitions.Select (h => reader.GetTypeDefinition (h)).ToList (); + Assert.Contains (types, t => reader.GetString (t.Name) == ""); + } + } finally { + CleanUp (path); + } + } + + // ---- Assembly reference tests ---- + + [Fact] + public void Generate_HasRequiredAssemblyReferences () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("System.Runtime", asmRefs); + Assert.Contains ("Mono.Android", asmRefs); + Assert.Contains ("Java.Interop", asmRefs); + Assert.Contains ("System.Runtime.InteropServices", asmRefs); + } + } finally { + CleanUp (path); + } + } + + // ---- TypeMap attribute tests ---- + + [Fact] + public void Generate_HasTypeMapAttributes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyCustomAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (assemblyCustomAttrs); + // We should have at least as many attributes as non-duplicate peers + // (TypeMap attrs + IgnoresAccessChecksTo attrs) + Assert.True (assemblyCustomAttrs.Count () >= 2); + } + } finally { + CleanUp (path); + } + } + + // ---- Proxy type tests ---- + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + // At least some proxy types should be generated + Assert.NotEmpty (proxyTypes); + + // Check that a proxy exists for java/lang/Object → java_lang_Object_Proxy + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "java_lang_Object_Proxy"); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ProxyTypesAreSealedClasses () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + foreach (var proxy in proxyTypes) { + Assert.True ((proxy.Attributes & TypeAttributes.Sealed) != 0, + $"Proxy {reader.GetString (proxy.Name)} should be sealed"); + Assert.True ((proxy.Attributes & TypeAttributes.Public) != 0, + $"Proxy {reader.GetString (proxy.Name)} should be public"); + } + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "java_lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); + } + } finally { + CleanUp (path); + } + } + + // ---- ACW proxy tests ---- + + [Fact] + public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () + { + var peers = ScanFixtures (); + // Find a non-MCW type with marshal methods (e.g., my/app/CustomView has constructors) + var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); + var path = GenerateAssembly (new [] { acwPeer }, "AcwTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains ("RegisterNatives", methods); + // UCO wrappers for each marshal method + Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0")); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); + var path = GenerateAssembly (new [] { acwPeer }, "UcoTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + + // Find a UCO method + var ucoMethod = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .First (m => reader.GetString (m.Name).Contains ("_uco_")); + + // Verify it has [UnmanagedCallersOnly] attribute + var attrs = ucoMethod.GetCustomAttributes () + .Select (h => reader.GetCustomAttribute (h)) + .ToList (); + Assert.NotEmpty (attrs); + } + } finally { + CleanUp (path); + } + } + + // ---- IgnoresAccessChecksTo tests ---- + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // The IgnoresAccessChecksToAttribute type should be defined + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + } + } finally { + CleanUp (path); + } + } + + // ---- Deduplication tests ---- + + [Fact] + public void Generate_DuplicateJniNames_KeepsFirstOnly () + { + // Create two peers with the same JNI name + var peers = new List { + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate1", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate1", + AssemblyName = "TestAssembly", + }, + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate2", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate2", + AssemblyName = "TestAssembly", + }, + }; + + var path = GenerateAssembly (peers, "DedupTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Should only have one proxy for "test/Duplicate" + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Name) == "test_Duplicate_Proxy") + .ToList (); + // No proxies because neither peer has an activation ctor or invoker + Assert.Empty (proxyTypes); + } + } finally { + CleanUp (path); + } + } + + // ---- Empty input tests ---- + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + var path = GenerateAssembly (Array.Empty (), "EmptyTest"); + try { + Assert.True (File.Exists (path)); + var (pe, reader) = OpenAssembly (path); + using (pe) { + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + } + } finally { + CleanUp (path); + } + } + + // ---- Per-assembly model tests ---- + + [Fact] + public void Generate_SingleAssemblyInput_Works () + { + var allPeers = ScanFixtures (); + // Filter to just one assembly's peers + var testFixturePeers = allPeers.Where (p => p.AssemblyName == "TestFixtures").ToList (); + + var path = GenerateAssembly (testFixturePeers, "_TestFixtures.TypeMap"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("_TestFixtures.TypeMap", reader.GetString (asmDef.Name)); + + // Should still have proxy types + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.NotEmpty (proxyTypes); + } + } finally { + CleanUp (path); + } + } + + // ---- JNI signature helper tests ---- + + [Theory] + [InlineData ("()V", 0)] + [InlineData ("(I)V", 1)] + [InlineData ("(Landroid/os/Bundle;)V", 1)] + [InlineData ("(IFJ)V", 3)] + [InlineData ("(ZLandroid/view/View;I)Z", 3)] + [InlineData ("([Ljava/lang/String;)V", 1)] + public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) + { + var actual = JniSignatureHelper.ParseParameterTypes (signature); + Assert.Equal (expectedCount, actual.Count); + } + + [Fact] + public void ParseParameterTypes_BooleanMapsToBoolean () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Z)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Boolean, types [0]); + } + + [Fact] + public void ParseParameterTypes_ObjectMapsToObject () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Object, types [0]); + } + + [Fact] + public void ParseReturnType_Void () + { + Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V")); + } + + [Fact] + public void ParseReturnType_Int () + { + Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I")); + } + + [Fact] + public void ParseReturnType_Boolean () + { + Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z")); + } + + [Fact] + public void ParseReturnType_Object () + { + Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); + } + + static void CleanUp (string path) + { + var dir = Path.GetDirectoryName (path); + if (dir != null && Directory.Exists (dir)) { + try { Directory.Delete (dir, true); } catch { } + } + } +} diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs new file mode 100644 index 00000000000..832bee9bd59 --- /dev/null +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -0,0 +1,480 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Build.TypeMap.Tests; + +public class TypeMapModelBuilderTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (TypeMapModelBuilderTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + TypeMapAssemblyModel BuildModel (IReadOnlyList peers, string? assemblyName = null) + { + var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); + var builder = new TypeMapModelBuilder (); + return builder.Build (peers, outputPath, assemblyName); + } + + // ---- Basic model structure ---- + + [Fact] + public void Build_EmptyPeers_ProducesEmptyModel () + { + var model = BuildModel (Array.Empty (), "Empty"); + Assert.Equal ("Empty", model.AssemblyName); + Assert.Equal ("Empty.dll", model.ModuleName); + Assert.Empty (model.Entries); + Assert.Empty (model.ProxyTypes); + } + + [Fact] + public void Build_AssemblyNameDerivedFromOutputPath () + { + var builder = new TypeMapModelBuilder (); + var model = builder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); + Assert.Equal ("Foo.Bar", model.AssemblyName); + Assert.Equal ("Foo.Bar.dll", model.ModuleName); + } + + [Fact] + public void Build_ExplicitAssemblyName_OverridesOutputPath () + { + var builder = new TypeMapModelBuilder (); + var model = builder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + Assert.Equal ("MyAssembly", model.AssemblyName); + } + + [Fact] + public void Build_DefaultIgnoresAccessChecksTo () + { + var model = BuildModel (Array.Empty ()); + Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); + Assert.Contains ("Java.Interop", model.IgnoresAccessChecksTo); + } + + // ---- TypeMap entries ---- + + [Fact] + public void Build_CreatesOneEntryPerPeer () + { + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"), + }; + + var model = BuildModel (peers); + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("java/lang/Object", model.Entries [0].JniName); + Assert.Equal ("android/app/Activity", model.Entries [1].JniName); + } + + [Fact] + public void Build_DuplicateJniNames_KeepsFirstOnly () + { + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "A"), + MakeMcwPeer ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers); + Assert.Single (model.Entries); + Assert.Equal ("test/Dup", model.Entries [0].JniName); + // First one wins - type reference should point to Test.First + Assert.Contains ("Test.First", model.Entries [0].TypeReference); + } + + [Fact] + public void Build_McwPeerWithoutActivation_NoProxy () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + // No activation ctor, no invoker → no proxy + var model = BuildModel (new [] { peer }); + + Assert.Empty (model.ProxyTypes); + Assert.Single (model.Entries); + Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].TypeReference); + } + + // ---- Proxy types ---- + + [Fact] + public void Build_PeerWithActivationCtor_CreatesProxy () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("java_lang_Object_Proxy", proxy.TypeName); + Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); + Assert.True (proxy.HasActivation); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); + } + + [Fact] + public void Build_PeerWithInvoker_CreatesProxy () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); + } + + [Fact] + public void Build_ProxyNaming_ReplacesSlashAndDollar () + { + var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.Equal ("com_example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); + } + + [Fact] + public void Build_EntryPointsToProxy_WhenProxyExists () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + var entry = model.Entries [0]; + Assert.Contains ("java_lang_Object_Proxy", entry.TypeReference); + Assert.Contains ("MyTypeMap", entry.TypeReference); + } + + // ---- ACW detection ---- + + [Fact] + public void Build_AcwType_IsAcwTrue () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.True (model.ProxyTypes [0].IsAcw); + Assert.True (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); + } + + [Fact] + public void Build_McwType_IsAcwFalse () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + Assert.False (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); + } + + [Fact] + public void Build_InterfaceWithMarshalMethods_IsNotAcw () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + MarshalMethods = new List { + MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"), + }, + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + // Interface is NOT an ACW even with marshal methods + Assert.False (model.ProxyTypes [0].IsAcw); + } + + [Fact] + public void Build_DoNotGenerateAcw_IsNotAcw () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + peer.DoNotGenerateAcw = true; + peer.MarshalMethods = new List { + MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"), + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + } + + // ---- UCO methods ---- + + [Fact] + public void Build_AcwWithMarshalMethods_CreatesUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + MakeMarshalMethod ("onResume", "n_OnResume", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Equal (2, proxy.UcoMethods.Count); + Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName); + Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature); + + Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName); + Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName); + } + + [Fact] + public void Build_UcoMethod_CallbackTypeIsDeclaringType () + { + var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"); + mm.DeclaringTypeName = "Java.Lang.Object"; + mm.DeclaringAssemblyName = "Mono.Android"; + + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + mm, + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName); + Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName); + } + + [Fact] + public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onPause", "n_OnPause", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName); + Assert.Equal ("App", uco.CallbackType.AssemblyName); + } + + [Fact] + public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true), + MakeMarshalMethod ("onStart", "n_OnStart", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + // Only 1 UCO method (constructors are skipped from UcoMethods) + Assert.Single (proxy.UcoMethods); + Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName); + } + + // ---- UCO constructors ---- + + [Fact] + public void Build_AcwWithConstructors_CreatesUcoConstructors () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Single (proxy.UcoConstructors); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + } + + [Fact] + public void Build_PeerWithoutActivationCtor_NoUcoConstructors () + { + // Peer with marshal methods but no activation ctor + var peer = new JavaPeerInfo { + JavaName = "my/app/Foo", + ManagedTypeName = "MyApp.Foo", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "Foo", + AssemblyName = "App", + InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy + MarshalMethods = new List { + MakeMarshalMethod ("bar", "n_Bar", "()V"), + }, + JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }, + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Empty (proxy.UcoConstructors); + } + + // ---- Native registrations ---- + + [Fact] + public void Build_NativeRegistrations_MatchUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + // 1 registration for method + 1 for constructor + Assert.Equal (2, proxy.NativeRegistrations.Count); + + var methodReg = proxy.NativeRegistrations [0]; + Assert.Equal ("n_OnCreate", methodReg.JniMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature); + Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName); + + var ctorReg = proxy.NativeRegistrations [1]; + Assert.Equal ("nctor_0", ctorReg.JniMethodName); + Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName); + } + + [Fact] + public void Build_NonAcwProxy_NoNativeRegistrations () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.Empty (model.ProxyTypes [0].NativeRegistrations); + } + + // ---- Full fixture scan ---- + + [Fact] + public void Build_FromScannedFixtures_ProducesValidModel () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "TestTypeMap"); + + Assert.Equal ("TestTypeMap", model.AssemblyName); + Assert.NotEmpty (model.Entries); + Assert.NotEmpty (model.ProxyTypes); + + // All entries have non-empty JNI names + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.TypeReference))); + } + + [Fact] + public void Build_FromScannedFixtures_NoProxiesForMcwWithoutActivation () + { + var peers = ScanFixtures (); + var model = BuildModel (peers); + + // Proxy type names should all end with _Proxy + Assert.All (model.ProxyTypes, p => Assert.EndsWith ("_Proxy", p.TypeName)); + } + + [Fact] + public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () + { + var peers = ScanFixtures (); + var model = BuildModel (peers); + + var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); + Assert.NotEmpty (acwProxies); + + // ACW proxies should have registrations + foreach (var proxy in acwProxies) { + Assert.NotEmpty (proxy.NativeRegistrations); + } + } + + // ---- Helpers ---- + + static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + { + var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; + var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; + return new JavaPeerInfo { + JavaName = jniName, + ManagedTypeName = managedName, + ManagedTypeNamespace = ns, + ManagedTypeShortName = shortName, + AssemblyName = asmName, + }; + } + + static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) + { + var peer = MakeMcwPeer (jniName, managedName, asmName); + peer.ActivationCtor = new ActivationCtorInfo { + Style = ActivationCtorStyle.XamarinAndroid, + }; + return peer; + } + + static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) + { + var peer = MakePeerWithActivation (jniName, managedName, asmName); + peer.DoNotGenerateAcw = false; + // Add a constructor so it qualifies as ACW + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }; + // Need at least 1 marshal method to be ACW + peer.MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + IsConstructor = true, + }, + }; + return peer; + } + + static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + IsConstructor = isConstructor, + }; + } +} From b979fb23676e24a4b0f1746384c8bce3782948ba Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 19:37:30 +0100 Subject: [PATCH 11/43] Rename model types: drop IR/IL/AST terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TypeMapAssemblyModel → TypeMapAssemblyData - TypeMapEntryModel → TypeMapAttributeData - ProxyTypeModel → JavaPeerProxyData - TypeRefModel → TypeRefData - UcoMethodModel → UcoMethodData - UcoConstructorModel → UcoConstructorData - NativeRegistrationModel → NativeRegistrationData - TypeMapModelBuilder → ModelBuilder --- ...ssemblyModel.cs => TypeMapAssemblyData.cs} | 36 ++++++++-------- ...TypeMapModelBuilder.cs => ModelBuilder.cs} | 42 +++++++++---------- .../Generator/TypeMapAssemblyEmitter.cs | 24 +++++------ .../Generator/TypeMapAssemblyGenerator.cs | 4 +- .../Generator/TypeMapModelBuilderTests.cs | 12 +++--- 5 files changed, 59 insertions(+), 59 deletions(-) rename src/Microsoft.Android.Build.TypeMap/Generator/Model/{TypeMapAssemblyModel.cs => TypeMapAssemblyData.cs} (82%) rename src/Microsoft.Android.Build.TypeMap/Generator/{TypeMapModelBuilder.cs => ModelBuilder.cs} (77%) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs similarity index 82% rename from src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs rename to src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs index 65255a0057c..bedf81830d7 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyModel.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -5,10 +5,10 @@ namespace Microsoft.Android.Build.TypeMap; /// /// Intermediate representation of a single TypeMap output assembly. -/// This is the "AST" that describes what to emit — the PE emitter translates it 1:1 into IL. -/// Built by , consumed by . +/// Describes what to emit — the emitter writes this directly into a PE assembly. +/// Built by , consumed by . /// -sealed class TypeMapAssemblyModel +sealed class TypeMapAssemblyData { /// Assembly name (e.g., "_MyApp.TypeMap"). public string AssemblyName { get; set; } = ""; @@ -17,10 +17,10 @@ sealed class TypeMapAssemblyModel public string ModuleName { get; set; } = ""; /// TypeMap entries — one per unique JNI name. - public List Entries { get; } = new (); + public List Entries { get; } = new (); /// Proxy types to emit in the assembly. - public List ProxyTypes { get; } = new (); + public List ProxyTypes { get; } = new (); /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. public List IgnoresAccessChecksTo { get; } = new () { "Mono.Android", "Java.Interop" }; @@ -29,7 +29,7 @@ sealed class TypeMapAssemblyModel /// /// One [assembly: TypeMap("jni/name", typeof(TargetOrProxy))] entry. /// -sealed class TypeMapEntryModel +sealed class TypeMapAttributeData { /// JNI type name, e.g., "android/app/Activity". public string JniName { get; set; } = ""; @@ -44,7 +44,7 @@ sealed class TypeMapEntryModel /// /// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy). /// -sealed class ProxyTypeModel +sealed class JavaPeerProxyData { /// Simple type name, e.g., "java_lang_Object_Proxy". public string TypeName { get; set; } = ""; @@ -53,10 +53,10 @@ sealed class ProxyTypeModel public string Namespace { get; set; } = "_TypeMap.Proxies"; /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property). - public TypeRefModel TargetType { get; set; } = new (); + public TypeRefData TargetType { get; set; } = new (); /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. - public TypeRefModel? InvokerType { get; set; } + public TypeRefData? InvokerType { get; set; } /// Whether this proxy has a CreateInstance that can actually create instances (has activation ctor). public bool HasActivation { get; set; } @@ -68,19 +68,19 @@ sealed class ProxyTypeModel public bool ImplementsIAndroidCallableWrapper => IsAcw; /// UCO method wrappers for marshal methods (non-constructor). - public List UcoMethods { get; } = new (); + public List UcoMethods { get; } = new (); /// UCO constructor wrappers. - public List UcoConstructors { get; } = new (); + public List UcoConstructors { get; } = new (); /// RegisterNatives registrations (method name, JNI signature, wrapper name). - public List NativeRegistrations { get; } = new (); + public List NativeRegistrations { get; } = new (); } /// /// A cross-assembly type reference (assembly name + full managed type name). /// -sealed class TypeRefModel +sealed class TypeRefData { /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". public string ManagedTypeName { get; set; } = ""; @@ -93,7 +93,7 @@ sealed class TypeRefModel /// An [UnmanagedCallersOnly] static wrapper for a marshal method. /// Body: load all args → call n_* callback → ret. /// -sealed class UcoMethodModel +sealed class UcoMethodData { /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0". public string WrapperName { get; set; } = ""; @@ -102,7 +102,7 @@ sealed class UcoMethodModel public string CallbackMethodName { get; set; } = ""; /// Type containing the callback method. - public TypeRefModel CallbackType { get; set; } = new (); + public TypeRefData CallbackType { get; set; } = new (); /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. public string JniSignature { get; set; } = ""; @@ -112,19 +112,19 @@ sealed class UcoMethodModel /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). /// -sealed class UcoConstructorModel +sealed class UcoConstructorData { /// Name of the generated wrapper, e.g., "nctor_0_uco". public string WrapperName { get; set; } = ""; /// Target type to pass to ActivateInstance. - public TypeRefModel TargetType { get; set; } = new (); + public TypeRefData TargetType { get; set; } = new (); } /// /// One JNI native method registration in RegisterNatives. /// -sealed class NativeRegistrationModel +sealed class NativeRegistrationData { /// JNI method name to register, e.g., "n_onCreate" or "nctor_0". public string JniMethodName { get; set; } = ""; diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs similarity index 77% rename from src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs rename to src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index 4e3a1636850..339494b3a40 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -5,11 +5,11 @@ namespace Microsoft.Android.Build.TypeMap; /// -/// Builds a from scanned records. +/// Builds a from scanned records. /// All decision logic (deduplication, ACW detection, callback resolution, proxy naming) lives here. -/// The output model is a plain data structure that the PE emitter translates 1:1. +/// The output model is a plain data structure that the emitter writes directly into a PE assembly. /// -sealed class TypeMapModelBuilder +sealed class ModelBuilder { /// /// Builds a TypeMap assembly model for the given peers. @@ -17,7 +17,7 @@ sealed class TypeMapModelBuilder /// Scanned Java peer types (typically from a single input assembly). /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . - public TypeMapAssemblyModel Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + public TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -29,7 +29,7 @@ public TypeMapAssemblyModel Build (IReadOnlyList peers, string out assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); string moduleName = Path.GetFileName (outputPath); - var model = new TypeMapAssemblyModel { + var model = new TypeMapAssemblyData { AssemblyName = assemblyName, ModuleName = moduleName, }; @@ -44,7 +44,7 @@ public TypeMapAssemblyModel Build (IReadOnlyList peers, string out bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; - ProxyTypeModel? proxy = null; + JavaPeerProxyData? proxy = null; if (hasProxy) { proxy = BuildProxyType (peer, isAcw); model.ProxyTypes.Add (proxy); @@ -56,13 +56,13 @@ public TypeMapAssemblyModel Build (IReadOnlyList peers, string out return model; } - static ProxyTypeModel BuildProxyType (JavaPeerInfo peer, bool isAcw) + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) { var proxyTypeName = peer.JavaName.Replace ('/', '_').Replace ('$', '_') + "_Proxy"; - var proxy = new ProxyTypeModel { + var proxy = new JavaPeerProxyData { TypeName = proxyTypeName, - TargetType = new TypeRefModel { + TargetType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, @@ -71,7 +71,7 @@ static ProxyTypeModel BuildProxyType (JavaPeerInfo peer, bool isAcw) }; if (peer.InvokerTypeName != null) { - proxy.InvokerType = new TypeRefModel { + proxy.InvokerType = new TypeRefData { ManagedTypeName = peer.InvokerTypeName, AssemblyName = peer.AssemblyName, }; @@ -86,7 +86,7 @@ static ProxyTypeModel BuildProxyType (JavaPeerInfo peer, bool isAcw) return proxy; } - static void BuildUcoMethods (JavaPeerInfo peer, ProxyTypeModel proxy) + static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) { int ucoIndex = 0; for (int i = 0; i < peer.MarshalMethods.Count; i++) { @@ -95,10 +95,10 @@ static void BuildUcoMethods (JavaPeerInfo peer, ProxyTypeModel proxy) continue; } - proxy.UcoMethods.Add (new UcoMethodModel { + proxy.UcoMethods.Add (new UcoMethodData { WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", CallbackMethodName = mm.NativeCallbackName, - CallbackType = new TypeRefModel { + CallbackType = new TypeRefData { ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, }, @@ -108,16 +108,16 @@ static void BuildUcoMethods (JavaPeerInfo peer, ProxyTypeModel proxy) } } - static void BuildUcoConstructors (JavaPeerInfo peer, ProxyTypeModel proxy) + static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) { if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) { return; } foreach (var ctor in peer.JavaConstructors) { - proxy.UcoConstructors.Add (new UcoConstructorModel { + proxy.UcoConstructors.Add (new UcoConstructorData { WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", - TargetType = new TypeRefModel { + TargetType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, @@ -125,12 +125,12 @@ static void BuildUcoConstructors (JavaPeerInfo peer, ProxyTypeModel proxy) } } - static void BuildNativeRegistrations (ProxyTypeModel proxy) + static void BuildNativeRegistrations (JavaPeerProxyData proxy) { foreach (var uco in proxy.UcoMethods) { // The JNI method name registered is the n_* callback name (e.g., "n_onCreate") // but we need the Java-side native method name which matches the callback name - proxy.NativeRegistrations.Add (new NativeRegistrationModel { + proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = uco.CallbackMethodName, JniSignature = uco.JniSignature, WrapperMethodName = uco.WrapperName, @@ -145,7 +145,7 @@ static void BuildNativeRegistrations (ProxyTypeModel proxy) jniName = jniName.Substring (0, ucoSuffix); } - proxy.NativeRegistrations.Add (new NativeRegistrationModel { + proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = jniName, // Constructor UCO wrappers have a fixed (IntPtr, IntPtr) signature — the JNI // signature for registration is the Java constructor's JNI signature. @@ -156,7 +156,7 @@ static void BuildNativeRegistrations (ProxyTypeModel proxy) } } - static TypeMapEntryModel BuildEntry (JavaPeerInfo peer, ProxyTypeModel? proxy, string outputAssemblyName) + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, string outputAssemblyName) { string typeRef; if (proxy != null) { @@ -165,7 +165,7 @@ static TypeMapEntryModel BuildEntry (JavaPeerInfo peer, ProxyTypeModel? proxy, s typeRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; } - return new TypeMapEntryModel { + return new TypeMapAttributeData { JniName = peer.JavaName, TypeReference = typeRef, }; diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index 48bacd9665b..480730a95cd 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -9,8 +9,8 @@ namespace Microsoft.Android.Build.TypeMap; /// -/// Emits a TypeMap PE assembly from a . -/// This is a mechanical translation — all decision logic lives in . +/// Emits a TypeMap PE assembly from a . +/// This is a mechanical translation — all decision logic lives in . /// sealed class TypeMapAssemblyEmitter { @@ -42,7 +42,7 @@ sealed class TypeMapAssemblyEmitter /// /// Emits a PE assembly from the given model and writes it to . /// - public void Emit (TypeMapAssemblyModel model, string outputPath) + public void Emit (TypeMapAssemblyData model, string outputPath) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -84,7 +84,7 @@ public void Emit (TypeMapAssemblyModel model, string outputPath) // ---- Assembly / Module ---- - void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyModel model) + void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyData model) { metadata.AddAssembly ( metadata.GetOrAddString (model.AssemblyName), @@ -217,7 +217,7 @@ void EmitModuleType (MetadataBuilder metadata) // ---- Proxy types ---- - void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyTypeModel proxy, + void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy, Dictionary wrapperHandles) { var typeDefHandle = metadata.AddTypeDefinition ( @@ -272,7 +272,7 @@ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyTypeMo } } - void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyTypeModel proxy) + void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy) { if (!proxy.HasActivation) { EmitBody (metadata, ilBuilder, "CreateInstance", @@ -312,7 +312,7 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, ProxyT } void EmitTypeGetter (MetadataBuilder metadata, BlobBuilder ilBuilder, string methodName, - TypeRefModel typeRef, MethodAttributes attrs) + TypeRefData typeRef, MethodAttributes attrs) { var handle = ResolveTypeRef (metadata, typeRef); @@ -330,7 +330,7 @@ void EmitTypeGetter (MetadataBuilder metadata, BlobBuilder ilBuilder, string met // ---- UCO wrappers ---- - MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoMethodModel uco) + MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoMethodData uco) { var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature); @@ -370,7 +370,7 @@ MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBu return handle; } - MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoConstructorModel uco) + MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoConstructorData uco) { var userTypeRef = ResolveTypeRef (metadata, uco.TargetType); @@ -398,7 +398,7 @@ MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder // ---- RegisterNatives ---- void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, - List registrations, Dictionary wrapperHandles) + List registrations, Dictionary wrapperHandles) { EmitBody (metadata, ilBuilder, "RegisterNatives", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | @@ -424,7 +424,7 @@ void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, // ---- TypeMap attributes ---- - void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapEntryModel entry) + void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) { var attrBlob = new BlobBuilder (); attrBlob.WriteUInt16 (0x0001); // Prolog @@ -503,7 +503,7 @@ static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandl return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob)); } - EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefModel typeRef) + EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefData typeRef) { var asmRef = FindOrAddAssemblyReference (metadata, typeRef.AssemblyName); return MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName); diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs index 264916b8d34..62ea1b77250 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -5,7 +5,7 @@ namespace Microsoft.Android.Build.TypeMap; /// /// High-level API: builds the model from peers, then emits the PE assembly. -/// Composes + . +/// Composes + . /// sealed class TypeMapAssemblyGenerator { @@ -17,7 +17,7 @@ sealed class TypeMapAssemblyGenerator /// Optional explicit assembly name. Derived from outputPath if null. public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) { - var builder = new TypeMapModelBuilder (); + var builder = new ModelBuilder (); var model = builder.Build (peers, outputPath, assemblyName); var emitter = new TypeMapAssemblyEmitter (); emitter.Emit (model, outputPath); diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 832bee9bd59..441536c7e29 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -6,11 +6,11 @@ namespace Microsoft.Android.Build.TypeMap.Tests; -public class TypeMapModelBuilderTests +public class ModelBuilderTests { static string TestFixtureAssemblyPath { get { - var testAssemblyDir = Path.GetDirectoryName (typeof (TypeMapModelBuilderTests).Assembly.Location)!; + var testAssemblyDir = Path.GetDirectoryName (typeof (ModelBuilderTests).Assembly.Location)!; var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); Assert.True (File.Exists (fixtureAssembly), $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); @@ -24,10 +24,10 @@ List ScanFixtures () return scanner.Scan (new [] { TestFixtureAssemblyPath }); } - TypeMapAssemblyModel BuildModel (IReadOnlyList peers, string? assemblyName = null) + TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); - var builder = new TypeMapModelBuilder (); + var builder = new ModelBuilder (); return builder.Build (peers, outputPath, assemblyName); } @@ -46,7 +46,7 @@ public void Build_EmptyPeers_ProducesEmptyModel () [Fact] public void Build_AssemblyNameDerivedFromOutputPath () { - var builder = new TypeMapModelBuilder (); + var builder = new ModelBuilder (); var model = builder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); Assert.Equal ("Foo.Bar", model.AssemblyName); Assert.Equal ("Foo.Bar.dll", model.ModuleName); @@ -55,7 +55,7 @@ public void Build_AssemblyNameDerivedFromOutputPath () [Fact] public void Build_ExplicitAssemblyName_OverridesOutputPath () { - var builder = new TypeMapModelBuilder (); + var builder = new ModelBuilder (); var model = builder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); Assert.Equal ("MyAssembly", model.AssemblyName); } From 13844f4cade65ca0529aab0122b1fbe9a790a552 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 19:41:37 +0100 Subject: [PATCH 12/43] Add extensive fixture-based model builder and pipeline tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 30 new tests covering every test fixture type: - MCW types: Object, Activity, Throwable, Exception, Service, Context, View, Button - User ACW types: MainActivity, MyHelper, TouchHandler, CustomView, AbstractBase - Interface types: IOnClickListener (with invoker dedup) - Nested types: Outer$Inner, ICallback$Result proxy naming - Multi-interface: ClickableView, MultiInterfaceView - Export methods: ExportExample - Generic types: GenericHolder - Full pipeline tests: scan → model → emit → read back PE Validates UCO wrapper signatures for all JNI types (bool, int, float, long, double, object, array), constructor wrappers, native registrations, TypeMap attribute counts, and proxy type names in emitted assemblies. 178 tests pass, 1 skipped. --- .../Generator/TypeMapModelBuilderTests.cs | 607 ++++++++++++++++++ 1 file changed, 607 insertions(+) diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 441536c7e29..46b5aba7589 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Xunit; namespace Microsoft.Android.Build.TypeMap.Tests; @@ -477,4 +479,609 @@ static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, IsConstructor = isConstructor, }; } + + // ======================================================================== + // Fixture-based tests: scan the real TestFixtures.dll and verify model output + // ======================================================================== + + JavaPeerInfo FindFixtureByJavaName (string javaName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) + { + return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName); + } + + TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) + { + return model.Entries.FirstOrDefault (e => e.JniName == jniName); + } + + // ---- MCW types from fixtures ---- + + [Fact] + public void Fixture_JavaLangObject_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "java_lang_Object_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("TestFixtures", proxy.TargetType.AssemblyName); + // MCW with DoNotGenerateAcw → not ACW + Assert.False (proxy.IsAcw); + Assert.Empty (proxy.UcoMethods); + Assert.Empty (proxy.UcoConstructors); + Assert.Empty (proxy.NativeRegistrations); + } + + [Fact] + public void Fixture_Activity_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "android_app_Activity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Android.App.Activity", proxy.TargetType.ManagedTypeName); + // MCW: DoNotGenerateAcw=true → not ACW (even though it has marshal methods) + Assert.False (proxy.IsAcw); + } + + [Fact] + public void Fixture_Activity_Entry_PointsToProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + var entry = FindEntry (model, "android/app/Activity"); + Assert.NotNull (entry); + Assert.Contains ("android_app_Activity_Proxy", entry!.TypeReference); + Assert.Contains ("MyTypeMap", entry.TypeReference); + } + + [Fact] + public void Fixture_Throwable_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Throwable"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "java_lang_Throwable_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.False (proxy.IsAcw); + } + + [Fact] + public void Fixture_Exception_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Exception"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "java_lang_Exception_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + } + + [Fact] + public void Fixture_Service_NoActivation_NoProxy () + { + // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object + // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor + var peer = FindFixtureByJavaName ("android/app/Service"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + Assert.Single (model.ProxyTypes); + } else { + Assert.Empty (model.ProxyTypes); + } + } + + [Fact] + public void Fixture_Context_HasActivation () + { + var peer = FindFixtureByJavaName ("android/content/Context"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // Context has (IntPtr, JniHandleOwnership) ctor + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "android_content_Context_Proxy"); + Assert.NotNull (proxy); + Assert.False (proxy!.IsAcw); + } + } + + [Fact] + public void Fixture_View_HasActivation () + { + var peer = FindFixtureByJavaName ("android/view/View"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "android_view_View_Proxy"); + Assert.NotNull (proxy); + } + } + + [Fact] + public void Fixture_Button_HasActivation () + { + var peer = FindFixtureByJavaName ("android/widget/Button"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "android_widget_Button_Proxy"); + Assert.NotNull (proxy); + } + } + + // ---- User ACW types from fixtures ---- + + [Fact] + public void Fixture_MainActivity_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.NotEmpty (peer.MarshalMethods); + Assert.NotNull (peer.ActivationCtor); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "my_app_MainActivity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.True (proxy.ImplementsIAndroidCallableWrapper); + Assert.True (proxy.HasActivation); + } + + [Fact] + public void Fixture_MainActivity_UcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "my_app_MainActivity_Proxy")!; + + // Should have UCO wrappers for non-constructor marshal methods + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); + + // Verify the onCreate wrapper + var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate"); + Assert.NotNull (onCreateUco); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature); + Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName); + } + + [Fact] + public void Fixture_MainActivity_NativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "my_app_MainActivity_Proxy")!; + + Assert.NotEmpty (proxy.NativeRegistrations); + + // Should register n_OnCreate + var onCreateReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_OnCreate"); + Assert.NotNull (onCreateReg); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateReg!.JniSignature); + } + + [Fact] + public void Fixture_MyHelper_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MyHelper"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // MyHelper has marshal methods and is not DoNotGenerateAcw + // Whether it's ACW depends on: not interface, has marshal methods, not DoNotGenerateAcw + if (peer.MarshalMethods.Count > 0 && peer.ActivationCtor != null) { + var proxy = FindProxy (model, "my_app_MyHelper_Proxy"); + Assert.NotNull (proxy); + } + } + + // ---- TouchHandler: various JNI types ---- + + [Fact] + public void Fixture_TouchHandler_AllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_TouchHandler_Proxy"); + Assert.NotNull (proxy); + + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count); + + // onTouch: (Landroid/view/View;I)Z + var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch"); + Assert.NotNull (onTouchUco); + Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature); + + // onFocusChange: (Landroid/view/View;Z)V + var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange"); + Assert.NotNull (onFocusUco); + Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature); + + // onScroll: (IFJD)V + var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll"); + Assert.NotNull (onScrollUco); + Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature); + + // getText: ()Ljava/lang/String; + var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText"); + Assert.NotNull (getTextUco); + Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature); + + // setItems: ([Ljava/lang/String;)V + var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems"); + Assert.NotNull (setItemsUco); + Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature); + } + + [Fact] + public void Fixture_TouchHandler_NativeRegistrationsMatchUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_TouchHandler_Proxy")!; + + // Every UCO method should have a matching registration + foreach (var uco in proxy.UcoMethods) { + var reg = proxy.NativeRegistrations.FirstOrDefault (r => r.WrapperMethodName == uco.WrapperName); + Assert.NotNull (reg); + Assert.Equal (uco.JniSignature, reg!.JniSignature); + } + } + + // ---- CustomView: registered constructors ---- + + [Fact] + public void Fixture_CustomView_HasTwoConstructorWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + Assert.Equal (2, peer.JavaConstructors.Count); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_CustomView_Proxy"); + Assert.NotNull (proxy); + + if (proxy!.IsAcw) { + Assert.Equal (2, proxy.UcoConstructors.Count); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); + + // Constructor registrations + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); + } + } + + // ---- Interface types ---- + + [Fact] + public void Fixture_IOnClickListener_HasInvokerProxy () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + Assert.True (listener!.IsInterface); + Assert.NotNull (listener.InvokerTypeName); + + var model = BuildModel (new [] { listener }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + Assert.NotNull (proxy!.InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); + } + + [Fact] + public void Fixture_IOnClickListener_IsNotAcw () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + + var model = BuildModel (new [] { listener! }, "TypeMap"); + + // Interface → not ACW even though it has marshal methods + foreach (var proxy in model.ProxyTypes) { + Assert.False (proxy.IsAcw); + } + } + + // ---- Nested types ---- + + [Fact] + public void Fixture_OuterInner_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/Outer$Inner"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // $ gets replaced with _ + var entry = FindEntry (model, "my/app/Outer$Inner"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "my_app_Outer_Inner_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.Outer+Inner", proxy!.TargetType.ManagedTypeName); + } + } + + [Fact] + public void Fixture_ICallbackResult_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/ICallback$Result"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var entry = FindEntry (model, "my/app/ICallback$Result"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "my_app_ICallback_Result_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.ICallback+Result", proxy!.TargetType.ManagedTypeName); + } + } + + // ---- Duplicate JNI names across interface + invoker ---- + + [Fact] + public void Fixture_InterfaceAndInvoker_ShareJniName_OnlyFirst () + { + var peers = ScanFixtures (); + // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" + var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickPeers.Count); + + var model = BuildModel (clickPeers, "TypeMap"); + + // Dedup: only one entry for this JNI name + var entries = model.Entries.Where (e => e.JniName == "android/view/View$OnClickListener").ToList (); + Assert.Single (entries); + } + + // ---- GenericHolder ---- + + [Fact] + public void Fixture_GenericHolder_Entry () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + Assert.True (peer.IsGenericDefinition); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + } + + // ---- AbstractBase ---- + + [Fact] + public void Fixture_AbstractBase_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/AbstractBase"); + Assert.True (peer.IsAbstract); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // AbstractBase has marshal methods (doWork) and activation ctor + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_AbstractBase_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + } + } + + // ---- ClickableView: implements interface ---- + + [Fact] + public void Fixture_ClickableView_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ClickableView"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_ClickableView_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + // Should have onClick UCO wrapper + var onClick = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + } + } + + // ---- MultiInterfaceView ---- + + [Fact] + public void Fixture_MultiInterfaceView_HasAllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_MultiInterfaceView_Proxy"); + Assert.NotNull (proxy); + + // Should have onClick and onLongClick UCO wrappers + Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); + Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); + } + } + + // ---- ExportExample ---- + + [Fact] + public void Fixture_ExportExample_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + Assert.False (peer.DoNotGenerateAcw); + Assert.Single (peer.MarshalMethods); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_ExportExample_Proxy"); + Assert.NotNull (proxy); + } + } + + // ---- Full pipeline: scan → model → emit → read back ---- + + [Fact] + public void FullPipeline_AllFixtures_ProducesLoadableAssembly () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "FullPipeline"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + Assert.True (File.Exists (outputPath)); + using var pe = new PEReader (File.OpenRead (outputPath)); + Assert.True (pe.HasMetadata); + + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name)); + + // Verify proxy types are present + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count); + + // Verify all proxy type names match + var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList (); + var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList (); + Assert.Equal (modelNames, proxyNames); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "AttrCount"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + + // Assembly attrs = TypeMap entries + IgnoresAccessChecksTo entries + int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "UcoAttrTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); + + var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList (); + Assert.NotEmpty (ucoMethods); + + // Each UCO method should have [UnmanagedCallersOnly] + foreach (var uco in ucoMethods) { + var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); + Assert.NotEmpty (attrs); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "my_app_CustomView_Proxy"); + + var methodNames = proxy.GetMethods () + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); + + Assert.Contains (".ctor", methodNames); + Assert.Contains ("CreateInstance", methodNames); + Assert.Contains ("get_TargetType", methodNames); + + if (model.ProxyTypes [0].IsAcw) { + Assert.Contains ("RegisterNatives", methodNames); + Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } } From b6568a2660df4354c20b92b404ee86e25a7d6fa4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 22:17:29 +0100 Subject: [PATCH 13/43] Add 3-arg trimmable TypeMap attrs, alias detection, root assembly generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical changes for TrimmableTypeMap: - TypeMapAttributeData now supports 2-arg (unconditional) and 3-arg (trimmable): - 2-arg: ACW user types (Android can instantiate), essential runtime types (java/lang/Object, Throwable, Exception, etc.) - 3-arg: MCW bindings and interfaces — trimmer preserves proxy only if target type is referenced by the app - Alias detection: when multiple .NET types share the same JNI name, they get indexed entries ("jni/name", "jni/name[1]", "jni/name[2]") with distinct proxy types - RootTypeMapAssemblyGenerator: generates _Microsoft.Android.TypeMaps.dll with [assembly: TypeMapAssemblyTarget("name")] for each per-assembly typemap assembly 208 tests pass, 1 skipped. --- .../Generator/Model/TypeMapAssemblyData.cs | 20 +- .../Generator/ModelBuilder.cs | 126 ++++++++-- .../Generator/RootTypeMapAssemblyGenerator.cs | 154 ++++++++++++ .../Generator/TypeMapAssemblyEmitter.cs | 37 ++- .../RootTypeMapAssemblyGeneratorTests.cs | 133 +++++++++++ .../TypeMapAssemblyGeneratorTests.cs | 19 +- .../Generator/TypeMapModelBuilderTests.cs | 225 +++++++++++++++++- 7 files changed, 659 insertions(+), 55 deletions(-) create mode 100644 src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs create mode 100644 tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs index bedf81830d7..f0c656b6d86 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -27,7 +27,11 @@ sealed class TypeMapAssemblyData } /// -/// One [assembly: TypeMap("jni/name", typeof(TargetOrProxy))] entry. +/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or +/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry. +/// +/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types. +/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app. /// sealed class TypeMapAttributeData { @@ -35,10 +39,20 @@ sealed class TypeMapAttributeData public string JniName { get; set; } = ""; /// - /// Assembly-qualified type reference for the attribute's Type argument. + /// Assembly-qualified proxy type reference string. /// Either points to a generated proxy or to the original managed type. /// - public string TypeReference { get; set; } = ""; + public string ProxyTypeReference { get; set; } = ""; + + /// + /// Assembly-qualified target type reference for the trimmable (3-arg) variant. + /// Null for unconditional (2-arg) entries. + /// The trimmer preserves the proxy only if this target type is used by the app. + /// + public string? TargetTypeReference { get; set; } + + /// True for 2-arg unconditional entries (ACW types, essential runtime types). + public bool IsUnconditional => TargetTypeReference == null; } /// diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index 339494b3a40..56ff04557ae 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -1,16 +1,29 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Microsoft.Android.Build.TypeMap; /// /// Builds a from scanned records. -/// All decision logic (deduplication, ACW detection, callback resolution, proxy naming) lives here. +/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute +/// selection, callback resolution, proxy naming) lives here. /// The output model is a plain data structure that the emitter writes directly into a PE assembly. /// sealed class ModelBuilder { + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { + "java/lang/Object", + "java/lang/Class", + "java/lang/String", + "java/lang/Throwable", + "java/lang/Exception", + "java/lang/RuntimeException", + "java/lang/Error", + "java/lang/Thread", + }; + /// /// Builds a TypeMap assembly model for the given peers. /// @@ -34,31 +47,97 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp ModuleName = moduleName, }; - var seenJniNames = new HashSet (StringComparer.Ordinal); - + // Group peers by JNI name to detect aliases (multiple .NET types → same Java class). + var groups = new Dictionary> (StringComparer.Ordinal); foreach (var peer in peers) { - if (!seenJniNames.Add (peer.JavaName)) { - continue; + if (!groups.TryGetValue (peer.JavaName, out var list)) { + list = new List (); + groups [peer.JavaName] = list; + } + list.Add (peer); + } + + foreach (var kvp in groups) { + string jniName = kvp.Key; + var peersForName = kvp.Value; + + if (peersForName.Count == 1) { + var peer = peersForName [0]; + EmitSinglePeer (model, peer, assemblyName); + } else { + EmitAliasedPeers (model, jniName, peersForName, assemblyName); } + } + + return model; + } + + void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemblyName) + { + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; + bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; + + JavaPeerProxyData? proxy = null; + if (hasProxy) { + proxy = BuildProxyType (peer, isAcw); + model.ProxyTypes.Add (proxy); + } + + model.Entries.Add (BuildEntry (peer, proxy, assemblyName)); + } + + void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, + List peersForName, string assemblyName) + { + // First peer is the "primary" — it gets the base JNI name entry. + // Remaining peers get indexed alias entries: "jni/name[0]", "jni/name[1]", ... + for (int i = 0; i < peersForName.Count; i++) { + var peer = peersForName [i]; + string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; JavaPeerProxyData? proxy = null; if (hasProxy) { - proxy = BuildProxyType (peer, isAcw); + string suffix = i == 0 ? "_Proxy" : $"_{i}_Proxy"; + proxy = BuildProxyType (peer, isAcw, suffix); model.ProxyTypes.Add (proxy); } - model.Entries.Add (BuildEntry (peer, proxy, assemblyName)); + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); } + } - return model; + /// + /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. + /// Unconditional types are always preserved by the trimmer. + /// + static bool IsUnconditionalEntry (JavaPeerInfo peer) + { + // Essential runtime types needed by the Java interop runtime + if (EssentialRuntimeTypes.Contains (peer.JavaName)) { + return true; + } + + // User-defined ACW types (not MCW bindings, not interfaces) are unconditional + // because Android can instantiate them from Java at any time. + if (!peer.DoNotGenerateAcw && !peer.IsInterface) { + return true; + } + + // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.) + if (peer.IsUnconditional) { + return true; + } + + return false; } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw, string? suffix = null) { - var proxyTypeName = peer.JavaName.Replace ('/', '_').Replace ('$', '_') + "_Proxy"; + suffix ??= "_Proxy"; + var proxyTypeName = peer.JavaName.Replace ('/', '_').Replace ('$', '_') + suffix; var proxy = new JavaPeerProxyData { TypeName = proxyTypeName, @@ -128,8 +207,6 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) static void BuildNativeRegistrations (JavaPeerProxyData proxy) { foreach (var uco in proxy.UcoMethods) { - // The JNI method name registered is the n_* callback name (e.g., "n_onCreate") - // but we need the Java-side native method name which matches the callback name proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = uco.CallbackMethodName, JniSignature = uco.JniSignature, @@ -138,7 +215,6 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } foreach (var uco in proxy.UcoConstructors) { - // Constructor wrapper name is "nctor_N_uco", JNI name is "nctor_N" string jniName = uco.WrapperName; int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); if (ucoSuffix >= 0) { @@ -147,27 +223,33 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = jniName, - // Constructor UCO wrappers have a fixed (IntPtr, IntPtr) signature — the JNI - // signature for registration is the Java constructor's JNI signature. - // For now, use "()V" as placeholder — the actual ctor signature is resolved at emit time. JniSignature = "()V", WrapperMethodName = uco.WrapperName, }); } } - static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, string outputAssemblyName) + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, + string outputAssemblyName, string? overrideJniName = null) { - string typeRef; + string proxyRef; if (proxy != null) { - typeRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; + proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; } else { - typeRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + bool isUnconditional = IsUnconditionalEntry (peer); + string? targetRef = null; + if (!isUnconditional) { + // Trimmable: the trimmer will preserve the proxy only if the target type is referenced. + targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; } return new TypeMapAttributeData { - JniName = peer.JavaName, - TypeReference = typeRef, + JniName = overrideJniName ?? peer.JavaName, + ProxyTypeReference = proxyRef, + TargetTypeReference = targetRef, }; } } diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..785bcbea24e --- /dev/null +++ b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Build.TypeMap; + +/// +/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references +/// all per-assembly typemap assemblies via [assembly: TypeMapAssemblyTarget("name")]. +/// +sealed class RootTypeMapAssemblyGenerator +{ + const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps"; + + /// + /// Generates the root typemap assembly. + /// + /// Names of per-assembly typemap assemblies to reference. + /// Path to write the output .dll. + /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). + public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= DefaultAssemblyName; + var moduleName = Path.GetFileName (outputPath); + + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + var metadata = new MetadataBuilder (); + var ilBuilder = new BlobBuilder (); + + // Assembly definition + metadata.AddAssembly ( + metadata.GetOrAddString (assemblyName), + new Version (1, 0, 0, 0), + culture: default, + publicKey: default, + flags: 0, + hashAlgorithm: AssemblyHashAlgorithm.None); + + // Module definition + metadata.AddModule ( + generation: 0, + metadata.GetOrAddString (moduleName), + metadata.GetOrAddGuid (Guid.NewGuid ()), + encId: default, + encBaseId: default); + + // Assembly reference for System.Runtime (needed for Attribute base class) + var systemRuntimeRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("System.Runtime"), + new Version (11, 0, 0, 0), default, default, 0, default); + + var systemRuntimeInteropServicesRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("System.Runtime.InteropServices"), + new Version (11, 0, 0, 0), default, default, 0, default); + + // type + metadata.AddTypeDefinition ( + default, default, + metadata.GetOrAddString (""), + default, + MetadataTokens.FieldDefinitionHandle (1), + MetadataTokens.MethodDefinitionHandle (1)); + + // TypeMapAssemblyTargetAttribute type definition + [assembly: ...] applications + var attributeTypeRef = metadata.AddTypeReference (systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); + + var baseAttrCtorRef = AddMemberRef (metadata, attributeTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + var stringRef = metadata.AddTypeReference (systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); + + // Define TypeMapAssemblyTargetAttribute with (string assemblyName) ctor + int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var ctorSigBlob = new BlobBuilder (); + new BlobEncoder (ctorSigBlob).MethodSignature (isInstanceMethod: true) + .Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ()); + + var ctorCodeBuilder = new BlobBuilder (); + var ctorEncoder = new InstructionEncoder (ctorCodeBuilder); + ctorEncoder.OpCode (ILOpCode.Ldarg_0); + ctorEncoder.Call (baseAttrCtorRef); + ctorEncoder.OpCode (ILOpCode.Ret); + + while (ilBuilder.Count % 4 != 0) { + ilBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); + int ctorBodyOffset = bodyEncoder.AddMethodBody (ctorEncoder); + + var ctorDef = metadata.AddMethodDefinition ( + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + MethodImplAttributes.IL, + metadata.GetOrAddString (".ctor"), + metadata.GetOrAddBlob (ctorSigBlob), + ctorBodyOffset, default); + + metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute"), + attributeTypeRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + // Add [assembly: TypeMapAssemblyTarget("name")] for each per-assembly typemap + foreach (var name in perAssemblyTypeMapNames) { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (1); // Prolog + attrBlob.WriteSerializedString (name); + attrBlob.WriteUInt16 (0); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, + metadata.GetOrAddBlob (attrBlob)); + } + + // Write PE + var peBuilder = new ManagedPEBuilder ( + new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (metadata), + ilBuilder); + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + using var fs = File.Create (outputPath); + peBlob.WriteContentTo (fs); + } + + static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, + Action encodeSig) + { + var blob = new BlobBuilder (); + encodeSig (new BlobEncoder (blob)); + return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob)); + } +} diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index 480730a95cd..e714cb7233e 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -37,7 +37,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; - MemberReferenceHandle _typeMapAttrCtorRef; + MemberReferenceHandle _typeMapAttrCtorRef2Arg; + MemberReferenceHandle _typeMapAttrCtorRef3Arg; /// /// Emits a PE assembly from the given model and writes it to . @@ -196,13 +197,24 @@ void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); - _typeMapAttrCtorRef = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional + _typeMapAttrCtorRef2Arg = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), p => { p.AddParameter ().Type ().Type (_stringRef, false); p.AddParameter ().Type ().Type (_systemTypeRef, false); })); + + // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable + _typeMapAttrCtorRef3Arg = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_stringRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); } void EmitModuleType (MetadataBuilder metadata) @@ -428,10 +440,23 @@ void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) { var attrBlob = new BlobBuilder (); attrBlob.WriteUInt16 (0x0001); // Prolog - attrBlob.WriteSerializedString (entry.JniName); - attrBlob.WriteSerializedString (entry.TypeReference); - attrBlob.WriteUInt16 (0x0000); // NumNamed - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef, metadata.GetOrAddBlob (attrBlob)); + + if (entry.IsUnconditional) { + // 2-arg: TypeMap(jniName, proxyType) — always preserved + attrBlob.WriteSerializedString (entry.JniName); + attrBlob.WriteSerializedString (entry.ProxyTypeReference); + attrBlob.WriteUInt16 (0x0000); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef2Arg, + metadata.GetOrAddBlob (attrBlob)); + } else { + // 3-arg: TypeMap(jniName, proxyType, targetType) — trimmable + attrBlob.WriteSerializedString (entry.JniName); + attrBlob.WriteSerializedString (entry.ProxyTypeReference); + attrBlob.WriteSerializedString (entry.TargetTypeReference!); + attrBlob.WriteUInt16 (0x0000); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef3Arg, + metadata.GetOrAddBlob (attrBlob)); + } } // ---- IgnoresAccessChecksTo ---- diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs new file mode 100644 index 00000000000..9d85c80ffbe --- /dev/null +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Build.TypeMap.Tests; + +public class RootTypeMapAssemblyGeneratorTests +{ + string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + { + var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}", + (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); + var generator = new RootTypeMapAssemblyGenerator (); + generator.Generate (perAssemblyNames, outputPath, assemblyName); + return outputPath; + } + + static void CleanUp (string path) + { + var dir = Path.GetDirectoryName (path); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + try { + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_DefaultAssemblyName () + { + var path = GenerateRootAssembly (Array.Empty ()); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("_Microsoft.Android.TypeMaps", reader.GetString (asmDef.Name)); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_CustomAssemblyName () + { + var path = GenerateRootAssembly (Array.Empty (), "MyRoot"); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("MyRoot", reader.GetString (asmDef.Name)); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_HasTypeMapAssemblyTargetAttributeType () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () + { + var path = GenerateRootAssembly (Array.Empty ()); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Empty (asmAttrs); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_MultipleTargets_HasCorrectAttributeCount () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + var path = GenerateRootAssembly (targets); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Equal (3, asmAttrs.Count ()); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_HasModuleType () + { + var path = GenerateRootAssembly (Array.Empty ()); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => reader.GetString (t.Name) == ""); + } finally { + CleanUp (path); + } + } +} diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 57f08f83616..7f63203c887 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -295,12 +295,12 @@ public void Generate_HasIgnoresAccessChecksToAttribute () } } - // ---- Deduplication tests ---- + // ---- Alias tests ---- [Fact] - public void Generate_DuplicateJniNames_KeepsFirstOnly () + public void Generate_DuplicateJniNames_CreatesAliasEntries () { - // Create two peers with the same JNI name + // Create two peers with the same JNI name — these become aliases var peers = new List { new JavaPeerInfo { JavaName = "test/Duplicate", @@ -318,17 +318,14 @@ public void Generate_DuplicateJniNames_KeepsFirstOnly () }, }; - var path = GenerateAssembly (peers, "DedupTest"); + var path = GenerateAssembly (peers, "AliasTest"); try { var (pe, reader) = OpenAssembly (path); using (pe) { - // Should only have one proxy for "test/Duplicate" - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Name) == "test_Duplicate_Proxy") - .ToList (); - // No proxies because neither peer has an activation ctor or invoker - Assert.Empty (proxyTypes); + // Neither peer has activation ctor → no proxies, but both get entries + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + // Should have 2 TypeMap entries + IgnoresAccessChecksTo entries + Assert.True (assemblyAttrs.Count () >= 2); } } finally { CleanUp (path); diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 46b5aba7589..9cc3dc984ae 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -87,7 +87,7 @@ public void Build_CreatesOneEntryPerPeer () } [Fact] - public void Build_DuplicateJniNames_KeepsFirstOnly () + public void Build_DuplicateJniNames_CreatesAliasEntries () { var peers = new List { MakeMcwPeer ("test/Dup", "Test.First", "A"), @@ -95,10 +95,134 @@ public void Build_DuplicateJniNames_KeepsFirstOnly () }; var model = BuildModel (peers); + // Two entries: primary "test/Dup" and alias "test/Dup[1]" + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("test/Dup", model.Entries [0].JniName); + Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); + Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); + Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); + } + + // ---- 2-arg (unconditional) vs 3-arg (trimmable) attributes ---- + + [Fact] + public void Build_EssentialRuntimeType_IsUnconditional () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + Assert.Null (model.Entries [0].TargetTypeReference); + } + + [Theory] + [InlineData ("java/lang/Object")] + [InlineData ("java/lang/Throwable")] + [InlineData ("java/lang/Exception")] + [InlineData ("java/lang/RuntimeException")] + [InlineData ("java/lang/Error")] + [InlineData ("java/lang/Class")] + [InlineData ("java/lang/String")] + [InlineData ("java/lang/Thread")] + public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) + { + var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); + } + + [Fact] + public void Build_UserAcwType_IsUnconditional () + { + // User-defined ACW types (not MCW, not interface) are unconditional + // because Android can instantiate them from Java + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); + + var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main"); + Assert.True (mainEntry.IsUnconditional); + Assert.Null (mainEntry.TargetTypeReference); + } + + [Fact] + public void Build_McwBinding_IsTrimmable () + { + // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential + var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); + Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!); + } + + [Fact] + public void Build_Interface_IsTrimmable () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); + } + + [Fact] + public void Build_UnconditionalScannedType_IsUnconditional () + { + // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); + peer.DoNotGenerateAcw = true; // simulate MCW-like + peer.IsUnconditional = true; // scanner marked it + var model = BuildModel (new [] { peer }); + + Assert.True (model.Entries [0].IsUnconditional); + } + + // ---- Alias tests ---- + + [Fact] + public void Build_AliasedPeers_GetIndexedJniNames () + { + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "A"), + MakeMcwPeer ("test/Dup", "Test.Second", "A"), + MakeMcwPeer ("test/Dup", "Test.Third", "A"), + }; + + var model = BuildModel (peers); + Assert.Equal (3, model.Entries.Count); Assert.Equal ("test/Dup", model.Entries [0].JniName); - // First one wins - type reference should point to Test.First - Assert.Contains ("Test.First", model.Entries [0].TypeReference); + Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); + Assert.Equal ("test/Dup[2]", model.Entries [2].JniName); + } + + [Fact] + public void Build_AliasedPeersWithActivation_GetDistinctProxies () + { + var peers = new List { + MakePeerWithActivation ("test/Dup", "Test.First", "A"), + MakePeerWithActivation ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers, "TypeMap"); + Assert.Equal (2, model.ProxyTypes.Count); + // Distinct proxy names: first gets _Proxy, second gets _1_Proxy + Assert.Equal ("test_Dup_Proxy", model.ProxyTypes [0].TypeName); + Assert.Equal ("test_Dup_1_Proxy", model.ProxyTypes [1].TypeName); } [Fact] @@ -110,7 +234,7 @@ public void Build_McwPeerWithoutActivation_NoProxy () Assert.Empty (model.ProxyTypes); Assert.Single (model.Entries); - Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].TypeReference); + Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); } // ---- Proxy types ---- @@ -167,8 +291,8 @@ public void Build_EntryPointsToProxy_WhenProxyExists () var model = BuildModel (new [] { peer }, "MyTypeMap"); var entry = model.Entries [0]; - Assert.Contains ("java_lang_Object_Proxy", entry.TypeReference); - Assert.Contains ("MyTypeMap", entry.TypeReference); + Assert.Contains ("java_lang_Object_Proxy", entry.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); } // ---- ACW detection ---- @@ -398,7 +522,7 @@ public void Build_FromScannedFixtures_ProducesValidModel () // All entries have non-empty JNI names Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); - Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.TypeReference))); + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference))); } [Fact] @@ -426,6 +550,80 @@ public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () } } + // ---- Fixture-based 2-arg vs 3-arg tests ---- + + [Fact] + public void Fixture_JavaLangObject_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_Throwable_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Throwable"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_Exception_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Exception"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_Activity_McwBinding_IsTrimmable () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + Assert.True (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + // Activity is MCW and not an essential runtime type → trimmable + Assert.False (model.Entries [0].IsUnconditional); + Assert.Contains ("Android.App.Activity", model.Entries [0].TargetTypeReference!); + } + + [Fact] + public void Fixture_MainActivity_UserAcw_IsUnconditional () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_IOnClickListener_Interface_IsTrimmable () + { + var peers = ScanFixtures (); + var listener = peers.First (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + var model = BuildModel (new [] { listener }); + Assert.False (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_TouchHandler_UserType_IsUnconditional () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + Assert.False (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Fact] + public void Fixture_Button_McwBinding_IsTrimmable () + { + var peer = FindFixtureByJavaName ("android/widget/Button"); + Assert.True (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.False (model.Entries [0].IsUnconditional); + } + // ---- Helpers ---- static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) @@ -544,8 +742,8 @@ public void Fixture_Activity_Entry_PointsToProxy () var entry = FindEntry (model, "android/app/Activity"); Assert.NotNull (entry); - Assert.Contains ("android_app_Activity_Proxy", entry!.TypeReference); - Assert.Contains ("MyTypeMap", entry.TypeReference); + Assert.Contains ("android_app_Activity_Proxy", entry!.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); } [Fact] @@ -841,7 +1039,7 @@ public void Fixture_ICallbackResult_ProxyNaming () // ---- Duplicate JNI names across interface + invoker ---- [Fact] - public void Fixture_InterfaceAndInvoker_ShareJniName_OnlyFirst () + public void Fixture_InterfaceAndInvoker_ShareJniName_CreateAliases () { var peers = ScanFixtures (); // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" @@ -850,9 +1048,10 @@ public void Fixture_InterfaceAndInvoker_ShareJniName_OnlyFirst () var model = BuildModel (clickPeers, "TypeMap"); - // Dedup: only one entry for this JNI name - var entries = model.Entries.Where (e => e.JniName == "android/view/View$OnClickListener").ToList (); - Assert.Single (entries); + // Aliases: primary entry + indexed alias + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); + Assert.Equal ("android/view/View$OnClickListener[1]", model.Entries [1].JniName); } // ---- GenericHolder ---- From 540c1bf9ac2048a08a8b8ea7b2cbc011fc77d448 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 22:36:12 +0100 Subject: [PATCH 14/43] Fix review findings: ctor JNI sig bug, deterministic ordering, test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review-driven fixes: Bug fixes: - Fix critical bug: constructor JNI signatures were hardcoded to "()V" in BuildNativeRegistrations. Now propagated from JavaConstructorInfo through UcoConstructorData.JniSignature to NativeRegistrationData. - Fix non-deterministic alias ordering: replaced Dictionary with SortedDictionary, sort alias peers by ManagedTypeName. - Fix Export attribute ThrownNames parsing: string[] was not being decoded from ImmutableArray>. Test improvements: - Cache scanner results with static Lazy<> in all test classes - Add parameterized constructor JNI signature test - Add fixture-based CustomView constructor signature assertions - Add PE blob validation tests (2-arg vs 3-arg TypeMap attributes) - Add determinism test (same input → same output) - Add Export with throws clause test fixture + JCW test - Fix Build_CreatesOneEntryPerPeer for alphabetical ordering Code quality: - Add ECMA-335 comment explaining Type args as serialized strings - Add comment explaining UCO constructor 2-param signature - Fix doc comment: remove 'Intermediate representation' wording 215 tests pass, 1 skipped. --- .../Generator/Model/TypeMapAssemblyData.cs | 5 +- .../Generator/ModelBuilder.cs | 11 +- .../Generator/TypeMapAssemblyEmitter.cs | 8 + .../Generator/JcwJavaSourceGeneratorTests.cs | 27 +- .../TypeMapAssemblyGeneratorTests.cs | 7 +- .../Generator/TypeMapModelBuilderTests.cs | 198 ++++++- .../TestFixtures/StubAttributes.cs | 4 + .../TestFixtures/TestTypes.cs | 518 ++++++++++++++---- 8 files changed, 649 insertions(+), 129 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs index f0c656b6d86..e9d72220fe1 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -4,7 +4,7 @@ namespace Microsoft.Android.Build.TypeMap; /// -/// Intermediate representation of a single TypeMap output assembly. +/// Data model for a single TypeMap output assembly. /// Describes what to emit — the emitter writes this directly into a PE assembly. /// Built by , consumed by . /// @@ -133,6 +133,9 @@ sealed class UcoConstructorData /// Target type to pass to ActivateInstance. public TypeRefData TargetType { get; set; } = new (); + + /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration. + public string JniSignature { get; set; } = "()V"; } /// diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index 56ff04557ae..25cb9cd919f 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -48,7 +48,8 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp }; // Group peers by JNI name to detect aliases (multiple .NET types → same Java class). - var groups = new Dictionary> (StringComparer.Ordinal); + // Use an ordered dictionary to ensure deterministic output across runs. + var groups = new SortedDictionary> (StringComparer.Ordinal); foreach (var peer in peers) { if (!groups.TryGetValue (peer.JavaName, out var list)) { list = new List (); @@ -61,6 +62,11 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp string jniName = kvp.Key; var peersForName = kvp.Value; + // Sort aliases by managed type name for deterministic proxy naming + if (peersForName.Count > 1) { + peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + } + if (peersForName.Count == 1) { var peer = peersForName [0]; EmitSinglePeer (model, peer, assemblyName); @@ -196,6 +202,7 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) foreach (var ctor in peer.JavaConstructors) { proxy.UcoConstructors.Add (new UcoConstructorData { WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", + JniSignature = ctor.JniSignature, TargetType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, @@ -223,7 +230,7 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = jniName, - JniSignature = "()V", + JniSignature = uco.JniSignature, WrapperMethodName = uco.WrapperName, }); } diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index e714cb7233e..c8e318a0b44 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -386,6 +386,12 @@ MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder { var userTypeRef = ResolveTypeRef (metadata, uco.TargetType); + // UCO constructor wrappers always take exactly (IntPtr jnienv, IntPtr self) regardless + // of the actual JNI constructor signature. The JNI parameters are not forwarded — + // ActivateInstance only needs the jobject handle to create the managed peer. + // The correct JNI signature is still used in RegisterNatives so the JNI runtime + // dispatches to this wrapper for the right constructor overload. + var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (2, @@ -438,6 +444,8 @@ void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) { + // Per ECMA-335 §II.23.3, System.Type-typed constructor arguments are encoded + // as SerString (assembly-qualified type name), not as TypeDefOrRef tokens. var attrBlob = new BlobBuilder (); attrBlob.WriteUInt16 (0x0001); // Prolog diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 1ca26f0596d..43c8f44ca9d 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -18,11 +18,12 @@ static string TestFixtureAssemblyPath { } } - List ScanFixtures () - { + static readonly Lazy> _cachedFixtures = new (() => { using var scanner = new JavaPeerScanner (); return scanner.Scan (new [] { TestFixtureAssemblyPath }); - } + }); + + static List ScanFixtures () => _cachedFixtures.Value; JavaPeerInfo FindByJavaName (List peers, string javaName) { @@ -290,4 +291,24 @@ public void Generate_CreatesCorrectFileStructure () } } } + + // ---- [Export] with throws clause ---- + + [Fact] + public void Generate_ExportWithThrows_HasThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportWithThrows"); + var java = GenerateToString (peer); + Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java); + } + + [Fact] + public void Generate_ExportWithoutThrows_HasNoThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportExample"); + var java = GenerateToString (peer); + Assert.DoesNotContain ("throws", java); + } } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 7f63203c887..6ea257534a8 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -21,11 +21,12 @@ static string TestFixtureAssemblyPath { } } - List ScanFixtures () - { + static readonly Lazy> _cachedFixtures = new (() => { using var scanner = new JavaPeerScanner (); return scanner.Scan (new [] { TestFixtureAssemblyPath }); - } + }); + + static List ScanFixtures () => _cachedFixtures.Value; string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) { diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9cc3dc984ae..e451d7c9ad2 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -20,11 +20,12 @@ static string TestFixtureAssemblyPath { } } - List ScanFixtures () - { + static readonly Lazy> _cachedFixtures = new (() => { using var scanner = new JavaPeerScanner (); return scanner.Scan (new [] { TestFixtureAssemblyPath }); - } + }); + + static List ScanFixtures () => _cachedFixtures.Value; TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { @@ -82,8 +83,9 @@ public void Build_CreatesOneEntryPerPeer () var model = BuildModel (peers); Assert.Equal (2, model.Entries.Count); - Assert.Equal ("java/lang/Object", model.Entries [0].JniName); - Assert.Equal ("android/app/Activity", model.Entries [1].JniName); + // Entries are ordered by JNI name (alphabetical) + Assert.Equal ("android/app/Activity", model.Entries [0].JniName); + Assert.Equal ("java/lang/Object", model.Entries [1].JniName); } [Fact] @@ -495,9 +497,37 @@ public void Build_NativeRegistrations_MatchUcoMethods () var ctorReg = proxy.NativeRegistrations [1]; Assert.Equal ("nctor_0", ctorReg.JniMethodName); + Assert.Equal ("()V", ctorReg.JniSignature); Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName); } + [Fact] + public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature () + { + var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App"); + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V", + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + } + }, + }; + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); + + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + } + [Fact] public void Build_NonAcwProxy_NoNativeRegistrations () { @@ -682,7 +712,7 @@ static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, // Fixture-based tests: scan the real TestFixtures.dll and verify model output // ======================================================================== - JavaPeerInfo FindFixtureByJavaName (string javaName) + static JavaPeerInfo FindFixtureByJavaName (string javaName) { var peers = ScanFixtures (); var peer = peers.FirstOrDefault (p => p.JavaName == javaName); @@ -690,12 +720,12 @@ JavaPeerInfo FindFixtureByJavaName (string javaName) return peer; } - JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) + static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) { return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName); } - TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) + static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) { return model.Entries.FirstOrDefault (e => e.JniName == jniName); } @@ -962,9 +992,15 @@ public void Fixture_CustomView_HasTwoConstructorWrappers () Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); - // Constructor registrations + // Constructor JNI signatures should be propagated + Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature); + + // Constructor registrations must use the actual JNI signatures var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); Assert.Equal (2, ctorRegs.Count); + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); } } @@ -1283,4 +1319,148 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () try { Directory.Delete (dir, true); } catch { } } } + + // ---- PE blob validation: 2-arg vs 3-arg TypeMap attributes ---- + + [Fact] + public void FullPipeline_EssentialType_Emits2ArgAttribute () + { + // java/lang/Object is essential → unconditional 2-arg attribute + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "Blob2Arg"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("java/lang/Object", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("java_lang_Object_Proxy", proxyRef!); + // 2-arg: no target type + Assert.Null (targetRef); + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_McwBinding_Emits3ArgAttribute () + { + // android/app/Activity is MCW → trimmable 3-arg attribute + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "Blob3Arg"); + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("android/app/Activity", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("android_app_Activity_Proxy", proxyRef!); + // 3-arg: has target type + Assert.NotNull (targetRef); + Assert.Contains ("Android.App.Activity", targetRef!); + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_UserAcw_Emits2ArgAttribute () + { + // my/app/MainActivity is user ACW → unconditional 2-arg + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "BlobAcw"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("my/app/MainActivity", jniName); + Assert.Null (targetRef); // unconditional → no target + } finally { + CleanUpDir (outputPath); + } + } + + // ---- Determinism ---- + + [Fact] + public void Build_SameInput_ProducesDeterministicOutput () + { + var peers = ScanFixtures (); + + var model1 = BuildModel (peers, "DetTest"); + var model2 = BuildModel (peers, "DetTest"); + + Assert.Equal (model1.Entries.Count, model2.Entries.Count); + for (int i = 0; i < model1.Entries.Count; i++) { + Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); + Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); + Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); + } + } + + // ---- Blob reading helpers ---- + + /// + /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef). + /// targetRef is null for 2-arg attributes. + /// + static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) + { + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + // Skip IgnoresAccessChecksTo attributes + if (attr.Constructor.Kind == HandleKind.MethodDefinition) + continue; + + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); // 0x0001 + if (prolog != 1) + continue; + + string? jniName = blobReader.ReadSerializedString (); + string? proxyRef = blobReader.ReadSerializedString (); + + // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg + string? targetRef = null; + if (blobReader.RemainingBytes > 2) { + targetRef = blobReader.ReadSerializedString (); + } + + return (jniName, proxyRef, targetRef); + } + + throw new InvalidOperationException ("No TypeMap attribute found on assembly"); + } + + static void CleanUpDir (string path) + { + var dir = Path.GetDirectoryName (path); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 36c7587eb28..e8b892815db 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -103,6 +103,10 @@ public sealed class ExportAttribute : Attribute { public string? Name { get; set; } + public string[]? ThrownNames { get; set; } + + public string? SuperArgumentsString { get; set; } + public ExportAttribute () { } public ExportAttribute (string name) => Name = name; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 35987f36f93..285aff424fe 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1,3 +1,7 @@ +// Test fixture types that exercise all scanner code paths. +// Each type is annotated with comments explaining which classification +// and behavior the scanner should produce. + using System; using Android.App; using Android.Content; @@ -8,14 +12,25 @@ namespace Java.Lang [Register ("java/lang/Object", DoNotGenerateAcw = true)] public class Object { - public Object () { } - protected Object (IntPtr handle, JniHandleOwnership transfer) { } + public Object () + { + } + + protected Object (IntPtr handle, JniHandleOwnership transfer) + { + } } +} +namespace Java.Lang +{ [Register ("java/lang/Throwable", DoNotGenerateAcw = true)] - public class Throwable : Object + public class Throwable : Java.Lang.Object { - protected Throwable (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Throwable (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")] public virtual string? Message { get; } @@ -24,7 +39,10 @@ protected Throwable (IntPtr handle, JniHandleOwnership transfer) : base (handle, [Register ("java/lang/Exception", DoNotGenerateAcw = true)] public class Exception : Throwable { - protected Exception (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Exception (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } @@ -33,29 +51,33 @@ namespace Android.App [Register ("android/app/Activity", DoNotGenerateAcw = true)] public class Activity : Java.Lang.Object { - public Activity () { } - protected Activity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + public Activity () + { + } + + protected Activity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual void OnCreate (object? savedInstanceState) { } + protected virtual void OnCreate (/* Bundle? */ object? savedInstanceState) + { + } [Register ("onStart", "()V", "")] - protected virtual void OnStart () { } + protected virtual void OnStart () + { + } } [Register ("android/app/Service", DoNotGenerateAcw = true)] public class Service : Java.Lang.Object { - protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - } -} - -namespace Android.App.Backup -{ - [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] - public class BackupAgent : Java.Lang.Object - { - protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Service (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } @@ -64,7 +86,10 @@ namespace Android.Content [Register ("android/content/Context", DoNotGenerateAcw = true)] public class Context : Java.Lang.Object { - protected Context (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Context (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } @@ -73,269 +98,540 @@ namespace Android.Views [Register ("android/view/View", DoNotGenerateAcw = true)] public class View : Java.Lang.Object { - protected View (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - } - - [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] - public interface IOnClickListener - { - [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] - void OnClick (View v); + protected View (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } +} - [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] - internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener +namespace Android.Widget +{ + [Register ("android/widget/Button", DoNotGenerateAcw = true)] + public class Button : Android.Views.View { - public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - public void OnClick (View v) { } + protected Button (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } - [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] - public interface IOnLongClickListener + [Register ("android/widget/TextView", DoNotGenerateAcw = true)] + public class TextView : Android.Views.View { - [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] - bool OnLongClick (View v); + protected TextView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } -namespace Android.Widget +namespace Android.Views { - [Register ("android/widget/Button", DoNotGenerateAcw = true)] - public class Button : Android.Views.View + [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] + public interface IOnClickListener { - protected Button (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] + void OnClick (View v); } - [Register ("android/widget/TextView", DoNotGenerateAcw = true)] - public class TextView : Android.Views.View + // Invoker types ARE internal implementation details. + // In real Mono.Android.dll, invokers DO have [Register] with DoNotGenerateAcw=true + // and the SAME JNI name as their interface. + // The scanner includes them — generators filter them later. + [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] + internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener { - protected TextView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + public void OnClick (View v) + { + } } } namespace MyApp { + // User types get their JNI name from [Activity(Name = "...")] + // NOT from [Register] — that's only on MCW binding types. [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] public class MainActivity : Android.App.Activity { - public MainActivity () { } + public MainActivity () + { + } [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected override void OnCreate (object? savedInstanceState) => base.OnCreate (savedInstanceState); + protected override void OnCreate (object? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } } + // User type without component attribute: TRIMMABLE [Register ("my/app/MyHelper")] public class MyHelper : Java.Lang.Object { [Register ("doSomething", "()V", "GetDoSomethingHandler")] - public virtual void DoSomething () { } + public virtual void DoSomething () + { + } } + // User service: UNCONDITIONAL — gets JNI name from [Service(Name = "...")] [Service (Name = "my.app.MyService")] public class MyService : Android.App.Service { - protected MyService (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected MyService (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } + // User broadcast receiver: UNCONDITIONAL — gets JNI name from [BroadcastReceiver(Name = "...")] [BroadcastReceiver (Name = "my.app.MyReceiver")] - public class MyReceiver : Java.Lang.Object { } + public class MyReceiver : Java.Lang.Object + { + } + // User content provider: UNCONDITIONAL — gets JNI name from [ContentProvider(Name = "...")] [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider")] - public class MyProvider : Java.Lang.Object { } + public class MyProvider : Java.Lang.Object + { + } +} +namespace MyApp.Generic +{ + [Register ("my/app/GenericHolder")] + public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + { + [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] + public virtual T? GetItem () + { + return default; + } + } +} + +namespace MyApp +{ [Register ("my/app/AbstractBase")] public abstract class AbstractBase : Java.Lang.Object { - protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("doWork", "()V", "")] public abstract void DoWork (); } +} +namespace MyApp +{ [Register ("my/app/SimpleActivity")] - public class SimpleActivity : Android.App.Activity { } + public class SimpleActivity : Android.App.Activity + { + // No (IntPtr, JniHandleOwnership) ctor — scanner should + // resolve to Activity's activation ctor + } +} +namespace MyApp +{ [Register ("my/app/ClickableView")] public class ClickableView : Android.Views.View, Android.Views.IOnClickListener { - protected ClickableView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected ClickableView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("onClick", "(Landroid/view/View;)V", "")] - public void OnClick (Android.Views.View v) { } + public void OnClick (Android.Views.View v) + { + } } +} +namespace MyApp +{ [Register ("my/app/CustomView")] public class CustomView : Android.Views.View { - protected CustomView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("", "()V", "")] - public CustomView () : base (default!, default) { } + public CustomView () + : base (default!, default) + { + } [Register ("", "(Landroid/content/Context;)V", "")] - public CustomView (Context context) : base (default!, default) { } + public CustomView (Context context) + : base (default!, default) + { + } } +} +namespace MyApp +{ [Register ("my/app/Outer")] public class Outer : Java.Lang.Object { [Register ("my/app/Outer$Inner")] public class Inner : Java.Lang.Object { - protected Inner (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Inner (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } +} +namespace MyApp +{ [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")] public interface ICallback { [Register ("my/app/ICallback$Result")] public class Result : Java.Lang.Object { - protected Result (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected Result (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } } +} +namespace MyApp +{ [Register ("my/app/TouchHandler")] public class TouchHandler : Java.Lang.Object { + // bool return type (non-blittable, needs byte wrapper in UCO) [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")] - public virtual bool OnTouch (Android.Views.View v, int action) => false; + public virtual bool OnTouch (Android.Views.View v, int action) + { + return false; + } + // bool parameter (non-blittable) [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")] - public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) { } + public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) + { + } + // Multiple params of different JNI types [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")] - public virtual void OnScroll (int x, float y, long timestamp, double velocity) { } + public virtual void OnScroll (int x, float y, long timestamp, double velocity) + { + } + // Object return type [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")] - public virtual string? GetText () => null; + public virtual string? GetText () + { + return null; + } + // Array parameter [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")] - public virtual void SetItems (string[]? items) { } + public virtual void SetItems (string[]? items) + { + } } +} +namespace MyApp +{ [Register ("my/app/ExportExample")] public class ExportExample : Java.Lang.Object { [Java.Interop.Export ("myExportedMethod")] - public void MyExportedMethod () { } + public void MyExportedMethod () + { + } } + [Register ("my/app/ExportWithThrows")] + public class ExportWithThrows : Java.Lang.Object + { + [Java.Interop.Export ("riskyMethod", ThrownNames = new [] { "java.io.IOException", "java.lang.IllegalStateException" })] + public void RiskyMethod () + { + } + } +} + +namespace Android.App.Backup +{ + [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] + public class BackupAgent : Java.Lang.Object + { + protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace MyApp +{ [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] - public class MyApplication : Java.Lang.Object { } + public class MyApplication : Java.Lang.Object + { + } [Instrumentation (Name = "my.app.MyInstrumentation")] - public class MyInstrumentation : Java.Lang.Object { } + public class MyInstrumentation : Java.Lang.Object + { + } + // BackupAgent without a component attribute — would normally be trimmable, + // but [Application(BackupAgent = typeof(...))] should force it unconditional. [Register ("my/app/MyBackupAgent")] public class MyBackupAgent : Android.App.Backup.BackupAgent { - protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } + // Activity without [Activity] attribute — would normally be trimmable, + // but [Application(ManageSpaceActivity = typeof(...))] should force it unconditional. [Register ("my/app/MyManageSpaceActivity")] public class MyManageSpaceActivity : Android.App.Activity { - protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } } - public class UnregisteredHelper : Java.Lang.Object { } + // User type WITHOUT [Register] — gets CRC64-computed JNI name. + // CompatJniName should use raw namespace instead of CRC64. + public class UnregisteredHelper : Java.Lang.Object + { + } +} +namespace MyApp +{ [Register ("my/app/MyButton")] public class MyButton : Android.Widget.Button { - protected MyButton (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected MyButton (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] + public interface IOnLongClickListener + { + [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] + bool OnLongClick (View v); } +} +namespace MyApp +{ [Register ("my/app/MultiInterfaceView")] public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener { - protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } [Register ("onClick", "(Landroid/view/View;)V", "")] public void OnClick (Android.Views.View v) { } [Register ("onLongClick", "(Landroid/view/View;)Z", "")] - public bool OnLongClick (Android.Views.View v) => false; + public bool OnLongClick (Android.Views.View v) { return false; } } + // User type with a custom IJniNameProviderAttribute — the scanner + // should detect this via interface resolution, not hardcoded attribute names. [CustomJniName ("com.example.CustomWidget")] - public class CustomWidget : Java.Lang.Object { } + public class CustomWidget : Java.Lang.Object + { + } +} - [Activity (Name = "my.app.BaseActivityNoRegister")] - public class BaseActivityNoRegister : Android.App.Activity { } +// ================================================================ +// Edge case: generic base type (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] + public class GenericBase : Java.Lang.Object where T : class + { + protected GenericBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } - public class DerivedFromComponentBase : BaseActivityNoRegister { } + [Register ("my/app/ConcreteFromGeneric")] + public class ConcreteFromGeneric : GenericBase + { + protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} - [Register ("my/app/RegisteredParent")] - public class RegisteredParent : Java.Lang.Object +// ================================================================ +// Edge case: generic interface (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/IGenericCallback", "", "")] + public interface IGenericCallback { - public class UnregisteredChild : Java.Lang.Object { } } - [Register ("my/app/DeepOuter")] - public class DeepOuter : Java.Lang.Object + [Register ("my/app/GenericCallbackImpl")] + public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback { - public class Middle : Java.Lang.Object + protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) { - public class DeepInner : Java.Lang.Object { } } } +} - public class PlainActivitySubclass : Android.App.Activity { } - - [Activity (Label = "Unnamed")] - public class UnnamedActivity : Android.App.Activity { } +// ================================================================ +// Edge case: component-only base detection +// ================================================================ +namespace MyApp +{ + [Activity (Name = "my.app.BaseActivityNoRegister")] + public class BaseActivityNoRegister : Android.App.Activity + { + } - public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + public class DerivedFromComponentBase : BaseActivityNoRegister { - [Register ("onClick", "(Landroid/view/View;)V", "")] - public void OnClick (Android.Views.View v) { } } +} - public class UnregisteredExporter : Java.Lang.Object +// ================================================================ +// Edge case: unregistered nested type inside [Register] parent +// ================================================================ +namespace MyApp +{ + [Register ("my/app/RegisteredParent")] + public class RegisteredParent : Java.Lang.Object { - [Java.Interop.Export ("doExportedWork")] - public void DoExportedWork () { } + public class UnregisteredChild : Java.Lang.Object + { + } } } -namespace MyApp.Generic +// ================================================================ +// Edge case: 3-level deep nesting +// ComputeTypeNameParts must walk multiple levels, collecting names. +// ================================================================ +namespace MyApp { - [Register ("my/app/GenericHolder")] - public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + [Register ("my/app/DeepOuter")] + public class DeepOuter : Java.Lang.Object { - [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] - public virtual T? GetItem () => default; + public class Middle : Java.Lang.Object + { + public class DeepInner : Java.Lang.Object + { + } + } } +} - [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] - public class GenericBase : Java.Lang.Object where T : class +// ================================================================ +// Edge case: plain Java peer subclass — no [Register], no component attribute +// ExtendsJavaPeer must detect it via base type chain, gets CRC64 name. +// ================================================================ +namespace MyApp +{ + public class PlainActivitySubclass : Android.App.Activity { - protected GenericBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } +} - [Register ("my/app/ConcreteFromGeneric")] - public class ConcreteFromGeneric : GenericBase +// ================================================================ +// Edge case: component attribute WITHOUT Name property +// HasComponentAttribute = true but ComponentAttributeJniName = null. +// Type should still get a CRC64 JNI name (not null). +// ================================================================ +namespace MyApp +{ + [Activity (Label = "Unnamed")] + public class UnnamedActivity : Android.App.Activity { - protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } +} - [Register ("my/app/IGenericCallback", "", "")] - public interface IGenericCallback { } +// ================================================================ +// Edge case: interface implementation on unregistered type +// Type gets CRC64 JNI name but still resolves interface names. +// ================================================================ +namespace MyApp +{ + public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) + { + } + } +} - [Register ("my/app/GenericCallbackImpl")] - public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback +// ================================================================ +// Edge case: [Export] method on unregistered type +// ParseExportAttribute runs on a type that gets CRC64 JNI name. +// ================================================================ +namespace MyApp +{ + public class UnregisteredExporter : Java.Lang.Object { - protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + [Java.Interop.Export ("doExportedWork")] + public void DoExportedWork () + { + } } } +// ================================================================ +// Edge case: type in empty namespace +// ================================================================ [Register ("my/app/GlobalType")] public class GlobalType : Java.Lang.Object { - protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } + protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } } -public class GlobalUnregisteredType : Java.Lang.Object { } +public class GlobalUnregisteredType : Java.Lang.Object +{ +} From c0a9acef434f5cc032ea5b1818ea9196ca56b1de Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 22:41:54 +0100 Subject: [PATCH 15/43] Fix review round 2: canonical string encoding, type ref caching, computed IgnoresAccessChecksTo, edge-case tests --- .../Generator/Model/TypeMapAssemblyData.cs | 2 +- .../Generator/ModelBuilder.cs | 14 +++++++ .../Generator/TypeMapAssemblyEmitter.cs | 17 +++++--- .../TypeMapAssemblyGeneratorTests.cs | 33 +++++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 42 ++++++++++++++++++- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs index e9d72220fe1..c1a44569e4a 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -23,7 +23,7 @@ sealed class TypeMapAssemblyData public List ProxyTypes { get; } = new (); /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. - public List IgnoresAccessChecksTo { get; } = new () { "Mono.Android", "Java.Interop" }; + public List IgnoresAccessChecksTo { get; } = new (); } /// diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index 25cb9cd919f..94f4725cdd5 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -75,6 +75,20 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp } } + // Compute IgnoresAccessChecksTo from actual cross-assembly references in UCO callback types + var referencedAssemblies = new SortedSet (StringComparer.Ordinal); + foreach (var proxy in model.ProxyTypes) { + foreach (var uco in proxy.UcoMethods) { + if (!string.Equals (uco.CallbackType.AssemblyName, assemblyName, StringComparison.Ordinal)) { + referencedAssemblies.Add (uco.CallbackType.AssemblyName); + } + } + if (proxy.TargetType != null && !string.Equals (proxy.TargetType.AssemblyName, assemblyName, StringComparison.Ordinal)) { + referencedAssemblies.Add (proxy.TargetType.AssemblyName); + } + } + model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); + return model; } diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index c8e318a0b44..b9780c29a19 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -15,6 +15,7 @@ namespace Microsoft.Android.Build.TypeMap; sealed class TypeMapAssemblyEmitter { readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); + readonly Dictionary _typeRefCache = new (StringComparer.Ordinal); AssemblyReferenceHandle _systemRuntimeRef; AssemblyReferenceHandle _monoAndroidRef; @@ -27,7 +28,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; - TypeReferenceHandle _stringRef; TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _trimmableNativeRegistrationRef; @@ -126,8 +126,6 @@ void EmitTypeReferences (MetadataBuilder metadata) metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); _runtimeTypeHandleRef = metadata.AddTypeReference (_systemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); - _stringRef = metadata.AddTypeReference (_systemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); _trimmableNativeRegistrationRef = metadata.AddTypeReference (_monoAndroidRef, @@ -202,7 +200,7 @@ void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), p => { - p.AddParameter ().Type ().Type (_stringRef, false); + p.AddParameter ().Type ().String (); p.AddParameter ().Type ().Type (_systemTypeRef, false); })); @@ -211,7 +209,7 @@ void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, rt => rt.Void (), p => { - p.AddParameter ().Type ().Type (_stringRef, false); + p.AddParameter ().Type ().String (); p.AddParameter ().Type ().Type (_systemTypeRef, false); p.AddParameter ().Type ().Type (_systemTypeRef, false); })); @@ -538,8 +536,15 @@ static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandl EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefData typeRef) { + // Cache key: "AssemblyName:ManagedTypeName" to avoid duplicate TypeRef rows + var cacheKey = $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}"; + if (_typeRefCache.TryGetValue (cacheKey, out var cached)) { + return cached; + } var asmRef = FindOrAddAssemblyReference (metadata, typeRef.AssemblyName); - return MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName); + var result = MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName); + _typeRefCache [cacheKey] = result; + return result; } TypeReferenceHandle MakeTypeRefForManagedName (MetadataBuilder metadata, EntityHandle scope, string managedTypeName) diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 6ea257534a8..147d4df078b 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -435,6 +435,39 @@ public void ParseReturnType_Object () Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); } + // ---- Negative / edge-case tests ---- + + [Theory] + [InlineData ("")] + [InlineData ("not-a-sig")] + [InlineData ("(")] + public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature) + { + // Should not crash — either returns empty or throws ArgumentException + try { + var result = JniSignatureHelper.ParseParameterTypes (signature); + // If it doesn't throw, empty is acceptable + Assert.NotNull (result); + } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) { + // Any of these are acceptable for malformed input + } + } + + [Fact] + public void Generate_NullPeers_ThrowsArgumentNull () + { + var gen = new TypeMapAssemblyGenerator (); + var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll"); + Assert.Throws (() => gen.Generate (null!, tmpPath)); + } + + [Fact] + public void Generate_NullOutputPath_ThrowsArgumentNull () + { + var gen = new TypeMapAssemblyGenerator (); + Assert.Throws (() => gen.Generate (Array.Empty (), null!)); + } + static void CleanUp (string path) { var dir = Path.GetDirectoryName (path); diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index e451d7c9ad2..aca8ac52e5b 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -64,11 +64,29 @@ public void Build_ExplicitAssemblyName_OverridesOutputPath () } [Fact] - public void Build_DefaultIgnoresAccessChecksTo () + public void Build_EmptyInput_HasEmptyIgnoresAccessChecksTo () { var model = BuildModel (Array.Empty ()); + Assert.Empty (model.IgnoresAccessChecksTo); + } + + [Fact] + public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + { + var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp"); + ((List) peer.MarshalMethods).Add (new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + IsConstructor = false, + DeclaringTypeName = "Android.App.Activity", + DeclaringAssemblyName = "Mono.Android", + }); + var model = BuildModel (new [] { peer }); + // The UCO callback type references Mono.Android, which is cross-assembly Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); - Assert.Contains ("Java.Interop", model.IgnoresAccessChecksTo); + // The output assembly itself should not appear + Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo); } // ---- TypeMap entries ---- @@ -276,6 +294,26 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } + [Fact] + public void Build_PeerWithInvokerButNoActivation_ProxyHasActivationFalse () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.False (proxy.HasActivation); + Assert.NotNull (proxy.InvokerType); + } + [Fact] public void Build_ProxyNaming_ReplacesSlashAndDollar () { From 5f19ddeecc92d1d95ecba1b08945012899649a65 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 22:52:59 +0100 Subject: [PATCH 16/43] Fix review round 3: static ModelBuilder, Implementor/EventDispatcher exclusion, invoker filtering, managed-name proxy naming, root assembly cleanup - Make ModelBuilder a static class with Build() method - Add IsInvokerType() to filter invokers from alias grouping (get proxy, no TypeMap entry) - Add IsImplementorOrEventDispatcher() to exclude from unconditional entries - Change proxy naming from JNI-based to managed-name-based for uniqueness - Remove unused stringRef/systemRuntimeInteropServicesRef from RootTypeMapAssemblyGenerator - Add Implementor and EventDispatcher test fixtures - Add tests for Implementor/EventDispatcher trimmability - Add root assembly attribute blob value verification test - All 225 tests pass --- .../Generator/ModelBuilder.cs | 69 ++++++++-- .../Generator/RootTypeMapAssemblyGenerator.cs | 7 - .../Generator/TypeMapAssemblyGenerator.cs | 3 +- .../RootTypeMapAssemblyGeneratorTests.cs | 30 +++++ .../TypeMapAssemblyGeneratorTests.cs | 10 +- .../Generator/TypeMapModelBuilderTests.cs | 122 ++++++++++++------ .../TestFixtures/TestTypes.cs | 41 ++++++ 7 files changed, 213 insertions(+), 69 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index 94f4725cdd5..aa47256af42 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Build.TypeMap; /// selection, callback resolution, proxy naming) lives here. /// The output model is a plain data structure that the emitter writes directly into a PE assembly. /// -sealed class ModelBuilder +static class ModelBuilder { static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", @@ -30,7 +30,7 @@ sealed class ModelBuilder /// Scanned Java peer types (typically from a single input assembly). /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . - public TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -47,10 +47,30 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp ModuleName = moduleName, }; - // Group peers by JNI name to detect aliases (multiple .NET types → same Java class). + // Separate invoker types — they get proxies (for CreateInstance) but NOT TypeMap entries. + // Invokers share JNI names with their interfaces and are created via the parent proxy. + var invokerPeers = new List (); + var nonInvokerPeers = new List (); + foreach (var peer in peers) { + if (IsInvokerType (peer)) { + invokerPeers.Add (peer); + } else { + nonInvokerPeers.Add (peer); + } + } + + // Generate proxies for invoker types (no TypeMap entries) + foreach (var invoker in invokerPeers) { + if (invoker.ActivationCtor != null) { + var proxy = BuildProxyType (invoker, isAcw: false); + model.ProxyTypes.Add (proxy); + } + } + + // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). // Use an ordered dictionary to ensure deterministic output across runs. var groups = new SortedDictionary> (StringComparer.Ordinal); - foreach (var peer in peers) { + foreach (var peer in nonInvokerPeers) { if (!groups.TryGetValue (peer.JavaName, out var list)) { list = new List (); groups [peer.JavaName] = list; @@ -92,7 +112,16 @@ public TypeMapAssemblyData Build (IReadOnlyList peers, string outp return model; } - void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemblyName) + /// + /// Invoker types (e.g., IOnClickListenerInvoker) wrap existing Java interfaces. + /// They get proxies for CreateInstance but no TypeMap entries — the interface entry covers them. + /// + static bool IsInvokerType (JavaPeerInfo peer) + { + return peer.DoNotGenerateAcw && peer.ManagedTypeName.EndsWith ("Invoker", StringComparison.Ordinal); + } + + static void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemblyName) { bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; @@ -106,11 +135,11 @@ void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemb model.Entries.Add (BuildEntry (peer, proxy, assemblyName)); } - void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, + static void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, List peersForName, string assemblyName) { // First peer is the "primary" — it gets the base JNI name entry. - // Remaining peers get indexed alias entries: "jni/name[0]", "jni/name[1]", ... + // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... for (int i = 0; i < peersForName.Count; i++) { var peer = peersForName [i]; string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; @@ -120,8 +149,7 @@ void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, JavaPeerProxyData? proxy = null; if (hasProxy) { - string suffix = i == 0 ? "_Proxy" : $"_{i}_Proxy"; - proxy = BuildProxyType (peer, isAcw, suffix); + proxy = BuildProxyType (peer, isAcw); model.ProxyTypes.Add (proxy); } @@ -140,6 +168,12 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return true; } + // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event + // is subscribed). They should NOT be unconditional — they're trimmable. + if (IsImplementorOrEventDispatcher (peer)) { + return false; + } + // User-defined ACW types (not MCW bindings, not interfaces) are unconditional // because Android can instantiate them from Java at any time. if (!peer.DoNotGenerateAcw && !peer.IsInterface) { @@ -154,10 +188,21 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return false; } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw, string? suffix = null) + /// + /// Implementor and EventDispatcher types are generated by the binding generator + /// and are only instantiated from .NET. They should be trimmable. + /// + static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) + { + return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || + peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal); + } + + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) { - suffix ??= "_Proxy"; - var proxyTypeName = peer.JavaName.Replace ('/', '_').Replace ('$', '_') + suffix; + // Use managed type name for proxy naming to guarantee uniqueness across aliases + // (two types with the same JNI name will have different managed names). + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; var proxy = new JavaPeerProxyData { TypeName = proxyTypeName, diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 785bcbea24e..a664dc4d1a9 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -64,10 +64,6 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp metadata.GetOrAddString ("System.Runtime"), new Version (11, 0, 0, 0), default, default, 0, default); - var systemRuntimeInteropServicesRef = metadata.AddAssemblyReference ( - metadata.GetOrAddString ("System.Runtime.InteropServices"), - new Version (11, 0, 0, 0), default, default, 0, default); - // type metadata.AddTypeDefinition ( default, default, @@ -83,9 +79,6 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp var baseAttrCtorRef = AddMemberRef (metadata, attributeTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - var stringRef = metadata.AddTypeReference (systemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); - // Define TypeMapAssemblyTargetAttribute with (string assemblyName) ctor int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs index 62ea1b77250..de1cf54ecdb 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -17,8 +17,7 @@ sealed class TypeMapAssemblyGenerator /// Optional explicit assembly name. Derived from outputPath if null. public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) { - var builder = new ModelBuilder (); - var model = builder.Build (peers, outputPath, assemblyName); + var model = ModelBuilder.Build (peers, outputPath, assemblyName); var emitter = new TypeMapAssemblyEmitter (); emitter.Emit (model, outputPath); } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 9d85c80ffbe..1bef48d5ace 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -130,4 +130,34 @@ public void Generate_HasModuleType () CleanUp (path); } } + + [Fact] + public void Generate_AttributeBlobValues_MatchTargetNames () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + var path = GenerateRootAssembly (targets); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + + var attrValues = new List (); + foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobReader (attr.Value); + + // Custom attribute blob: prolog (2 bytes) + SerString value + var prolog = blob.ReadUInt16 (); + Assert.Equal (1, prolog); // ECMA-335 prolog + var value = blob.ReadSerializedString (); + Assert.NotNull (value); + attrValues.Add (value!); + } + + Assert.Equal (2, attrValues.Count); + Assert.Contains ("_App.TypeMap", attrValues); + Assert.Contains ("_Mono.Android.TypeMap", attrValues); + } finally { + CleanUp (path); + } + } } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 147d4df078b..710773acb87 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -155,8 +155,8 @@ public void Generate_CreatesProxyTypes () // At least some proxy types should be generated Assert.NotEmpty (proxyTypes); - // Check that a proxy exists for java/lang/Object → java_lang_Object_Proxy - Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "java_lang_Object_Proxy"); + // Check that a proxy exists for java/lang/Object → Java_Lang_Object_Proxy + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); } } finally { CleanUp (path); @@ -198,7 +198,7 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () using (pe) { var objectProxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "java_lang_Object_Proxy"); + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); var methods = objectProxy.GetMethods () .Select (h => reader.GetMethodDefinition (h)) @@ -228,7 +228,7 @@ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () using (pe) { var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); var methods = proxy.GetMethods () .Select (h => reader.GetMethodDefinition (h)) @@ -255,7 +255,7 @@ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () using (pe) { var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); // Find a UCO method var ucoMethod = proxy.GetMethods () diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index aca8ac52e5b..de42e8182cb 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -30,8 +30,7 @@ static string TestFixtureAssemblyPath { TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); - var builder = new ModelBuilder (); - return builder.Build (peers, outputPath, assemblyName); + return ModelBuilder.Build (peers, outputPath, assemblyName); } // ---- Basic model structure ---- @@ -49,8 +48,7 @@ public void Build_EmptyPeers_ProducesEmptyModel () [Fact] public void Build_AssemblyNameDerivedFromOutputPath () { - var builder = new ModelBuilder (); - var model = builder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); Assert.Equal ("Foo.Bar", model.AssemblyName); Assert.Equal ("Foo.Bar.dll", model.ModuleName); } @@ -58,8 +56,7 @@ public void Build_AssemblyNameDerivedFromOutputPath () [Fact] public void Build_ExplicitAssemblyName_OverridesOutputPath () { - var builder = new ModelBuilder (); - var model = builder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); Assert.Equal ("MyAssembly", model.AssemblyName); } @@ -240,9 +237,9 @@ public void Build_AliasedPeersWithActivation_GetDistinctProxies () var model = BuildModel (peers, "TypeMap"); Assert.Equal (2, model.ProxyTypes.Count); - // Distinct proxy names: first gets _Proxy, second gets _1_Proxy - Assert.Equal ("test_Dup_Proxy", model.ProxyTypes [0].TypeName); - Assert.Equal ("test_Dup_1_Proxy", model.ProxyTypes [1].TypeName); + // Distinct proxy names based on managed type names + Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName); + Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName); } [Fact] @@ -267,7 +264,7 @@ public void Build_PeerWithActivationCtor_CreatesProxy () Assert.Single (model.ProxyTypes); var proxy = model.ProxyTypes [0]; - Assert.Equal ("java_lang_Object_Proxy", proxy.TypeName); + Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName); Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); Assert.True (proxy.HasActivation); Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); @@ -315,13 +312,13 @@ public void Build_PeerWithInvokerButNoActivation_ProxyHasActivationFalse () } [Fact] - public void Build_ProxyNaming_ReplacesSlashAndDollar () + public void Build_ProxyNaming_ReplacesDotAndPlus () { var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); - Assert.Equal ("com_example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); + Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); } [Fact] @@ -331,7 +328,7 @@ public void Build_EntryPointsToProxy_WhenProxyExists () var model = BuildModel (new [] { peer }, "MyTypeMap"); var entry = model.Entries [0]; - Assert.Contains ("java_lang_Object_Proxy", entry.ProxyTypeReference); + Assert.Contains ("Java_Lang_Object_Proxy", entry.ProxyTypeReference); Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); } @@ -776,7 +773,7 @@ public void Fixture_JavaLangObject_HasActivation_CreatesProxy () var peer = FindFixtureByJavaName ("java/lang/Object"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "java_lang_Object_Proxy"); + var proxy = FindProxy (model, "Java_Lang_Object_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.HasActivation); Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); @@ -794,7 +791,7 @@ public void Fixture_Activity_HasActivation_CreatesProxy () var peer = FindFixtureByJavaName ("android/app/Activity"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "android_app_Activity_Proxy"); + var proxy = FindProxy (model, "Android_App_Activity_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.HasActivation); Assert.Equal ("Android.App.Activity", proxy.TargetType.ManagedTypeName); @@ -810,7 +807,7 @@ public void Fixture_Activity_Entry_PointsToProxy () var entry = FindEntry (model, "android/app/Activity"); Assert.NotNull (entry); - Assert.Contains ("android_app_Activity_Proxy", entry!.ProxyTypeReference); + Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference); Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); } @@ -820,7 +817,7 @@ public void Fixture_Throwable_HasActivation () var peer = FindFixtureByJavaName ("java/lang/Throwable"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "java_lang_Throwable_Proxy"); + var proxy = FindProxy (model, "Java_Lang_Throwable_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.HasActivation); Assert.False (proxy.IsAcw); @@ -832,7 +829,7 @@ public void Fixture_Exception_HasActivation () var peer = FindFixtureByJavaName ("java/lang/Exception"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "java_lang_Exception_Proxy"); + var proxy = FindProxy (model, "Java_Lang_Exception_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.HasActivation); } @@ -860,7 +857,7 @@ public void Fixture_Context_HasActivation () // Context has (IntPtr, JniHandleOwnership) ctor if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "android_content_Context_Proxy"); + var proxy = FindProxy (model, "Android_Content_Context_Proxy"); Assert.NotNull (proxy); Assert.False (proxy!.IsAcw); } @@ -873,7 +870,7 @@ public void Fixture_View_HasActivation () var model = BuildModel (new [] { peer }, "TypeMap"); if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "android_view_View_Proxy"); + var proxy = FindProxy (model, "Android_Views_View_Proxy"); Assert.NotNull (proxy); } } @@ -885,7 +882,7 @@ public void Fixture_Button_HasActivation () var model = BuildModel (new [] { peer }, "TypeMap"); if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "android_widget_Button_Proxy"); + var proxy = FindProxy (model, "Android_Widget_Button_Proxy"); Assert.NotNull (proxy); } } @@ -901,7 +898,7 @@ public void Fixture_MainActivity_IsAcw () Assert.NotNull (peer.ActivationCtor); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "my_app_MainActivity_Proxy"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.IsAcw); Assert.True (proxy.ImplementsIAndroidCallableWrapper); @@ -913,7 +910,7 @@ public void Fixture_MainActivity_UcoMethods () { var peer = FindFixtureByJavaName ("my/app/MainActivity"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "my_app_MainActivity_Proxy")!; + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; // Should have UCO wrappers for non-constructor marshal methods var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); @@ -931,7 +928,7 @@ public void Fixture_MainActivity_NativeRegistrations () { var peer = FindFixtureByJavaName ("my/app/MainActivity"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "my_app_MainActivity_Proxy")!; + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; Assert.NotEmpty (proxy.NativeRegistrations); @@ -952,7 +949,7 @@ public void Fixture_MyHelper_IsAcw () // MyHelper has marshal methods and is not DoNotGenerateAcw // Whether it's ACW depends on: not interface, has marshal methods, not DoNotGenerateAcw if (peer.MarshalMethods.Count > 0 && peer.ActivationCtor != null) { - var proxy = FindProxy (model, "my_app_MyHelper_Proxy"); + var proxy = FindProxy (model, "MyApp_MyHelper_Proxy"); Assert.NotNull (proxy); } } @@ -964,7 +961,7 @@ public void Fixture_TouchHandler_AllUcoMethods () { var peer = FindFixtureByJavaName ("my/app/TouchHandler"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_TouchHandler_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy"); Assert.NotNull (proxy); var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); @@ -1001,7 +998,7 @@ public void Fixture_TouchHandler_NativeRegistrationsMatchUcoMethods () { var peer = FindFixtureByJavaName ("my/app/TouchHandler"); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_TouchHandler_Proxy")!; + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy")!; // Every UCO method should have a matching registration foreach (var uco in proxy.UcoMethods) { @@ -1020,7 +1017,7 @@ public void Fixture_CustomView_HasTwoConstructorWrappers () Assert.Equal (2, peer.JavaConstructors.Count); var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_CustomView_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); Assert.NotNull (proxy); if (proxy!.IsAcw) { @@ -1083,12 +1080,12 @@ public void Fixture_OuterInner_ProxyNaming () var peer = FindFixtureByJavaName ("my/app/Outer$Inner"); var model = BuildModel (new [] { peer }, "TypeMap"); - // $ gets replaced with _ + // . and + get replaced with _ var entry = FindEntry (model, "my/app/Outer$Inner"); Assert.NotNull (entry); if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "my_app_Outer_Inner_Proxy"); + var proxy = FindProxy (model, "MyApp_Outer_Inner_Proxy"); Assert.NotNull (proxy); Assert.Equal ("MyApp.Outer+Inner", proxy!.TargetType.ManagedTypeName); } @@ -1104,7 +1101,7 @@ public void Fixture_ICallbackResult_ProxyNaming () Assert.NotNull (entry); if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "my_app_ICallback_Result_Proxy"); + var proxy = FindProxy (model, "MyApp_ICallback_Result_Proxy"); Assert.NotNull (proxy); Assert.Equal ("MyApp.ICallback+Result", proxy!.TargetType.ManagedTypeName); } @@ -1113,7 +1110,7 @@ public void Fixture_ICallbackResult_ProxyNaming () // ---- Duplicate JNI names across interface + invoker ---- [Fact] - public void Fixture_InterfaceAndInvoker_ShareJniName_CreateAliases () + public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () { var peers = ScanFixtures (); // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" @@ -1122,10 +1119,13 @@ public void Fixture_InterfaceAndInvoker_ShareJniName_CreateAliases () var model = BuildModel (clickPeers, "TypeMap"); - // Aliases: primary entry + indexed alias - Assert.Equal (2, model.Entries.Count); + // Invoker is separated from non-invokers before alias grouping, + // so only the interface gets a TypeMap entry + Assert.Single (model.Entries); Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); - Assert.Equal ("android/view/View$OnClickListener[1]", model.Entries [1].JniName); + + // Both the interface and the invoker should get proxy types + Assert.Equal (2, model.ProxyTypes.Count); } // ---- GenericHolder ---- @@ -1154,7 +1154,7 @@ public void Fixture_AbstractBase_IsAcw () // AbstractBase has marshal methods (doWork) and activation ctor if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_AbstractBase_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_AbstractBase_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.IsAcw); } @@ -1171,7 +1171,7 @@ public void Fixture_ClickableView_IsAcw () var model = BuildModel (new [] { peer }, "TypeMap"); if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_ClickableView_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.IsAcw); // Should have onClick UCO wrapper @@ -1192,7 +1192,7 @@ public void Fixture_MultiInterfaceView_HasAllUcoMethods () var model = BuildModel (new [] { peer }, "TypeMap"); if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_MultiInterfaceView_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); Assert.NotNull (proxy); // Should have onClick and onLongClick UCO wrappers @@ -1213,11 +1213,47 @@ public void Fixture_ExportExample_IsAcw () var model = BuildModel (new [] { peer }, "TypeMap"); if (peer.ActivationCtor != null) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "my_app_ExportExample_Proxy"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); Assert.NotNull (proxy); } } + // ---- Implementor types ---- + + [Fact] + public void Fixture_Implementor_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_IOnClickListenerImplementor"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // Implementor types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "Implementor should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } + + // ---- EventDispatcher types ---- + + [Fact] + public void Fixture_EventDispatcher_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_ClickEventDispatcher"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // EventDispatcher types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "EventDispatcher should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } + // ---- Full pipeline: scan → model → emit → read back ---- [Fact] @@ -1300,7 +1336,7 @@ public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "my_app_TouchHandler_Proxy"); + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); var methods = proxy.GetMethods () .Select (h => reader.GetMethodDefinition (h)) @@ -1337,7 +1373,7 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "my_app_CustomView_Proxy"); + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); var methodNames = proxy.GetMethods () .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) @@ -1380,7 +1416,7 @@ public void FullPipeline_EssentialType_Emits2ArgAttribute () Assert.Equal ("java/lang/Object", jniName); Assert.NotNull (proxyRef); - Assert.Contains ("java_lang_Object_Proxy", proxyRef!); + Assert.Contains ("Java_Lang_Object_Proxy", proxyRef!); // 2-arg: no target type Assert.Null (targetRef); } finally { @@ -1408,7 +1444,7 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () Assert.Equal ("android/app/Activity", jniName); Assert.NotNull (proxyRef); - Assert.Contains ("android_app_Activity_Proxy", proxyRef!); + Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); // 3-arg: has target type Assert.NotNull (targetRef); Assert.Contains ("Android.App.Activity", targetRef!); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 285aff424fe..a8ef855a0d8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -448,6 +448,47 @@ public interface IOnLongClickListener [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] bool OnLongClick (View v); } + + // Implementor types are generated by the binding generator. + // They are NOT DoNotGenerateAcw because they ARE ACW types, but they should still + // be trimmable because they are only instantiated from .NET (e.g., when subscribing to an event). + [Register ("android/view/View_IOnClickListenerImplementor")] + public class IOnClickListenerImplementor : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerImplementor () + { + } + + protected IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (View v) + { + } + } + + // EventDispatcher types are used for the event-based pattern in Android bindings. + // Like Implementor types, they should be trimmable. + [Register ("android/view/View_ClickEventDispatcher")] + public class ClickEventDispatcher : Java.Lang.Object + { + public ClickEventDispatcher () + { + } + + protected ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("dispatch", "()V", "")] + public void Dispatch () + { + } + } } namespace MyApp From 33d4deaa35f27cec35596156ed0548af62819ae2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Feb 2026 23:00:30 +0100 Subject: [PATCH 17/43] Fix review round 4: clear typeRefCache, extract constants, document name heuristics, add tests - Clear _typeRefCache in Emit() alongside _asmRefCache to prevent stale handles on reuse - Extract MonoAndroidPublicKeyToken and SystemRuntimeVersion as named constants - Document name-based IsInvokerType/IsImplementorOrEventDispatcher heuristic limitations - Fix stale doc comment on JavaPeerProxyData.TypeName - Add FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip test - Add FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams PE-level test - Add FullPipeline_GenericHolder_ProducesValidAssembly PE pipeline test - Add edge case tests documenting name-based detection limitations - Refactor ReadFirstTypeMapAttributeBlob into ReadAllTypeMapAttributeBlobs - All 230 tests pass --- .../Generator/Model/TypeMapAssemblyData.cs | 2 +- .../Generator/ModelBuilder.cs | 5 + .../Generator/TypeMapAssemblyEmitter.cs | 13 +- .../Generator/TypeMapModelBuilderTests.cs | 152 +++++++++++++++++- 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs index c1a44569e4a..214264edfaf 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -60,7 +60,7 @@ sealed class TypeMapAttributeData /// sealed class JavaPeerProxyData { - /// Simple type name, e.g., "java_lang_Object_Proxy". + /// Simple type name, e.g., "Java_Lang_Object_Proxy". public string TypeName { get; set; } = ""; /// Namespace for all proxy types. diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index aa47256af42..fdfd6013eeb 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -115,6 +115,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri /// /// Invoker types (e.g., IOnClickListenerInvoker) wrap existing Java interfaces. /// They get proxies for CreateInstance but no TypeMap entries — the interface entry covers them. + /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. /// static bool IsInvokerType (JavaPeerInfo peer) { @@ -191,6 +192,10 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) /// /// Implementor and EventDispatcher types are generated by the binding generator /// and are only instantiated from .NET. They should be trimmable. + /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. + /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be + /// misclassified as trimmable. This is acceptable for now since such naming in user code + /// is unlikely and would only affect trimming behavior, not correctness. /// static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) { diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index b9780c29a19..ee27847b24d 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -53,6 +53,7 @@ public void Emit (TypeMapAssemblyData model, string outputPath) } _asmRefCache.Clear (); + _typeRefCache.Clear (); var dir = Path.GetDirectoryName (outputPath); if (!string.IsNullOrEmpty (dir)) { @@ -103,13 +104,19 @@ void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyData model) encBaseId: default); } + // Mono.Android strong name public key token (84e04ff9cfb79065) + static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; + + // TODO: Make these configurable per target framework instead of hardcoding .NET 11 + static readonly Version SystemRuntimeVersion = new Version (11, 0, 0, 0); + void EmitAssemblyReferences (MetadataBuilder metadata) { - _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", new Version (11, 0, 0, 0)); + _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", SystemRuntimeVersion); _monoAndroidRef = AddAssemblyRef (metadata, "Mono.Android", new Version (0, 0, 0, 0), - publicKeyOrToken: new byte [] { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }); + publicKeyOrToken: MonoAndroidPublicKeyToken); _javaInteropRef = AddAssemblyRef (metadata, "Java.Interop", new Version (0, 0, 0, 0)); - _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", new Version (11, 0, 0, 0)); + _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", SystemRuntimeVersion); } void EmitTypeReferences (MetadataBuilder metadata) diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index de42e8182cb..3935f201fbb 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1254,6 +1254,38 @@ public void Fixture_EventDispatcher_IsTrimmable_NotUnconditional () Assert.NotNull (entry.TargetTypeReference); } + // ---- Name-based detection edge cases ---- + + [Fact] + public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + { + // Limitation: name-based heuristic means a user type ending in "Implementor" + // will be treated as trimmable even if it's genuinely a user ACW type. + // This test documents the known behavior. + var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); + var model = BuildModel (new [] { peer }); + + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + // The heuristic treats this as an Implementor → trimmable (not unconditional) + Assert.False (entry!.IsUnconditional, + "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); + } + + [Fact] + public void Build_UserTypeNamedInvoker_WithDoNotGenerateAcw_IsTreatedAsInvoker () + { + // Limitation: name-based heuristic means a MCW type ending in "Invoker" + // will be treated as an invoker (no TypeMap entry, only proxy). + var peer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + + // The heuristic treats this as an invoker → proxy but no entry + Assert.Empty (model.Entries); + Assert.Single (model.ProxyTypes); + } + // ---- Full pipeline: scan → model → emit → read back ---- [Fact] @@ -1394,8 +1426,109 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () } } + [Fact] + public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorSigTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + // Find UCO constructor wrappers (nctor_*_uco) + var ucoCtors = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco")) + .ToList (); + + Assert.NotEmpty (ucoCtors); + foreach (var uco in ucoCtors) { + // UCO constructor wrappers always take exactly 2 params (IntPtr jnienv, IntPtr self) + var sig = reader.GetBlobReader (uco.Signature); + var header = sig.ReadSignatureHeader (); + int paramCount = sig.ReadCompressedInteger (); + Assert.Equal (2, paramCount); + } + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_GenericHolder_ProducesValidAssembly () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + var model = BuildModel (new [] { peer }, "GenericTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + // Verify the assembly is loadable and has entries + Assert.True (pe.HasMetadata); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + + // Verify assembly attributes were emitted + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (asmAttrs); + } finally { + CleanUpDir (outputPath); + } + } + // ---- PE blob validation: 2-arg vs 3-arg TypeMap attributes ---- + [Fact] + public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () + { + // java/lang/Object → essential → 2-arg unconditional + var objectPeer = FindFixtureByJavaName ("java/lang/Object"); + // android/app/Activity → MCW → 3-arg trimmable + var activityPeer = FindFixtureByJavaName ("android/app/Activity"); + + var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); + Assert.Equal (2, model.Entries.Count); + + var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var attrs = ReadAllTypeMapAttributeBlobs (reader); + Assert.Equal (2, attrs.Count); + + // Find the 2-arg (unconditional) entry + var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); + Assert.NotNull (unconditional.jniName); + Assert.Null (unconditional.targetRef); // 2-arg: no target + + // Find the 3-arg (trimmable) entry + var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); + Assert.NotNull (trimmable.jniName); + Assert.NotNull (trimmable.targetRef); // 3-arg: has target + Assert.Contains ("Android.App.Activity", trimmable.targetRef!); + } finally { + CleanUpDir (outputPath); + } + } + [Fact] public void FullPipeline_EssentialType_Emits2ArgAttribute () { @@ -1504,15 +1637,25 @@ public void Build_SameInput_ProducesDeterministicOutput () /// static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) { + var all = ReadAllTypeMapAttributeBlobs (reader); + if (all.Count == 0) { + throw new InvalidOperationException ("No TypeMap attribute found on assembly"); + } + return all [0]; + } + + static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) + { + var result = new List<(string?, string?, string?)> (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); foreach (var attrHandle in asmAttrs) { var attr = reader.GetCustomAttribute (attrHandle); - // Skip IgnoresAccessChecksTo attributes + // Skip IgnoresAccessChecksTo attributes (their ctor is a MethodDefinition, not MemberRef) if (attr.Constructor.Kind == HandleKind.MethodDefinition) continue; var blobReader = reader.GetBlobReader (attr.Value); - ushort prolog = blobReader.ReadUInt16 (); // 0x0001 + ushort prolog = blobReader.ReadUInt16 (); if (prolog != 1) continue; @@ -1525,10 +1668,9 @@ public void Build_SameInput_ProducesDeterministicOutput () targetRef = blobReader.ReadSerializedString (); } - return (jniName, proxyRef, targetRef); + result.Add ((jniName, proxyRef, targetRef)); } - - throw new InvalidOperationException ("No TypeMap attribute found on assembly"); + return result; } static void CleanUpDir (string path) From f1ba60d1b95ebe377751949162135460975b244e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 08:56:51 +0100 Subject: [PATCH 18/43] Fix invoker handling, add dotnet version param, SuperArgumentsString support - Invoker types no longer get their own proxy types or TypeMap entries. They are only referenced as a TypeRef in the interface proxy's get_InvokerType property and CreateInstance method. - Invoker detection now uses explicit relationship from [Register] third argument (InvokerTypeName) instead of name-based heuristic. - Interface proxy CreateInstance creates the invoker type, not the interface itself. - Added dotnetVersion parameter to TypeMapAssemblyEmitter, TypeMapAssemblyGenerator, and RootTypeMapAssemblyGenerator constructors (will be passed from $(DotNetTargetVersion) MSBuild property later). - JCW generator now uses SuperArgumentsString for [Export] constructor super() calls instead of always forwarding all parameters. - Documented PE-reading test helpers with clear comments explaining the approach and its limitations. --- .../Generator/JcwJavaSourceGenerator.cs | 9 +- .../Generator/ModelBuilder.cs | 36 ++---- .../Generator/RootTypeMapAssemblyGenerator.cs | 10 +- .../Generator/TypeMapAssemblyEmitter.cs | 29 +++-- .../Generator/TypeMapAssemblyGenerator.cs | 10 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 85 +++++++++++++ .../RootTypeMapAssemblyGeneratorTests.cs | 2 +- .../TypeMapAssemblyGeneratorTests.cs | 6 +- .../Generator/TypeMapModelBuilderTests.cs | 114 ++++++++++++++---- 9 files changed, 237 insertions(+), 64 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs index df2a57f1963..b80dea6c1c6 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs @@ -129,9 +129,14 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) writer.WriteLine (')'); writer.WriteLine ("\t{"); - // super() call with parameters + // super() call — use SuperArgumentsString if provided ([Export] constructors), + // otherwise forward all constructor parameters. writer.Write ("\t\tsuper ("); - WriteArgumentList (ctor.Parameters, writer); + if (ctor.SuperArgumentsString != null) { + writer.Write (ctor.SuperArgumentsString); + } else { + WriteArgumentList (ctor.Parameters, writer); + } writer.WriteLine (");"); // Activation guard: only activate if this is the exact class diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs index fdfd6013eeb..fa82092d170 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs @@ -47,23 +47,21 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri ModuleName = moduleName, }; - // Separate invoker types — they get proxies (for CreateInstance) but NOT TypeMap entries. - // Invokers share JNI names with their interfaces and are created via the parent proxy. - var invokerPeers = new List (); - var nonInvokerPeers = new List (); + // Build a set of invoker type names referenced by interfaces/abstract types via [Register]'s + // third argument. Invoker types are NOT emitted as separate proxies or TypeMap entries — + // they only appear as a TypeRef in the interface proxy's get_InvokerType property. + var invokerTypeNames = new HashSet (StringComparer.Ordinal); foreach (var peer in peers) { - if (IsInvokerType (peer)) { - invokerPeers.Add (peer); - } else { - nonInvokerPeers.Add (peer); + if (peer.InvokerTypeName != null) { + invokerTypeNames.Add (peer.InvokerTypeName); } } - // Generate proxies for invoker types (no TypeMap entries) - foreach (var invoker in invokerPeers) { - if (invoker.ActivationCtor != null) { - var proxy = BuildProxyType (invoker, isAcw: false); - model.ProxyTypes.Add (proxy); + // Exclude invoker types from further processing — they don't get TypeMap entries or proxies. + var nonInvokerPeers = new List (); + foreach (var peer in peers) { + if (!invokerTypeNames.Contains (peer.ManagedTypeName)) { + nonInvokerPeers.Add (peer); } } @@ -112,16 +110,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri return model; } - /// - /// Invoker types (e.g., IOnClickListenerInvoker) wrap existing Java interfaces. - /// They get proxies for CreateInstance but no TypeMap entries — the interface entry covers them. - /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. - /// - static bool IsInvokerType (JavaPeerInfo peer) - { - return peer.DoNotGenerateAcw && peer.ManagedTypeName.EndsWith ("Invoker", StringComparison.Ordinal); - } - static void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemblyName) { bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; @@ -215,7 +203,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, - HasActivation = peer.ActivationCtor != null, + HasActivation = peer.ActivationCtor != null || peer.InvokerTypeName != null, IsAcw = isAcw, }; diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs index a664dc4d1a9..71391bc0e01 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -16,6 +16,14 @@ sealed class RootTypeMapAssemblyGenerator { const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps"; + readonly Version _systemRuntimeVersion; + + /// Target .NET version (e.g., 11 for .NET 11). + public RootTypeMapAssemblyGenerator (int dotnetVersion) + { + _systemRuntimeVersion = new Version (dotnetVersion, 0, 0, 0); + } + /// /// Generates the root typemap assembly. /// @@ -62,7 +70,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp // Assembly reference for System.Runtime (needed for Attribute base class) var systemRuntimeRef = metadata.AddAssemblyReference ( metadata.GetOrAddString ("System.Runtime"), - new Version (11, 0, 0, 0), default, default, 0, default); + _systemRuntimeVersion, default, default, 0, default); // type metadata.AddTypeDefinition ( diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index ee27847b24d..0caecd28d5f 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -17,6 +17,8 @@ sealed class TypeMapAssemblyEmitter readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); readonly Dictionary _typeRefCache = new (StringComparer.Ordinal); + readonly Version _systemRuntimeVersion; + AssemblyReferenceHandle _systemRuntimeRef; AssemblyReferenceHandle _monoAndroidRef; AssemblyReferenceHandle _javaInteropRef; @@ -40,6 +42,18 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; + /// + /// Creates a new emitter. + /// + /// + /// Target .NET version (e.g., 11 for .NET 11). Used for System.Runtime assembly reference version. + /// Will be passed from $(DotNetTargetVersion) MSBuild property in the build task. + /// + public TypeMapAssemblyEmitter (int dotnetVersion) + { + _systemRuntimeVersion = new Version (dotnetVersion, 0, 0, 0); + } + /// /// Emits a PE assembly from the given model and writes it to . /// @@ -107,16 +121,13 @@ void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyData model) // Mono.Android strong name public key token (84e04ff9cfb79065) static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; - // TODO: Make these configurable per target framework instead of hardcoding .NET 11 - static readonly Version SystemRuntimeVersion = new Version (11, 0, 0, 0); - void EmitAssemblyReferences (MetadataBuilder metadata) { - _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", SystemRuntimeVersion); + _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", _systemRuntimeVersion); _monoAndroidRef = AddAssemblyRef (metadata, "Mono.Android", new Version (0, 0, 0, 0), publicKeyOrToken: MonoAndroidPublicKeyToken); _javaInteropRef = AddAssemblyRef (metadata, "Java.Interop", new Version (0, 0, 0, 0)); - _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", SystemRuntimeVersion); + _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", _systemRuntimeVersion); } void EmitTypeReferences (MetadataBuilder metadata) @@ -307,7 +318,11 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe return; } - var userTypeRef = ResolveTypeRef (metadata, proxy.TargetType); + // For interface proxies with an invoker type, CreateInstance instantiates the invoker + // (e.g., IOnClickListenerInvoker), not the interface itself. For regular types, it + // instantiates the target type directly. + var activatedType = proxy.InvokerType ?? proxy.TargetType; + var activatedTypeRef = ResolveTypeRef (metadata, activatedType); EmitBody (metadata, ilBuilder, "CreateInstance", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, @@ -321,7 +336,7 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); + encoder.Token (activatedTypeRef); encoder.Call (_getTypeFromHandleRef); encoder.Call (_createManagedPeerRef); encoder.OpCode (ILOpCode.Ret); diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs index de1cf54ecdb..fad46d2717a 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -9,6 +9,14 @@ namespace Microsoft.Android.Build.TypeMap; /// sealed class TypeMapAssemblyGenerator { + readonly int _dotnetVersion; + + /// Target .NET version (e.g., 11 for .NET 11). + public TypeMapAssemblyGenerator (int dotnetVersion) + { + _dotnetVersion = dotnetVersion; + } + /// /// Generates a TypeMap PE assembly from the given Java peer info records. /// @@ -18,7 +26,7 @@ sealed class TypeMapAssemblyGenerator public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) { var model = ModelBuilder.Build (peers, outputPath, assemblyName); - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (_dotnetVersion); emitter.Emit (model, outputPath); } } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 43c8f44ca9d..c73eb54c87b 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -203,6 +203,91 @@ public void Generate_Constructor_HasActivationGuard () Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java); } + [Fact] + public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () + { + // [Export] constructors with SuperArgumentsString should use it in super() call + var type = new JavaPeerInfo { + JavaName = "my/app/CustomService", + ManagedTypeName = "MyApp.CustomService", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "CustomService", + AssemblyName = "App", + BaseJavaName = "android/app/Service", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;I)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "I" }, + }, + SuperArgumentsString = "p0", + }, + }, + }; + + var java = GenerateToString (type); + // super() should use the custom args, not all parameters + Assert.Contains ("super (p0);", java); + Assert.DoesNotContain ("super (p0, p1);", java); + } + + [Fact] + public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () + { + // Empty string means super() with no arguments + var type = new JavaPeerInfo { + JavaName = "my/app/MyWidget", + ManagedTypeName = "MyApp.MyWidget", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyWidget", + AssemblyName = "App", + BaseJavaName = "android/appwidget/AppWidgetProvider", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + }, + SuperArgumentsString = "", + }, + }, + }; + + var java = GenerateToString (type); + Assert.Contains ("super ();", java); + Assert.DoesNotContain ("super (p0);", java); + } + + [Fact] + public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams () + { + // null SuperArgumentsString means forward all params (default behavior) + var type = new JavaPeerInfo { + JavaName = "my/app/MyView", + ManagedTypeName = "MyApp.MyView", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyView", + AssemblyName = "App", + BaseJavaName = "android/view/View", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" }, + }, + }, + }, + }; + + var java = GenerateToString (type); + Assert.Contains ("super (p0, p1);", java); + } + // ---- Method tests ---- [Fact] diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 1bef48d5ace..ccf69136515 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -14,7 +14,7 @@ string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? ass { var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}", (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); - var generator = new RootTypeMapAssemblyGenerator (); + var generator = new RootTypeMapAssemblyGenerator (11); generator.Generate (perAssemblyNames, outputPath, assemblyName); return outputPath; } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 710773acb87..3da606e4677 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -32,7 +32,7 @@ string GenerateAssembly (IReadOnlyList peers, string? assemblyName { var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", (assemblyName ?? "TestTypeMap") + ".dll"); - var generator = new TypeMapAssemblyGenerator (); + var generator = new TypeMapAssemblyGenerator (11); generator.Generate (peers, outputPath, assemblyName); return outputPath; } @@ -456,7 +456,7 @@ public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string si [Fact] public void Generate_NullPeers_ThrowsArgumentNull () { - var gen = new TypeMapAssemblyGenerator (); + var gen = new TypeMapAssemblyGenerator (11); var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll"); Assert.Throws (() => gen.Generate (null!, tmpPath)); } @@ -464,7 +464,7 @@ public void Generate_NullPeers_ThrowsArgumentNull () [Fact] public void Generate_NullOutputPath_ThrowsArgumentNull () { - var gen = new TypeMapAssemblyGenerator (); + var gen = new TypeMapAssemblyGenerator (11); Assert.Throws (() => gen.Generate (Array.Empty (), null!)); } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 3935f201fbb..dbea4eb0de5 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -292,8 +292,10 @@ public void Build_PeerWithInvoker_CreatesProxy () } [Fact] - public void Build_PeerWithInvokerButNoActivation_ProxyHasActivationFalse () + public void Build_PeerWithInvokerButNoActivationCtor_ProxyHasActivationTrue () { + // An interface with an invoker type has HasActivation = true because + // CreateInstance will instantiate the invoker type. var peer = new JavaPeerInfo { JavaName = "android/view/View$OnClickListener", ManagedTypeName = "Android.Views.View+IOnClickListener", @@ -307,7 +309,7 @@ public void Build_PeerWithInvokerButNoActivation_ProxyHasActivationFalse () var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); var proxy = model.ProxyTypes [0]; - Assert.False (proxy.HasActivation); + Assert.True (proxy.HasActivation); Assert.NotNull (proxy.InvokerType); } @@ -1119,13 +1121,48 @@ public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () var model = BuildModel (clickPeers, "TypeMap"); - // Invoker is separated from non-invokers before alias grouping, - // so only the interface gets a TypeMap entry + // Invoker is excluded entirely — no TypeMap entry, no proxy. + // Only the interface gets a TypeMap entry and a proxy. Assert.Single (model.Entries); Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); - // Both the interface and the invoker should get proxy types - Assert.Equal (2, model.ProxyTypes.Count); + // Only the interface proxy exists; the invoker type is referenced + // only as a TypeRef in the interface proxy's InvokerType property. + Assert.Single (model.ProxyTypes); + Assert.NotNull (model.ProxyTypes [0].InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); + } + + [Fact] + public void Build_InvokerType_NoProxyNoEntry () + { + // Invoker types should never get their own proxy or TypeMap entry. + // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/IFoo", + ManagedTypeName = "MyApp.IFoo", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.FooInvoker", + }; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; + + var model = BuildModel (new [] { ifacePeer, invokerPeer }); + + // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy + Assert.Single (model.Entries); + Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference); + + // Only the interface gets a proxy — the invoker is referenced, not proxied + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName); + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName); + + // Interface proxy has activation because it will create the invoker + Assert.True (proxy.HasActivation); } // ---- GenericHolder ---- @@ -1273,17 +1310,29 @@ public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () } [Fact] - public void Build_UserTypeNamedInvoker_WithDoNotGenerateAcw_IsTreatedAsInvoker () + public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () { - // Limitation: name-based heuristic means a MCW type ending in "Invoker" - // will be treated as an invoker (no TypeMap entry, only proxy). - var peer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); - peer.DoNotGenerateAcw = true; - var model = BuildModel (new [] { peer }); + // A type is only treated as an invoker when another peer's InvokerTypeName references it. + // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; - // The heuristic treats this as an invoker → proxy but no entry - Assert.Empty (model.Entries); - Assert.Single (model.ProxyTypes); + // Without a referencing peer, it gets a normal entry + var model1 = BuildModel (new [] { invokerPeer }); + Assert.Single (model1.Entries); + + // When an interface references it as invoker, it is excluded + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/MyInvoker", + ManagedTypeName = "MyApp.IMyInterface", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.MyInvoker", + }; + var model2 = BuildModel (new [] { ifacePeer, invokerPeer }); + // Only the interface gets entries/proxies, the invoker is excluded + Assert.Single (model2.Entries); + Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); } // ---- Full pipeline: scan → model → emit → read back ---- @@ -1296,7 +1345,7 @@ public void FullPipeline_AllFixtures_ProducesLoadableAssembly () var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); Assert.True (File.Exists (outputPath)); @@ -1333,7 +1382,7 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1360,7 +1409,7 @@ public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1397,7 +1446,7 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1434,7 +1483,7 @@ public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1471,7 +1520,7 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1505,7 +1554,7 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1540,7 +1589,7 @@ public void FullPipeline_EssentialType_Emits2ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1568,7 +1617,7 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1597,7 +1646,7 @@ public void FullPipeline_UserAcw_Emits2ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); try { - var emitter = new TypeMapAssemblyEmitter (); + var emitter = new TypeMapAssemblyEmitter (11); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1644,6 +1693,21 @@ public void Build_SameInput_ProducesDeterministicOutput () return all [0]; } + /// + /// Reads TypeMap attribute blobs from a PE assembly's metadata. + /// + /// NOTE: This is a PE-level integration test helper, not a primary unit test mechanism. + /// The model-level tests (which verify TypeMapAssemblyData directly) are the main unit tests. + /// These PE round-trip tests exist to catch encoding bugs in the emitter and to verify that + /// the full scan→model→emit pipeline produces a valid, loadable assembly. + /// + /// The distinction between TypeMap and IgnoresAccessChecksTo attributes relies on + /// attr.Constructor.Kind: TypeMap attributes reference their ctor via MemberReference + /// (because the attribute type is a TypeSpec — generic), while IgnoresAccessChecksTo + /// uses MethodDefinition (the attribute type is defined in the same assembly as a TypeDef). + /// If this logic breaks, the test will either fail to find TypeMap attributes or + /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures. + /// static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) { var result = new List<(string?, string?, string?)> (); From 1a28d4c0788bcab7af35a9d0081e1d1a469a2b75 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 09:13:10 +0100 Subject: [PATCH 19/43] Change dotnetVersion parameter from int to Version --- .../Generator/RootTypeMapAssemblyGenerator.cs | 6 +++--- .../Generator/TypeMapAssemblyEmitter.cs | 10 +++++----- .../Generator/TypeMapAssemblyGenerator.cs | 10 +++++----- .../RootTypeMapAssemblyGeneratorTests.cs | 2 +- .../TypeMapAssemblyGeneratorTests.cs | 6 +++--- .../Generator/TypeMapModelBuilderTests.cs | 20 +++++++++---------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 71391bc0e01..cd73c555d18 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -18,10 +18,10 @@ sealed class RootTypeMapAssemblyGenerator readonly Version _systemRuntimeVersion; - /// Target .NET version (e.g., 11 for .NET 11). - public RootTypeMapAssemblyGenerator (int dotnetVersion) + /// Version for System.Runtime assembly references. + public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) { - _systemRuntimeVersion = new Version (dotnetVersion, 0, 0, 0); + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); } /// diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs index 0caecd28d5f..a2c9cd9589a 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -45,13 +45,13 @@ sealed class TypeMapAssemblyEmitter /// /// Creates a new emitter. /// - /// - /// Target .NET version (e.g., 11 for .NET 11). Used for System.Runtime assembly reference version. - /// Will be passed from $(DotNetTargetVersion) MSBuild property in the build task. + /// + /// Version for System.Runtime assembly references. + /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task. /// - public TypeMapAssemblyEmitter (int dotnetVersion) + public TypeMapAssemblyEmitter (Version systemRuntimeVersion) { - _systemRuntimeVersion = new Version (dotnetVersion, 0, 0, 0); + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); } /// diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs index fad46d2717a..0373cd16ffe 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -9,12 +9,12 @@ namespace Microsoft.Android.Build.TypeMap; /// sealed class TypeMapAssemblyGenerator { - readonly int _dotnetVersion; + readonly Version _systemRuntimeVersion; - /// Target .NET version (e.g., 11 for .NET 11). - public TypeMapAssemblyGenerator (int dotnetVersion) + /// Version for System.Runtime assembly references. + public TypeMapAssemblyGenerator (Version systemRuntimeVersion) { - _dotnetVersion = dotnetVersion; + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); } /// @@ -26,7 +26,7 @@ public TypeMapAssemblyGenerator (int dotnetVersion) public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) { var model = ModelBuilder.Build (peers, outputPath, assemblyName); - var emitter = new TypeMapAssemblyEmitter (_dotnetVersion); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, outputPath); } } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index ccf69136515..02619c50325 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -14,7 +14,7 @@ string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? ass { var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}", (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); - var generator = new RootTypeMapAssemblyGenerator (11); + var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); generator.Generate (perAssemblyNames, outputPath, assemblyName); return outputPath; } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 3da606e4677..7bf47bb34c8 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -32,7 +32,7 @@ string GenerateAssembly (IReadOnlyList peers, string? assemblyName { var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", (assemblyName ?? "TestTypeMap") + ".dll"); - var generator = new TypeMapAssemblyGenerator (11); + var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); generator.Generate (peers, outputPath, assemblyName); return outputPath; } @@ -456,7 +456,7 @@ public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string si [Fact] public void Generate_NullPeers_ThrowsArgumentNull () { - var gen = new TypeMapAssemblyGenerator (11); + var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll"); Assert.Throws (() => gen.Generate (null!, tmpPath)); } @@ -464,7 +464,7 @@ public void Generate_NullPeers_ThrowsArgumentNull () [Fact] public void Generate_NullOutputPath_ThrowsArgumentNull () { - var gen = new TypeMapAssemblyGenerator (11); + var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); Assert.Throws (() => gen.Generate (Array.Empty (), null!)); } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index dbea4eb0de5..cce2f6a6c03 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1345,7 +1345,7 @@ public void FullPipeline_AllFixtures_ProducesLoadableAssembly () var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); Assert.True (File.Exists (outputPath)); @@ -1382,7 +1382,7 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1409,7 +1409,7 @@ public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1446,7 +1446,7 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1483,7 +1483,7 @@ public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1520,7 +1520,7 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1554,7 +1554,7 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1589,7 +1589,7 @@ public void FullPipeline_EssentialType_Emits2ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1617,7 +1617,7 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); @@ -1646,7 +1646,7 @@ public void FullPipeline_UserAcw_Emits2ArgAttribute () var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); try { - var emitter = new TypeMapAssemblyEmitter (11); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); emitter.Emit (model, outputPath); using var pe = new PEReader (File.OpenRead (outputPath)); From 0a6f26748293d3ddf811977a54a7be7fa52f68e7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 09:36:45 +0100 Subject: [PATCH 20/43] Fix RootTypeMapAssemblyGenerator to use generic TypeMapAssemblyTargetAttribute Instead of defining a non-generic TypeMapAssemblyTargetAttribute in the root assembly, reference the existing generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices and close it with Java.Lang.Object as the type argument. This matches the runtime API: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.typemapassemblytargetattribute-1 --- .../Generator/RootTypeMapAssemblyGenerator.cs | 86 +++++++++---------- .../RootTypeMapAssemblyGeneratorTests.cs | 45 ++++++++-- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs index cd73c555d18..f9854ff8192 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -10,12 +10,16 @@ namespace Microsoft.Android.Build.TypeMap; /// /// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references -/// all per-assembly typemap assemblies via [assembly: TypeMapAssemblyTarget("name")]. +/// all per-assembly typemap assemblies via +/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")]. /// sealed class RootTypeMapAssemblyGenerator { const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps"; + // Mono.Android strong name public key token (84e04ff9cfb79065) + static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; + readonly Version _systemRuntimeVersion; /// Version for System.Runtime assembly references. @@ -67,11 +71,20 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp encId: default, encBaseId: default); - // Assembly reference for System.Runtime (needed for Attribute base class) + // Assembly references var systemRuntimeRef = metadata.AddAssemblyReference ( metadata.GetOrAddString ("System.Runtime"), _systemRuntimeVersion, default, default, 0, default); + var systemRuntimeInteropServicesRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("System.Runtime.InteropServices"), + _systemRuntimeVersion, default, default, 0, default); + + var monoAndroidRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("Mono.Android"), + new Version (0, 0, 0, 0), default, + metadata.GetOrAddBlob (MonoAndroidPublicKeyToken), 0, default); + // type metadata.AddTypeDefinition ( default, default, @@ -80,57 +93,38 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); - // TypeMapAssemblyTargetAttribute type definition + [assembly: ...] applications - var attributeTypeRef = metadata.AddTypeReference (systemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); - - var baseAttrCtorRef = AddMemberRef (metadata, attributeTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - - // Define TypeMapAssemblyTargetAttribute with (string assemblyName) ctor - int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; - int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; - - var ctorSigBlob = new BlobBuilder (); - new BlobEncoder (ctorSigBlob).MethodSignature (isInstanceMethod: true) - .Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().String ()); - - var ctorCodeBuilder = new BlobBuilder (); - var ctorEncoder = new InstructionEncoder (ctorCodeBuilder); - ctorEncoder.OpCode (ILOpCode.Ldarg_0); - ctorEncoder.Call (baseAttrCtorRef); - ctorEncoder.OpCode (ILOpCode.Ret); - - while (ilBuilder.Count % 4 != 0) { - ilBuilder.WriteByte (0); - } - var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); - int ctorBodyOffset = bodyEncoder.AddMethodBody (ctorEncoder); - - var ctorDef = metadata.AddMethodDefinition ( - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, - MethodImplAttributes.IL, - metadata.GetOrAddString (".ctor"), - metadata.GetOrAddBlob (ctorSigBlob), - ctorBodyOffset, default); - - metadata.AddTypeDefinition ( - TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices + var openAttrRef = metadata.AddTypeReference (systemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), - metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute"), - attributeTypeRef, - MetadataTokens.FieldDefinitionHandle (typeFieldStart), - MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1")); + + // Reference Java.Lang.Object from Mono.Android (the type universe) + var javaLangObjectRef = metadata.AddTypeReference (monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + + // Build TypeSpec for TypeMapAssemblyTargetAttribute + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openAttrRef)); + genericInstBlob.WriteCompressedInteger (1); // generic arity = 1 + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + + // MemberRef for .ctor(string) on the closed generic type + var ctorRef = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); - // Add [assembly: TypeMapAssemblyTarget("name")] for each per-assembly typemap + // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap foreach (var name in perAssemblyTypeMapNames) { var attrBlob = new BlobBuilder (); attrBlob.WriteUInt16 (1); // Prolog attrBlob.WriteSerializedString (name); attrBlob.WriteUInt16 (0); // NumNamed - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (attrBlob)); } diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 02619c50325..bd376c46114 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -68,19 +68,54 @@ public void Generate_CustomAssemblyName () } [Fact] - public void Generate_HasTypeMapAssemblyTargetAttributeType () + public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () { var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); try { using var pe = new PEReader (File.OpenRead (path)); var reader = pe.GetMetadataReader (); - var types = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) + // The attribute type is referenced (not defined) — look for TypeRef + var typeRefs = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) .ToList (); - Assert.Contains (types, t => - reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute" && + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" && reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); + + // Java.Lang.Object must also be referenced (generic type argument) + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "Object" && + reader.GetString (t.Namespace) == "Java.Lang"); + + // No TypeDefinition for the attribute (it's external) + var typeDefs = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.DoesNotContain (typeDefs, t => + reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_AttributeCtorIsOnGenericTypeSpec () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + + var attr = reader.GetCustomAttribute ( + reader.GetCustomAttributes (EntityHandle.AssemblyDefinition).First ()); + + // The ctor should be a MemberReference (on a TypeSpec), not a MethodDefinition + Assert.Equal (HandleKind.MemberReference, attr.Constructor.Kind); + + var memberRef = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor); + // Parent should be a TypeSpec (closed generic) + Assert.Equal (HandleKind.TypeSpecification, memberRef.Parent.Kind); } finally { CleanUp (path); } From 339549c1b6e136710d7540b863eeb026443a5f27 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 09:43:28 +0100 Subject: [PATCH 21/43] Refactor test sections into nested classes for better isolation Each '// ---- Section ----' comment block is now a nested class inside the outer test class. This allows running individual test groups in isolation and makes test failures easier to locate. Shared helpers remain as static methods on the outer class. --- .../Generator/JcwJavaSourceGeneratorTests.cs | 626 ++-- .../TypeMapAssemblyGeneratorTests.cs | 736 ++--- .../Generator/TypeMapModelBuilderTests.cs | 2661 +++++++++-------- 3 files changed, 2085 insertions(+), 1938 deletions(-) diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index c73eb54c87b..2d7bf0e375c 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -25,14 +25,14 @@ static string TestFixtureAssemblyPath { static List ScanFixtures () => _cachedFixtures.Value; - JavaPeerInfo FindByJavaName (List peers, string javaName) + static JavaPeerInfo FindByJavaName (List peers, string javaName) { var peer = peers.FirstOrDefault (p => p.JavaName == javaName); Assert.NotNull (peer); return peer; } - string GenerateToString (JavaPeerInfo type) + static string GenerateToString (JavaPeerInfo type) { var generator = new JcwJavaSourceGenerator (); using var writer = new StringWriter (); @@ -40,360 +40,390 @@ string GenerateToString (JavaPeerInfo type) return writer.ToString (); } - // ---- JNI name conversion tests ---- - [Theory] - [InlineData ("android/app/Activity", "android.app.Activity")] - [InlineData ("java/lang/Object", "java.lang.Object")] - [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")] - public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected) + public class JniNameConversion { - Assert.Equal (expected, JcwJavaSourceGenerator.JniNameToJavaName (jniName)); - } - [Theory] - [InlineData ("com/example/MainActivity", "com.example")] - [InlineData ("java/lang/Object", "java.lang")] - [InlineData ("TopLevelClass", null)] - public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected) - { - Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaPackageName (jniName)); - } - - [Theory] - [InlineData ("com/example/MainActivity", "MainActivity")] - [InlineData ("com/example/Outer$Inner", "Outer$Inner")] - [InlineData ("TopLevelClass", "TopLevelClass")] - public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected) - { - Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (jniName)); - } + [Theory] + [InlineData ("android/app/Activity", "android.app.Activity")] + [InlineData ("java/lang/Object", "java.lang.Object")] + [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")] + public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.JniNameToJavaName (jniName)); + } - [Theory] - [InlineData ("V", "void")] - [InlineData ("Z", "boolean")] - [InlineData ("B", "byte")] - [InlineData ("I", "int")] - [InlineData ("J", "long")] - [InlineData ("F", "float")] - [InlineData ("D", "double")] - [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")] - [InlineData ("[I", "int[]")] - [InlineData ("[Ljava/lang/String;", "java.lang.String[]")] - public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) - { - Assert.Equal (expected, JcwJavaSourceGenerator.JniTypeToJava (jniType)); - } + [Theory] + [InlineData ("com/example/MainActivity", "com.example")] + [InlineData ("java/lang/Object", "java.lang")] + [InlineData ("TopLevelClass", null)] + public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaPackageName (jniName)); + } - // ---- Filtering tests ---- + [Theory] + [InlineData ("com/example/MainActivity", "MainActivity")] + [InlineData ("com/example/Outer$Inner", "Outer$Inner")] + [InlineData ("TopLevelClass", "TopLevelClass")] + public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (jniName)); + } - [Fact] - public void Generate_SkipsMcwTypes () - { - var peers = ScanFixtures (); - var generator = new JcwJavaSourceGenerator (); - var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); - try { - var files = generator.Generate (peers, outputDir); - // MCW types like java/lang/Object, android/app/Activity should NOT be generated - Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java")); - Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java")); - // User ACW types should be generated - Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java")); - } finally { - if (Directory.Exists (outputDir)) { - Directory.Delete (outputDir, true); - } + [Theory] + [InlineData ("V", "void")] + [InlineData ("Z", "boolean")] + [InlineData ("B", "byte")] + [InlineData ("I", "int")] + [InlineData ("J", "long")] + [InlineData ("F", "float")] + [InlineData ("D", "double")] + [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")] + [InlineData ("[I", "int[]")] + [InlineData ("[Ljava/lang/String;", "java.lang.String[]")] + public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.JniTypeToJava (jniType)); } - } - // ---- Package declaration tests ---- + } - [Fact] - public void Generate_MainActivity_HasPackageDeclaration () + public class Filtering { - var peers = ScanFixtures (); - var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); - var java = GenerateToString (mainActivity); - Assert.StartsWith ("package my.app;\n", java); - } - // ---- Class declaration tests ---- + [Fact] + public void Generate_SkipsMcwTypes () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + var files = generator.Generate (peers, outputDir); + // MCW types like java/lang/Object, android/app/Activity should NOT be generated + Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java")); + Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java")); + // User ACW types should be generated + Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java")); + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } + } + } - [Fact] - public void Generate_MainActivity_HasClassDeclaration () - { - var peers = ScanFixtures (); - var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); - var java = GenerateToString (mainActivity); - Assert.Contains ("public class MainActivity\n", java); - Assert.Contains ("\textends android.app.Activity\n", java); - Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java); } - [Fact] - public void Generate_AbstractType_HasAbstractModifier () + public class PackageDeclaration { - var peers = ScanFixtures (); - var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); - var java = GenerateToString (abstractBase); - Assert.Contains ("public abstract class AbstractBase\n", java); + + [Fact] + public void Generate_MainActivity_HasPackageDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.StartsWith ("package my.app;\n", java); + } + } - [Fact] - public void Generate_TypeWithInterfaces_HasImplementsClause () + public class ClassDeclaration { - var peers = ScanFixtures (); - var multiView = FindByJavaName (peers, "my/app/MultiInterfaceView"); - var java = GenerateToString (multiView); - Assert.Contains ("\timplements\n", java); - Assert.Contains ("\t\tmono.android.IGCUserPeer", java); - Assert.Contains ("android.view.View$OnClickListener", java); - Assert.Contains ("android.view.View$OnLongClickListener", java); - } - // ---- Static initializer tests ---- + [Fact] + public void Generate_MainActivity_HasClassDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("public class MainActivity\n", java); + Assert.Contains ("\textends android.app.Activity\n", java); + Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java); + } - [Fact] - public void Generate_AcwType_HasRegisterNativesStaticBlock () - { - var peers = ScanFixtures (); - var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); - var java = GenerateToString (mainActivity); - Assert.Contains ("static {\n", java); - Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); - } + [Fact] + public void Generate_AbstractType_HasAbstractModifier () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + var java = GenerateToString (abstractBase); + Assert.Contains ("public abstract class AbstractBase\n", java); + } - // ---- Constructor tests ---- + [Fact] + public void Generate_TypeWithInterfaces_HasImplementsClause () + { + var peers = ScanFixtures (); + var multiView = FindByJavaName (peers, "my/app/MultiInterfaceView"); + var java = GenerateToString (multiView); + Assert.Contains ("\timplements\n", java); + Assert.Contains ("\t\tmono.android.IGCUserPeer", java); + Assert.Contains ("android.view.View$OnClickListener", java); + Assert.Contains ("android.view.View$OnLongClickListener", java); + } - [Fact] - public void Generate_CustomView_HasConstructors () - { - var peers = ScanFixtures (); - var customView = FindByJavaName (peers, "my/app/CustomView"); - var java = GenerateToString (customView); - - // Default constructor - Assert.Contains ("public CustomView ()\n", java); - Assert.Contains ("super ();\n", java); - Assert.Contains ("nctor_0 ();\n", java); - - // Context constructor - Assert.Contains ("public CustomView (android.content.Context p0)\n", java); - Assert.Contains ("super (p0);\n", java); - Assert.Contains ("nctor_1 (p0);\n", java); } - [Fact] - public void Generate_CustomView_HasNativeConstructorDeclarations () + public class StaticInitializer { - var peers = ScanFixtures (); - var customView = FindByJavaName (peers, "my/app/CustomView"); - var java = GenerateToString (customView); - Assert.Contains ("private native void nctor_0 ();\n", java); - Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java); - } - [Fact] - public void Generate_Constructor_HasActivationGuard () - { - var peers = ScanFixtures (); - var customView = FindByJavaName (peers, "my/app/CustomView"); - var java = GenerateToString (customView); - Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java); + [Fact] + public void Generate_AcwType_HasRegisterNativesStaticBlock () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("static {\n", java); + Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); + } + } - [Fact] - public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () + public class Constructor { - // [Export] constructors with SuperArgumentsString should use it in super() call - var type = new JavaPeerInfo { - JavaName = "my/app/CustomService", - ManagedTypeName = "MyApp.CustomService", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "CustomService", - AssemblyName = "App", - BaseJavaName = "android/app/Service", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;I)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - new JniParameterInfo { JniType = "I" }, + + [Fact] + public void Generate_CustomView_HasConstructors () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + + // Default constructor + Assert.Contains ("public CustomView ()\n", java); + Assert.Contains ("super ();\n", java); + Assert.Contains ("nctor_0 ();\n", java); + + // Context constructor + Assert.Contains ("public CustomView (android.content.Context p0)\n", java); + Assert.Contains ("super (p0);\n", java); + Assert.Contains ("nctor_1 (p0);\n", java); + } + + [Fact] + public void Generate_CustomView_HasNativeConstructorDeclarations () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + Assert.Contains ("private native void nctor_0 ();\n", java); + Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java); + } + + [Fact] + public void Generate_Constructor_HasActivationGuard () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var java = GenerateToString (customView); + Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java); + } + + [Fact] + public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () + { + // [Export] constructors with SuperArgumentsString should use it in super() call + var type = new JavaPeerInfo { + JavaName = "my/app/CustomService", + ManagedTypeName = "MyApp.CustomService", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "CustomService", + AssemblyName = "App", + BaseJavaName = "android/app/Service", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;I)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "I" }, + }, + SuperArgumentsString = "p0", }, - SuperArgumentsString = "p0", }, - }, - }; + }; - var java = GenerateToString (type); - // super() should use the custom args, not all parameters - Assert.Contains ("super (p0);", java); - Assert.DoesNotContain ("super (p0, p1);", java); - } + var java = GenerateToString (type); + // super() should use the custom args, not all parameters + Assert.Contains ("super (p0);", java); + Assert.DoesNotContain ("super (p0, p1);", java); + } - [Fact] - public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () - { - // Empty string means super() with no arguments - var type = new JavaPeerInfo { - JavaName = "my/app/MyWidget", - ManagedTypeName = "MyApp.MyWidget", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "MyWidget", - AssemblyName = "App", - BaseJavaName = "android/appwidget/AppWidgetProvider", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, + [Fact] + public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () + { + // Empty string means super() with no arguments + var type = new JavaPeerInfo { + JavaName = "my/app/MyWidget", + ManagedTypeName = "MyApp.MyWidget", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyWidget", + AssemblyName = "App", + BaseJavaName = "android/appwidget/AppWidgetProvider", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + }, + SuperArgumentsString = "", }, - SuperArgumentsString = "", }, - }, - }; + }; - var java = GenerateToString (type); - Assert.Contains ("super ();", java); - Assert.DoesNotContain ("super (p0);", java); - } + var java = GenerateToString (type); + Assert.Contains ("super ();", java); + Assert.DoesNotContain ("super (p0);", java); + } - [Fact] - public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams () - { - // null SuperArgumentsString means forward all params (default behavior) - var type = new JavaPeerInfo { - JavaName = "my/app/MyView", - ManagedTypeName = "MyApp.MyView", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "MyView", - AssemblyName = "App", - BaseJavaName = "android/view/View", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" }, + [Fact] + public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams () + { + // null SuperArgumentsString means forward all params (default behavior) + var type = new JavaPeerInfo { + JavaName = "my/app/MyView", + ManagedTypeName = "MyApp.MyView", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyView", + AssemblyName = "App", + BaseJavaName = "android/view/View", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" }, + }, }, }, - }, - }; - - var java = GenerateToString (type); - Assert.Contains ("super (p0, p1);", java); - } + }; - // ---- Method tests ---- + var java = GenerateToString (type); + Assert.Contains ("super (p0, p1);", java); + } - [Fact] - public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () - { - var peers = ScanFixtures (); - var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); - var java = GenerateToString (mainActivity); - Assert.Contains ("@Override\n", java); - Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); - Assert.Contains ("n_OnCreate (p0);\n", java); - Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); } - [Fact] - public void Generate_MethodWithReturnValue_HasReturnStatement () + public class Method { - var peers = ScanFixtures (); - var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); - var java = GenerateToString (touchHandler); - Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java); - Assert.Contains ("return n_OnTouch (p0, p1);\n", java); - } - [Fact] - public void Generate_MethodWithMultipleParams_HasAllParameters () - { - var peers = ScanFixtures (); - var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); - var java = GenerateToString (touchHandler); - Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java); - } + [Fact] + public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + var java = GenerateToString (mainActivity); + Assert.Contains ("@Override\n", java); + Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); + Assert.Contains ("n_OnCreate (p0);\n", java); + Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + } + + [Fact] + public void Generate_MethodWithReturnValue_HasReturnStatement () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java); + Assert.Contains ("return n_OnTouch (p0, p1);\n", java); + } + + [Fact] + public void Generate_MethodWithMultipleParams_HasAllParameters () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java); + } + + [Fact] + public void Generate_MethodWithObjectReturnType_HasCorrectType () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public java.lang.String getText ()\n", java); + Assert.Contains ("return n_GetText ();\n", java); + } + + [Fact] + public void Generate_MethodWithArrayParam_HasCorrectType () + { + var peers = ScanFixtures (); + var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); + var java = GenerateToString (touchHandler); + Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java); + } - [Fact] - public void Generate_MethodWithObjectReturnType_HasCorrectType () - { - var peers = ScanFixtures (); - var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); - var java = GenerateToString (touchHandler); - Assert.Contains ("public java.lang.String getText ()\n", java); - Assert.Contains ("return n_GetText ();\n", java); } - [Fact] - public void Generate_MethodWithArrayParam_HasCorrectType () + public class NestedType { - var peers = ScanFixtures (); - var touchHandler = FindByJavaName (peers, "my/app/TouchHandler"); - var java = GenerateToString (touchHandler); - Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java); - } - // ---- Nested type tests ---- + [Fact] + public void Generate_NestedType_HasCorrectPackageAndClassName () + { + var peers = ScanFixtures (); + var inner = FindByJavaName (peers, "my/app/Outer$Inner"); + var java = GenerateToString (inner); + Assert.Contains ("package my.app;\n", java); + Assert.Contains ("public class Outer$Inner\n", java); + } - [Fact] - public void Generate_NestedType_HasCorrectPackageAndClassName () - { - var peers = ScanFixtures (); - var inner = FindByJavaName (peers, "my/app/Outer$Inner"); - var java = GenerateToString (inner); - Assert.Contains ("package my.app;\n", java); - Assert.Contains ("public class Outer$Inner\n", java); } - // ---- Output file path tests ---- - - [Fact] - public void Generate_CreatesCorrectFileStructure () + public class OutputFilePath { - var peers = ScanFixtures (); - var generator = new JcwJavaSourceGenerator (); - var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); - try { - var files = generator.Generate (peers, outputDir); - Assert.NotEmpty (files); - - // All files should be under the output directory - foreach (var file in files) { - Assert.StartsWith (outputDir, file); - Assert.True (File.Exists (file), $"Generated file should exist: {file}"); - Assert.EndsWith (".java", file); - } - } finally { - if (Directory.Exists (outputDir)) { - Directory.Delete (outputDir, true); + + [Fact] + public void Generate_CreatesCorrectFileStructure () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + var files = generator.Generate (peers, outputDir); + Assert.NotEmpty (files); + + // All files should be under the output directory + foreach (var file in files) { + Assert.StartsWith (outputDir, file); + Assert.True (File.Exists (file), $"Generated file should exist: {file}"); + Assert.EndsWith (".java", file); + } + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } } } - } - // ---- [Export] with throws clause ---- - - [Fact] - public void Generate_ExportWithThrows_HasThrowsClause () - { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, "my/app/ExportWithThrows"); - var java = GenerateToString (peer); - Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java); } - [Fact] - public void Generate_ExportWithoutThrows_HasNoThrowsClause () + public class ExportWithThrowsClause { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, "my/app/ExportExample"); - var java = GenerateToString (peer); - Assert.DoesNotContain ("throws", java); + + [Fact] + public void Generate_ExportWithThrows_HasThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportWithThrows"); + var java = GenerateToString (peer); + Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java); + } + + [Fact] + public void Generate_ExportWithoutThrows_HasNoThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportExample"); + var java = GenerateToString (peer); + Assert.DoesNotContain ("throws", java); + } } -} +} \ No newline at end of file diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 7bf47bb34c8..ec361b15341 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -28,7 +28,7 @@ static string TestFixtureAssemblyPath { static List ScanFixtures () => _cachedFixtures.Value; - string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) + static string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) { var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", (assemblyName ?? "TestTypeMap") + ".dll"); @@ -37,435 +37,469 @@ string GenerateAssembly (IReadOnlyList peers, string? assemblyName return outputPath; } - (PEReader pe, MetadataReader reader) OpenAssembly (string path) + static (PEReader pe, MetadataReader reader) OpenAssembly (string path) { var pe = new PEReader (File.OpenRead (path)); return (pe, pe.GetMetadataReader ()); } - // ---- Basic assembly structure tests ---- - [Fact] - public void Generate_ProducesValidPEAssembly () + public class BasicAssemblyStructure { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - Assert.True (File.Exists (path)); - using var pe = new PEReader (File.OpenRead (path)); - Assert.True (pe.HasMetadata); - var reader = pe.GetMetadataReader (); - Assert.NotNull (reader); - } finally { - CleanUp (path); + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } finally { + CleanUp (path); + } } - } - [Fact] - public void Generate_AssemblyHasCorrectName () - { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers, "MyTestTypeMap"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("MyTestTypeMap", reader.GetString (asmDef.Name)); + [Fact] + public void Generate_AssemblyHasCorrectName () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, "MyTestTypeMap"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("MyTestTypeMap", reader.GetString (asmDef.Name)); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - [Fact] - public void Generate_HasModuleType () - { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var types = reader.TypeDefinitions.Select (h => reader.GetTypeDefinition (h)).ToList (); - Assert.Contains (types, t => reader.GetString (t.Name) == ""); + [Fact] + public void Generate_HasModuleType () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var types = reader.TypeDefinitions.Select (h => reader.GetTypeDefinition (h)).ToList (); + Assert.Contains (types, t => reader.GetString (t.Name) == ""); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- Assembly reference tests ---- + } - [Fact] - public void Generate_HasRequiredAssemblyReferences () + public class AssemblyReference { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var asmRefs = reader.AssemblyReferences - .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) - .ToList (); - Assert.Contains ("System.Runtime", asmRefs); - Assert.Contains ("Mono.Android", asmRefs); - Assert.Contains ("Java.Interop", asmRefs); - Assert.Contains ("System.Runtime.InteropServices", asmRefs); + + [Fact] + public void Generate_HasRequiredAssemblyReferences () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("System.Runtime", asmRefs); + Assert.Contains ("Mono.Android", asmRefs); + Assert.Contains ("Java.Interop", asmRefs); + Assert.Contains ("System.Runtime.InteropServices", asmRefs); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- TypeMap attribute tests ---- + } - [Fact] - public void Generate_HasTypeMapAttributes () + public class TypemapAttribute { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var assemblyCustomAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.NotEmpty (assemblyCustomAttrs); - // We should have at least as many attributes as non-duplicate peers - // (TypeMap attrs + IgnoresAccessChecksTo attrs) - Assert.True (assemblyCustomAttrs.Count () >= 2); + + [Fact] + public void Generate_HasTypeMapAttributes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyCustomAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (assemblyCustomAttrs); + // We should have at least as many attributes as non-duplicate peers + // (TypeMap attrs + IgnoresAccessChecksTo attrs) + Assert.True (assemblyCustomAttrs.Count () >= 2); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- Proxy type tests ---- + } - [Fact] - public void Generate_CreatesProxyTypes () + public class ProxyType { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - // At least some proxy types should be generated - Assert.NotEmpty (proxyTypes); - - // Check that a proxy exists for java/lang/Object → Java_Lang_Object_Proxy - Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + // At least some proxy types should be generated + Assert.NotEmpty (proxyTypes); + + // Check that a proxy exists for java/lang/Object → Java_Lang_Object_Proxy + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - [Fact] - public void Generate_ProxyTypesAreSealedClasses () - { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - foreach (var proxy in proxyTypes) { - Assert.True ((proxy.Attributes & TypeAttributes.Sealed) != 0, - $"Proxy {reader.GetString (proxy.Name)} should be sealed"); - Assert.True ((proxy.Attributes & TypeAttributes.Public) != 0, - $"Proxy {reader.GetString (proxy.Name)} should be public"); + [Fact] + public void Generate_ProxyTypesAreSealedClasses () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + foreach (var proxy in proxyTypes) { + Assert.True ((proxy.Attributes & TypeAttributes.Sealed) != 0, + $"Proxy {reader.GetString (proxy.Name)} should be sealed"); + Assert.True ((proxy.Attributes & TypeAttributes.Public) != 0, + $"Proxy {reader.GetString (proxy.Name)} should be public"); + } } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - [Fact] - public void Generate_ProxyType_HasCtorAndCreateInstance () - { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var objectProxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - - var methods = objectProxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains (".ctor", methods); - Assert.Contains ("CreateInstance", methods); - Assert.Contains ("get_TargetType", methods); + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- ACW proxy tests ---- + } - [Fact] - public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () + public class AcwProxy { - var peers = ScanFixtures (); - // Find a non-MCW type with marshal methods (e.g., my/app/CustomView has constructors) - var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); - var path = GenerateAssembly (new [] { acwPeer }, "AcwTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var proxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); - - var methods = proxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains ("RegisterNatives", methods); - // UCO wrappers for each marshal method - Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0")); + + [Fact] + public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () + { + var peers = ScanFixtures (); + // Find a non-MCW type with marshal methods (e.g., my/app/CustomView has constructors) + var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); + var path = GenerateAssembly (new [] { acwPeer }, "AcwTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains ("RegisterNatives", methods); + // UCO wrappers for each marshal method + Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0")); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - [Fact] - public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () - { - var peers = ScanFixtures (); - var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); - var path = GenerateAssembly (new [] { acwPeer }, "UcoTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var proxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); - - // Find a UCO method - var ucoMethod = proxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .First (m => reader.GetString (m.Name).Contains ("_uco_")); - - // Verify it has [UnmanagedCallersOnly] attribute - var attrs = ucoMethod.GetCustomAttributes () - .Select (h => reader.GetCustomAttribute (h)) - .ToList (); - Assert.NotEmpty (attrs); + [Fact] + public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); + var path = GenerateAssembly (new [] { acwPeer }, "UcoTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + + // Find a UCO method + var ucoMethod = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .First (m => reader.GetString (m.Name).Contains ("_uco_")); + + // Verify it has [UnmanagedCallersOnly] attribute + var attrs = ucoMethod.GetCustomAttributes () + .Select (h => reader.GetCustomAttribute (h)) + .ToList (); + Assert.NotEmpty (attrs); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- IgnoresAccessChecksTo tests ---- + } - [Fact] - public void Generate_HasIgnoresAccessChecksToAttribute () + public class Ignoresaccesschecksto { - var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - // The IgnoresAccessChecksToAttribute type should be defined - var types = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .ToList (); - Assert.Contains (types, t => - reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && - reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // The IgnoresAccessChecksToAttribute type should be defined + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- Alias tests ---- + } - [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntries () + public class Alias { - // Create two peers with the same JNI name — these become aliases - var peers = new List { - new JavaPeerInfo { - JavaName = "test/Duplicate", - ManagedTypeName = "Test.Duplicate1", - ManagedTypeNamespace = "Test", - ManagedTypeShortName = "Duplicate1", - AssemblyName = "TestAssembly", - }, - new JavaPeerInfo { - JavaName = "test/Duplicate", - ManagedTypeName = "Test.Duplicate2", - ManagedTypeNamespace = "Test", - ManagedTypeShortName = "Duplicate2", - AssemblyName = "TestAssembly", - }, - }; - - var path = GenerateAssembly (peers, "AliasTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - // Neither peer has activation ctor → no proxies, but both get entries - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - // Should have 2 TypeMap entries + IgnoresAccessChecksTo entries - Assert.True (assemblyAttrs.Count () >= 2); + + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntries () + { + // Create two peers with the same JNI name — these become aliases + var peers = new List { + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate1", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate1", + AssemblyName = "TestAssembly", + }, + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate2", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate2", + AssemblyName = "TestAssembly", + }, + }; + + var path = GenerateAssembly (peers, "AliasTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Neither peer has activation ctor → no proxies, but both get entries + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + // Should have 2 TypeMap entries + IgnoresAccessChecksTo entries + Assert.True (assemblyAttrs.Count () >= 2); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- Empty input tests ---- + } - [Fact] - public void Generate_EmptyPeerList_ProducesValidAssembly () + public class EmptyInput { - var path = GenerateAssembly (Array.Empty (), "EmptyTest"); - try { - Assert.True (File.Exists (path)); - var (pe, reader) = OpenAssembly (path); - using (pe) { - Assert.NotNull (reader); - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + var path = GenerateAssembly (Array.Empty (), "EmptyTest"); + try { + Assert.True (File.Exists (path)); + var (pe, reader) = OpenAssembly (path); + using (pe) { + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- Per-assembly model tests ---- + } - [Fact] - public void Generate_SingleAssemblyInput_Works () + public class PerassemblyModel { - var allPeers = ScanFixtures (); - // Filter to just one assembly's peers - var testFixturePeers = allPeers.Where (p => p.AssemblyName == "TestFixtures").ToList (); - - var path = GenerateAssembly (testFixturePeers, "_TestFixtures.TypeMap"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("_TestFixtures.TypeMap", reader.GetString (asmDef.Name)); - - // Should still have proxy types - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - Assert.NotEmpty (proxyTypes); + + [Fact] + public void Generate_SingleAssemblyInput_Works () + { + var allPeers = ScanFixtures (); + // Filter to just one assembly's peers + var testFixturePeers = allPeers.Where (p => p.AssemblyName == "TestFixtures").ToList (); + + var path = GenerateAssembly (testFixturePeers, "_TestFixtures.TypeMap"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("_TestFixtures.TypeMap", reader.GetString (asmDef.Name)); + + // Should still have proxy types + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.NotEmpty (proxyTypes); + } + } finally { + CleanUp (path); } - } finally { - CleanUp (path); } - } - // ---- JNI signature helper tests ---- - - [Theory] - [InlineData ("()V", 0)] - [InlineData ("(I)V", 1)] - [InlineData ("(Landroid/os/Bundle;)V", 1)] - [InlineData ("(IFJ)V", 3)] - [InlineData ("(ZLandroid/view/View;I)Z", 3)] - [InlineData ("([Ljava/lang/String;)V", 1)] - public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) - { - var actual = JniSignatureHelper.ParseParameterTypes (signature); - Assert.Equal (expectedCount, actual.Count); } - [Fact] - public void ParseParameterTypes_BooleanMapsToBoolean () + public class JniSignatureHelperTests { - var types = JniSignatureHelper.ParseParameterTypes ("(Z)V"); - Assert.Single (types); - Assert.Equal (JniParamKind.Boolean, types [0]); - } - [Fact] - public void ParseParameterTypes_ObjectMapsToObject () - { - var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V"); - Assert.Single (types); - Assert.Equal (JniParamKind.Object, types [0]); - } + [Theory] + [InlineData ("()V", 0)] + [InlineData ("(I)V", 1)] + [InlineData ("(Landroid/os/Bundle;)V", 1)] + [InlineData ("(IFJ)V", 3)] + [InlineData ("(ZLandroid/view/View;I)Z", 3)] + [InlineData ("([Ljava/lang/String;)V", 1)] + public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) + { + var actual = JniSignatureHelper.ParseParameterTypes (signature); + Assert.Equal (expectedCount, actual.Count); + } - [Fact] - public void ParseReturnType_Void () - { - Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V")); - } + [Fact] + public void ParseParameterTypes_BooleanMapsToBoolean () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Z)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Boolean, types [0]); + } - [Fact] - public void ParseReturnType_Int () - { - Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I")); - } + [Fact] + public void ParseParameterTypes_ObjectMapsToObject () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Object, types [0]); + } - [Fact] - public void ParseReturnType_Boolean () - { - Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z")); - } + [Fact] + public void ParseReturnType_Void () + { + Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V")); + } - [Fact] - public void ParseReturnType_Object () - { - Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); - } + [Fact] + public void ParseReturnType_Int () + { + Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I")); + } - // ---- Negative / edge-case tests ---- + [Fact] + public void ParseReturnType_Boolean () + { + Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z")); + } - [Theory] - [InlineData ("")] - [InlineData ("not-a-sig")] - [InlineData ("(")] - public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature) - { - // Should not crash — either returns empty or throws ArgumentException - try { - var result = JniSignatureHelper.ParseParameterTypes (signature); - // If it doesn't throw, empty is acceptable - Assert.NotNull (result); - } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) { - // Any of these are acceptable for malformed input + [Fact] + public void ParseReturnType_Object () + { + Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); } - } - [Fact] - public void Generate_NullPeers_ThrowsArgumentNull () - { - var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll"); - Assert.Throws (() => gen.Generate (null!, tmpPath)); } - [Fact] - public void Generate_NullOutputPath_ThrowsArgumentNull () + public class NegativeEdgecase { - var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - Assert.Throws (() => gen.Generate (Array.Empty (), null!)); + + [Theory] + [InlineData ("")] + [InlineData ("not-a-sig")] + [InlineData ("(")] + public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature) + { + // Should not crash — either returns empty or throws ArgumentException + try { + var result = JniSignatureHelper.ParseParameterTypes (signature); + // If it doesn't throw, empty is acceptable + Assert.NotNull (result); + } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) { + // Any of these are acceptable for malformed input + } + } + + [Fact] + public void Generate_NullPeers_ThrowsArgumentNull () + { + var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll"); + Assert.Throws (() => gen.Generate (null!, tmpPath)); + } + + [Fact] + public void Generate_NullOutputPath_ThrowsArgumentNull () + { + var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + Assert.Throws (() => gen.Generate (Array.Empty (), null!)); + } + } static void CleanUp (string path) @@ -475,4 +509,4 @@ static void CleanUp (string path) try { Directory.Delete (dir, true); } catch { } } } -} +} \ No newline at end of file diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index cce2f6a6c03..8bec374aa34 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -27,672 +27,704 @@ static string TestFixtureAssemblyPath { static List ScanFixtures () => _cachedFixtures.Value; - TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) + static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); return ModelBuilder.Build (peers, outputPath, assemblyName); } - // ---- Basic model structure ---- - [Fact] - public void Build_EmptyPeers_ProducesEmptyModel () + public class BasicStructure { - var model = BuildModel (Array.Empty (), "Empty"); - Assert.Equal ("Empty", model.AssemblyName); - Assert.Equal ("Empty.dll", model.ModuleName); - Assert.Empty (model.Entries); - Assert.Empty (model.ProxyTypes); - } - [Fact] - public void Build_AssemblyNameDerivedFromOutputPath () - { - var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); - Assert.Equal ("Foo.Bar", model.AssemblyName); - Assert.Equal ("Foo.Bar.dll", model.ModuleName); - } + [Fact] + public void Build_EmptyPeers_ProducesEmptyModel () + { + var model = BuildModel (Array.Empty (), "Empty"); + Assert.Equal ("Empty", model.AssemblyName); + Assert.Equal ("Empty.dll", model.ModuleName); + Assert.Empty (model.Entries); + Assert.Empty (model.ProxyTypes); + } - [Fact] - public void Build_ExplicitAssemblyName_OverridesOutputPath () - { - var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); - Assert.Equal ("MyAssembly", model.AssemblyName); - } + [Fact] + public void Build_AssemblyNameDerivedFromOutputPath () + { + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); + Assert.Equal ("Foo.Bar", model.AssemblyName); + Assert.Equal ("Foo.Bar.dll", model.ModuleName); + } + + [Fact] + public void Build_ExplicitAssemblyName_OverridesOutputPath () + { + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + Assert.Equal ("MyAssembly", model.AssemblyName); + } + + [Fact] + public void Build_EmptyInput_HasEmptyIgnoresAccessChecksTo () + { + var model = BuildModel (Array.Empty ()); + Assert.Empty (model.IgnoresAccessChecksTo); + } + + [Fact] + public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + { + var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp"); + ((List) peer.MarshalMethods).Add (new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + IsConstructor = false, + DeclaringTypeName = "Android.App.Activity", + DeclaringAssemblyName = "Mono.Android", + }); + var model = BuildModel (new [] { peer }); + // The UCO callback type references Mono.Android, which is cross-assembly + Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); + // The output assembly itself should not appear + Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo); + } - [Fact] - public void Build_EmptyInput_HasEmptyIgnoresAccessChecksTo () - { - var model = BuildModel (Array.Empty ()); - Assert.Empty (model.IgnoresAccessChecksTo); } - [Fact] - public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + public class TypeMapEntries { - var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp"); - ((List) peer.MarshalMethods).Add (new MarshalMethodInfo { - JniName = "onCreate", - NativeCallbackName = "n_OnCreate", - JniSignature = "(Landroid/os/Bundle;)V", - IsConstructor = false, - DeclaringTypeName = "Android.App.Activity", - DeclaringAssemblyName = "Mono.Android", - }); - var model = BuildModel (new [] { peer }); - // The UCO callback type references Mono.Android, which is cross-assembly - Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); - // The output assembly itself should not appear - Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo); - } - // ---- TypeMap entries ---- + [Fact] + public void Build_CreatesOneEntryPerPeer () + { + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"), + }; - [Fact] - public void Build_CreatesOneEntryPerPeer () - { - var peers = new List { - MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), - MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"), - }; + var model = BuildModel (peers); + Assert.Equal (2, model.Entries.Count); + // Entries are ordered by JNI name (alphabetical) + Assert.Equal ("android/app/Activity", model.Entries [0].JniName); + Assert.Equal ("java/lang/Object", model.Entries [1].JniName); + } + + [Fact] + public void Build_DuplicateJniNames_CreatesAliasEntries () + { + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "A"), + MakeMcwPeer ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers); + // Two entries: primary "test/Dup" and alias "test/Dup[1]" + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("test/Dup", model.Entries [0].JniName); + Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); + Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); + Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); + } - var model = BuildModel (peers); - Assert.Equal (2, model.Entries.Count); - // Entries are ordered by JNI name (alphabetical) - Assert.Equal ("android/app/Activity", model.Entries [0].JniName); - Assert.Equal ("java/lang/Object", model.Entries [1].JniName); } - [Fact] - public void Build_DuplicateJniNames_CreatesAliasEntries () + public class ConditionalAttributes { - var peers = new List { - MakeMcwPeer ("test/Dup", "Test.First", "A"), - MakeMcwPeer ("test/Dup", "Test.Second", "A"), - }; - var model = BuildModel (peers); - // Two entries: primary "test/Dup" and alias "test/Dup[1]" - Assert.Equal (2, model.Entries.Count); - Assert.Equal ("test/Dup", model.Entries [0].JniName); - Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); - Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); - Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); - } + [Fact] + public void Build_EssentialRuntimeType_IsUnconditional () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); - // ---- 2-arg (unconditional) vs 3-arg (trimmable) attributes ---- + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + Assert.Null (model.Entries [0].TargetTypeReference); + } - [Fact] - public void Build_EssentialRuntimeType_IsUnconditional () - { - var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - peer.DoNotGenerateAcw = true; - var model = BuildModel (new [] { peer }); + [Theory] + [InlineData ("java/lang/Object")] + [InlineData ("java/lang/Throwable")] + [InlineData ("java/lang/Exception")] + [InlineData ("java/lang/RuntimeException")] + [InlineData ("java/lang/Error")] + [InlineData ("java/lang/Class")] + [InlineData ("java/lang/String")] + [InlineData ("java/lang/Thread")] + public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) + { + var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); + } - Assert.Single (model.Entries); - Assert.True (model.Entries [0].IsUnconditional); - Assert.Null (model.Entries [0].TargetTypeReference); - } + [Fact] + public void Build_UserAcwType_IsUnconditional () + { + // User-defined ACW types (not MCW, not interface) are unconditional + // because Android can instantiate them from Java + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); + + var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main"); + Assert.True (mainEntry.IsUnconditional); + Assert.Null (mainEntry.TargetTypeReference); + } - [Theory] - [InlineData ("java/lang/Object")] - [InlineData ("java/lang/Throwable")] - [InlineData ("java/lang/Exception")] - [InlineData ("java/lang/RuntimeException")] - [InlineData ("java/lang/Error")] - [InlineData ("java/lang/Class")] - [InlineData ("java/lang/String")] - [InlineData ("java/lang/Thread")] - public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) - { - var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android"); - peer.DoNotGenerateAcw = true; - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); - } + [Fact] + public void Build_McwBinding_IsTrimmable () + { + // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential + var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); + Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!); + } - [Fact] - public void Build_UserAcwType_IsUnconditional () - { - // User-defined ACW types (not MCW, not interface) are unconditional - // because Android can instantiate them from Java - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - var model = BuildModel (new [] { peer }); - - var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main"); - Assert.True (mainEntry.IsUnconditional); - Assert.Null (mainEntry.TargetTypeReference); - } + [Fact] + public void Build_Interface_IsTrimmable () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); + } - [Fact] - public void Build_McwBinding_IsTrimmable () - { - // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential - var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"); - peer.DoNotGenerateAcw = true; - var model = BuildModel (new [] { peer }); - - Assert.Single (model.Entries); - Assert.False (model.Entries [0].IsUnconditional); - Assert.NotNull (model.Entries [0].TargetTypeReference); - Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!); - } + [Fact] + public void Build_UnconditionalScannedType_IsUnconditional () + { + // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); + peer.DoNotGenerateAcw = true; // simulate MCW-like + peer.IsUnconditional = true; // scanner marked it + var model = BuildModel (new [] { peer }); - [Fact] - public void Build_Interface_IsTrimmable () - { - var peer = new JavaPeerInfo { - JavaName = "android/view/View$OnClickListener", - ManagedTypeName = "Android.Views.View+IOnClickListener", - ManagedTypeNamespace = "Android.Views", - ManagedTypeShortName = "IOnClickListener", - AssemblyName = "Mono.Android", - IsInterface = true, - InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", - }; + Assert.True (model.Entries [0].IsUnconditional); + } - var model = BuildModel (new [] { peer }); - Assert.Single (model.Entries); - Assert.False (model.Entries [0].IsUnconditional); - Assert.NotNull (model.Entries [0].TargetTypeReference); } - [Fact] - public void Build_UnconditionalScannedType_IsUnconditional () + public class Aliases { - // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) - var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); - peer.DoNotGenerateAcw = true; // simulate MCW-like - peer.IsUnconditional = true; // scanner marked it - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Build_AliasedPeers_GetIndexedJniNames () + { + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "A"), + MakeMcwPeer ("test/Dup", "Test.Second", "A"), + MakeMcwPeer ("test/Dup", "Test.Third", "A"), + }; - // ---- Alias tests ---- + var model = BuildModel (peers); + Assert.Equal (3, model.Entries.Count); + Assert.Equal ("test/Dup", model.Entries [0].JniName); + Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); + Assert.Equal ("test/Dup[2]", model.Entries [2].JniName); + } - [Fact] - public void Build_AliasedPeers_GetIndexedJniNames () - { - var peers = new List { - MakeMcwPeer ("test/Dup", "Test.First", "A"), - MakeMcwPeer ("test/Dup", "Test.Second", "A"), - MakeMcwPeer ("test/Dup", "Test.Third", "A"), - }; + [Fact] + public void Build_AliasedPeersWithActivation_GetDistinctProxies () + { + var peers = new List { + MakePeerWithActivation ("test/Dup", "Test.First", "A"), + MakePeerWithActivation ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers, "TypeMap"); + Assert.Equal (2, model.ProxyTypes.Count); + // Distinct proxy names based on managed type names + Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName); + Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName); + } - var model = BuildModel (peers); - Assert.Equal (3, model.Entries.Count); - Assert.Equal ("test/Dup", model.Entries [0].JniName); - Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); - Assert.Equal ("test/Dup[2]", model.Entries [2].JniName); - } + [Fact] + public void Build_McwPeerWithoutActivation_NoProxy () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + // No activation ctor, no invoker → no proxy + var model = BuildModel (new [] { peer }); - [Fact] - public void Build_AliasedPeersWithActivation_GetDistinctProxies () - { - var peers = new List { - MakePeerWithActivation ("test/Dup", "Test.First", "A"), - MakePeerWithActivation ("test/Dup", "Test.Second", "A"), - }; + Assert.Empty (model.ProxyTypes); + Assert.Single (model.Entries); + Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); + } - var model = BuildModel (peers, "TypeMap"); - Assert.Equal (2, model.ProxyTypes.Count); - // Distinct proxy names based on managed type names - Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName); - Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName); } - [Fact] - public void Build_McwPeerWithoutActivation_NoProxy () + public class ProxyTypes { - var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - // No activation ctor, no invoker → no proxy - var model = BuildModel (new [] { peer }); - Assert.Empty (model.ProxyTypes); - Assert.Single (model.Entries); - Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); - } + [Fact] + public void Build_PeerWithActivationCtor_CreatesProxy () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); - // ---- Proxy types ---- + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName); + Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); + Assert.True (proxy.HasActivation); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); + } - [Fact] - public void Build_PeerWithActivationCtor_CreatesProxy () - { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - var model = BuildModel (new [] { peer }, "MyTypeMap"); - - Assert.Single (model.ProxyTypes); - var proxy = model.ProxyTypes [0]; - Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName); - Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); - Assert.True (proxy.HasActivation); - Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); - Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); - } + [Fact] + public void Build_PeerWithInvoker_CreatesProxy () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); + } - [Fact] - public void Build_PeerWithInvoker_CreatesProxy () - { - var peer = new JavaPeerInfo { - JavaName = "android/view/View$OnClickListener", - ManagedTypeName = "Android.Views.View+IOnClickListener", - ManagedTypeNamespace = "Android.Views", - ManagedTypeShortName = "IOnClickListener", - AssemblyName = "Mono.Android", - IsInterface = true, - InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", - }; + [Fact] + public void Build_PeerWithInvokerButNoActivationCtor_ProxyHasActivationTrue () + { + // An interface with an invoker type has HasActivation = true because + // CreateInstance will instantiate the invoker type. + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.True (proxy.HasActivation); + Assert.NotNull (proxy.InvokerType); + } - var model = BuildModel (new [] { peer }); - Assert.Single (model.ProxyTypes); - var proxy = model.ProxyTypes [0]; - Assert.NotNull (proxy.InvokerType); - Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); - } + [Fact] + public void Build_ProxyNaming_ReplacesDotAndPlus () + { + var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); + var model = BuildModel (new [] { peer }); - [Fact] - public void Build_PeerWithInvokerButNoActivationCtor_ProxyHasActivationTrue () - { - // An interface with an invoker type has HasActivation = true because - // CreateInstance will instantiate the invoker type. - var peer = new JavaPeerInfo { - JavaName = "android/view/View$OnClickListener", - ManagedTypeName = "Android.Views.View+IOnClickListener", - ManagedTypeNamespace = "Android.Views", - ManagedTypeShortName = "IOnClickListener", - AssemblyName = "Mono.Android", - IsInterface = true, - InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", - }; + Assert.Single (model.ProxyTypes); + Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); + } - var model = BuildModel (new [] { peer }); - Assert.Single (model.ProxyTypes); - var proxy = model.ProxyTypes [0]; - Assert.True (proxy.HasActivation); - Assert.NotNull (proxy.InvokerType); - } + [Fact] + public void Build_EntryPointsToProxy_WhenProxyExists () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); - [Fact] - public void Build_ProxyNaming_ReplacesDotAndPlus () - { - var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); - var model = BuildModel (new [] { peer }); + var entry = model.Entries [0]; + Assert.Contains ("Java_Lang_Object_Proxy", entry.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); + } - Assert.Single (model.ProxyTypes); - Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); } - [Fact] - public void Build_EntryPointsToProxy_WhenProxyExists () + public class AcwDetection { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - var model = BuildModel (new [] { peer }, "MyTypeMap"); - var entry = model.Entries [0]; - Assert.Contains ("Java_Lang_Object_Proxy", entry.ProxyTypeReference); - Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); - } + [Fact] + public void Build_AcwType_IsAcwTrue () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); - // ---- ACW detection ---- + Assert.Single (model.ProxyTypes); + Assert.True (model.ProxyTypes [0].IsAcw); + Assert.True (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); + } - [Fact] - public void Build_AcwType_IsAcwTrue () - { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - var model = BuildModel (new [] { peer }); + [Fact] + public void Build_McwType_IsAcwFalse () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); - Assert.Single (model.ProxyTypes); - Assert.True (model.ProxyTypes [0].IsAcw); - Assert.True (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); - } + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + Assert.False (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); + } - [Fact] - public void Build_McwType_IsAcwFalse () - { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - var model = BuildModel (new [] { peer }); + [Fact] + public void Build_InterfaceWithMarshalMethods_IsNotAcw () + { + var peer = new JavaPeerInfo { + JavaName = "android/view/View$OnClickListener", + ManagedTypeName = "Android.Views.View+IOnClickListener", + ManagedTypeNamespace = "Android.Views", + ManagedTypeShortName = "IOnClickListener", + AssemblyName = "Mono.Android", + IsInterface = true, + InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", + MarshalMethods = new List { + MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"), + }, + }; + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + // Interface is NOT an ACW even with marshal methods + Assert.False (model.ProxyTypes [0].IsAcw); + } - Assert.Single (model.ProxyTypes); - Assert.False (model.ProxyTypes [0].IsAcw); - Assert.False (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); - } + [Fact] + public void Build_DoNotGenerateAcw_IsNotAcw () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + peer.DoNotGenerateAcw = true; + peer.MarshalMethods = new List { + MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"), + }; - [Fact] - public void Build_InterfaceWithMarshalMethods_IsNotAcw () - { - var peer = new JavaPeerInfo { - JavaName = "android/view/View$OnClickListener", - ManagedTypeName = "Android.Views.View+IOnClickListener", - ManagedTypeNamespace = "Android.Views", - ManagedTypeShortName = "IOnClickListener", - AssemblyName = "Mono.Android", - IsInterface = true, - InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", - MarshalMethods = new List { - MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"), - }, - }; + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + Assert.False (model.ProxyTypes [0].IsAcw); + } - var model = BuildModel (new [] { peer }); - Assert.Single (model.ProxyTypes); - // Interface is NOT an ACW even with marshal methods - Assert.False (model.ProxyTypes [0].IsAcw); } - [Fact] - public void Build_DoNotGenerateAcw_IsNotAcw () + public class UcoMethods { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - peer.DoNotGenerateAcw = true; - peer.MarshalMethods = new List { - MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"), - }; - var model = BuildModel (new [] { peer }); - Assert.Single (model.ProxyTypes); - Assert.False (model.ProxyTypes [0].IsAcw); - } + [Fact] + public void Build_AcwWithMarshalMethods_CreatesUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + MakeMarshalMethod ("onResume", "n_OnResume", "()V"), + }; - // ---- UCO methods ---- + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; - [Fact] - public void Build_AcwWithMarshalMethods_CreatesUcoMethods () - { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), - MakeMarshalMethod ("onResume", "n_OnResume", "()V"), - }; - - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; + Assert.Equal (2, proxy.UcoMethods.Count); + Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName); + Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature); - Assert.Equal (2, proxy.UcoMethods.Count); - Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName); - Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName); - Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature); + Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName); + Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName); + } - Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName); - Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName); - } + [Fact] + public void Build_UcoMethod_CallbackTypeIsDeclaringType () + { + var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"); + mm.DeclaringTypeName = "Java.Lang.Object"; + mm.DeclaringAssemblyName = "Mono.Android"; + + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + mm, + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName); + Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName); + } - [Fact] - public void Build_UcoMethod_CallbackTypeIsDeclaringType () - { - var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"); - mm.DeclaringTypeName = "Java.Lang.Object"; - mm.DeclaringAssemblyName = "Mono.Android"; + [Fact] + public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onPause", "n_OnPause", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var uco = model.ProxyTypes [0].UcoMethods [0]; + Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName); + Assert.Equal ("App", uco.CallbackType.AssemblyName); + } - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - mm, - }; + [Fact] + public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true), + MakeMarshalMethod ("onStart", "n_OnStart", "()V"), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + // Only 1 UCO method (constructors are skipped from UcoMethods) + Assert.Single (proxy.UcoMethods); + Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName); + } - var model = BuildModel (new [] { peer }); - var uco = model.ProxyTypes [0].UcoMethods [0]; - Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName); - Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName); } - [Fact] - public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty () + public class UcoConstructors { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onPause", "n_OnPause", "()V"), - }; - var model = BuildModel (new [] { peer }); - var uco = model.ProxyTypes [0].UcoMethods [0]; - Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName); - Assert.Equal ("App", uco.CallbackType.AssemblyName); - } + [Fact] + public void Build_AcwWithConstructors_CreatesUcoConstructors () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - [Fact] - public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () - { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true), - MakeMarshalMethod ("onStart", "n_OnStart", "()V"), - }; + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; + Assert.Single (proxy.UcoConstructors); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + } - // Only 1 UCO method (constructors are skipped from UcoMethods) - Assert.Single (proxy.UcoMethods); - Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName); - } + [Fact] + public void Build_PeerWithoutActivationCtor_NoUcoConstructors () + { + // Peer with marshal methods but no activation ctor + var peer = new JavaPeerInfo { + JavaName = "my/app/Foo", + ManagedTypeName = "MyApp.Foo", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "Foo", + AssemblyName = "App", + InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy + MarshalMethods = new List { + MakeMarshalMethod ("bar", "n_Bar", "()V"), + }, + JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }, + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.Empty (proxy.UcoConstructors); + } - // ---- UCO constructors ---- + } - [Fact] - public void Build_AcwWithConstructors_CreatesUcoConstructors () + public class NativeRegistrations { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; + [Fact] + public void Build_NativeRegistrations_MatchUcoMethods () + { + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + }; - Assert.Single (proxy.UcoConstructors); - Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); - Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName); - } + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; - [Fact] - public void Build_PeerWithoutActivationCtor_NoUcoConstructors () - { - // Peer with marshal methods but no activation ctor - var peer = new JavaPeerInfo { - JavaName = "my/app/Foo", - ManagedTypeName = "MyApp.Foo", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "Foo", - AssemblyName = "App", - InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy - MarshalMethods = new List { - MakeMarshalMethod ("bar", "n_Bar", "()V"), - }, - JavaConstructors = new List { - new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, - }, - }; + // 1 registration for method + 1 for constructor + Assert.Equal (2, proxy.NativeRegistrations.Count); - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; + var methodReg = proxy.NativeRegistrations [0]; + Assert.Equal ("n_OnCreate", methodReg.JniMethodName); + Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature); + Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName); - Assert.Empty (proxy.UcoConstructors); - } + var ctorReg = proxy.NativeRegistrations [1]; + Assert.Equal ("nctor_0", ctorReg.JniMethodName); + Assert.Equal ("()V", ctorReg.JniSignature); + Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName); + } - // ---- Native registrations ---- + [Fact] + public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature () + { + var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App"); + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V", + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + } + }, + }; + peer.MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true), + }; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; - [Fact] - public void Build_NativeRegistrations_MatchUcoMethods () - { - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), - }; + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + } - // 1 registration for method + 1 for constructor - Assert.Equal (2, proxy.NativeRegistrations.Count); + [Fact] + public void Build_NonAcwProxy_NoNativeRegistrations () + { + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); - var methodReg = proxy.NativeRegistrations [0]; - Assert.Equal ("n_OnCreate", methodReg.JniMethodName); - Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature); - Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName); + Assert.Single (model.ProxyTypes); + Assert.Empty (model.ProxyTypes [0].NativeRegistrations); + } - var ctorReg = proxy.NativeRegistrations [1]; - Assert.Equal ("nctor_0", ctorReg.JniMethodName); - Assert.Equal ("()V", ctorReg.JniSignature); - Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName); } - [Fact] - public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature () + public class FixtureScan { - var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App"); - peer.JavaConstructors = new List { - new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, - new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V", - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - } - }, - }; - peer.MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true), - }; - - var model = BuildModel (new [] { peer }); - var proxy = model.ProxyTypes [0]; - var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); - Assert.Equal (2, ctorRegs.Count); - - Assert.Equal ("()V", ctorRegs [0].JniSignature); - Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); - } + [Fact] + public void Build_FromScannedFixtures_ProducesValidModel () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "TestTypeMap"); - [Fact] - public void Build_NonAcwProxy_NoNativeRegistrations () - { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); - var model = BuildModel (new [] { peer }); + Assert.Equal ("TestTypeMap", model.AssemblyName); + Assert.NotEmpty (model.Entries); + Assert.NotEmpty (model.ProxyTypes); - Assert.Single (model.ProxyTypes); - Assert.Empty (model.ProxyTypes [0].NativeRegistrations); - } + // All entries have non-empty JNI names + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference))); + } - // ---- Full fixture scan ---- + [Fact] + public void Build_FromScannedFixtures_NoProxiesForMcwWithoutActivation () + { + var peers = ScanFixtures (); + var model = BuildModel (peers); - [Fact] - public void Build_FromScannedFixtures_ProducesValidModel () - { - var peers = ScanFixtures (); - var model = BuildModel (peers, "TestTypeMap"); + // Proxy type names should all end with _Proxy + Assert.All (model.ProxyTypes, p => Assert.EndsWith ("_Proxy", p.TypeName)); + } - Assert.Equal ("TestTypeMap", model.AssemblyName); - Assert.NotEmpty (model.Entries); - Assert.NotEmpty (model.ProxyTypes); + [Fact] + public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () + { + var peers = ScanFixtures (); + var model = BuildModel (peers); - // All entries have non-empty JNI names - Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); - Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference))); - } + var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); + Assert.NotEmpty (acwProxies); - [Fact] - public void Build_FromScannedFixtures_NoProxiesForMcwWithoutActivation () - { - var peers = ScanFixtures (); - var model = BuildModel (peers); + // ACW proxies should have registrations + foreach (var proxy in acwProxies) { + Assert.NotEmpty (proxy.NativeRegistrations); + } + } - // Proxy type names should all end with _Proxy - Assert.All (model.ProxyTypes, p => Assert.EndsWith ("_Proxy", p.TypeName)); } - [Fact] - public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () + public class FixtureConditionalAttributes { - var peers = ScanFixtures (); - var model = BuildModel (peers); - var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); - Assert.NotEmpty (acwProxies); - - // ACW proxies should have registrations - foreach (var proxy in acwProxies) { - Assert.NotEmpty (proxy.NativeRegistrations); + [Fact] + public void Fixture_JavaLangObject_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); } - } - // ---- Fixture-based 2-arg vs 3-arg tests ---- - - [Fact] - public void Fixture_JavaLangObject_IsUnconditional () - { - var peer = FindFixtureByJavaName ("java/lang/Object"); - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_Throwable_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Throwable"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_Throwable_IsUnconditional () - { - var peer = FindFixtureByJavaName ("java/lang/Throwable"); - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_Exception_IsUnconditional () + { + var peer = FindFixtureByJavaName ("java/lang/Exception"); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_Exception_IsUnconditional () - { - var peer = FindFixtureByJavaName ("java/lang/Exception"); - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_Activity_McwBinding_IsTrimmable () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + Assert.True (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + // Activity is MCW and not an essential runtime type → trimmable + Assert.False (model.Entries [0].IsUnconditional); + Assert.Contains ("Android.App.Activity", model.Entries [0].TargetTypeReference!); + } - [Fact] - public void Fixture_Activity_McwBinding_IsTrimmable () - { - var peer = FindFixtureByJavaName ("android/app/Activity"); - Assert.True (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }); - // Activity is MCW and not an essential runtime type → trimmable - Assert.False (model.Entries [0].IsUnconditional); - Assert.Contains ("Android.App.Activity", model.Entries [0].TargetTypeReference!); - } + [Fact] + public void Fixture_MainActivity_UserAcw_IsUnconditional () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_MainActivity_UserAcw_IsUnconditional () - { - var peer = FindFixtureByJavaName ("my/app/MainActivity"); - Assert.False (peer.DoNotGenerateAcw); - Assert.False (peer.IsInterface); - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_IOnClickListener_Interface_IsTrimmable () + { + var peers = ScanFixtures (); + var listener = peers.First (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + var model = BuildModel (new [] { listener }); + Assert.False (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_IOnClickListener_Interface_IsTrimmable () - { - var peers = ScanFixtures (); - var listener = peers.First (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); - var model = BuildModel (new [] { listener }); - Assert.False (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_TouchHandler_UserType_IsUnconditional () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + Assert.False (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_TouchHandler_UserType_IsUnconditional () - { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); - Assert.False (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }); - Assert.True (model.Entries [0].IsUnconditional); - } + [Fact] + public void Fixture_Button_McwBinding_IsTrimmable () + { + var peer = FindFixtureByJavaName ("android/widget/Button"); + Assert.True (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.False (model.Entries [0].IsUnconditional); + } - [Fact] - public void Fixture_Button_McwBinding_IsTrimmable () - { - var peer = FindFixtureByJavaName ("android/widget/Button"); - Assert.True (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }); - Assert.False (model.Entries [0].IsUnconditional); } - // ---- Helpers ---- - static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; @@ -745,9 +777,7 @@ static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, }; } - // ======================================================================== // Fixture-based tests: scan the real TestFixtures.dll and verify model output - // ======================================================================== static JavaPeerInfo FindFixtureByJavaName (string javaName) { @@ -767,918 +797,971 @@ static JavaPeerInfo FindFixtureByJavaName (string javaName) return model.Entries.FirstOrDefault (e => e.JniName == jniName); } - // ---- MCW types from fixtures ---- - - [Fact] - public void Fixture_JavaLangObject_HasActivation_CreatesProxy () - { - var peer = FindFixtureByJavaName ("java/lang/Object"); - var model = BuildModel (new [] { peer }, "TypeMap"); - - var proxy = FindProxy (model, "Java_Lang_Object_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.HasActivation); - Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); - Assert.Equal ("TestFixtures", proxy.TargetType.AssemblyName); - // MCW with DoNotGenerateAcw → not ACW - Assert.False (proxy.IsAcw); - Assert.Empty (proxy.UcoMethods); - Assert.Empty (proxy.UcoConstructors); - Assert.Empty (proxy.NativeRegistrations); - } - - [Fact] - public void Fixture_Activity_HasActivation_CreatesProxy () - { - var peer = FindFixtureByJavaName ("android/app/Activity"); - var model = BuildModel (new [] { peer }, "TypeMap"); - - var proxy = FindProxy (model, "Android_App_Activity_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.HasActivation); - Assert.Equal ("Android.App.Activity", proxy.TargetType.ManagedTypeName); - // MCW: DoNotGenerateAcw=true → not ACW (even though it has marshal methods) - Assert.False (proxy.IsAcw); - } - [Fact] - public void Fixture_Activity_Entry_PointsToProxy () + public class FixtureMcwTypes { - var peer = FindFixtureByJavaName ("android/app/Activity"); - var model = BuildModel (new [] { peer }, "MyTypeMap"); - var entry = FindEntry (model, "android/app/Activity"); - Assert.NotNull (entry); - Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference); - Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); - } - - [Fact] - public void Fixture_Throwable_HasActivation () - { - var peer = FindFixtureByJavaName ("java/lang/Throwable"); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_JavaLangObject_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "Java_Lang_Throwable_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.HasActivation); - Assert.False (proxy.IsAcw); - } + var proxy = FindProxy (model, "Java_Lang_Object_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("TestFixtures", proxy.TargetType.AssemblyName); + // MCW with DoNotGenerateAcw → not ACW + Assert.False (proxy.IsAcw); + Assert.Empty (proxy.UcoMethods); + Assert.Empty (proxy.UcoConstructors); + Assert.Empty (proxy.NativeRegistrations); + } - [Fact] - public void Fixture_Exception_HasActivation () - { - var peer = FindFixtureByJavaName ("java/lang/Exception"); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_Activity_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "Java_Lang_Exception_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.HasActivation); - } + var proxy = FindProxy (model, "Android_App_Activity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Android.App.Activity", proxy.TargetType.ManagedTypeName); + // MCW: DoNotGenerateAcw=true → not ACW (even though it has marshal methods) + Assert.False (proxy.IsAcw); + } - [Fact] - public void Fixture_Service_NoActivation_NoProxy () - { - // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object - // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor - var peer = FindFixtureByJavaName ("android/app/Service"); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_Activity_Entry_PointsToProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); - if (peer.ActivationCtor != null) { - Assert.Single (model.ProxyTypes); - } else { - Assert.Empty (model.ProxyTypes); + var entry = FindEntry (model, "android/app/Activity"); + Assert.NotNull (entry); + Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); } - } - [Fact] - public void Fixture_Context_HasActivation () - { - var peer = FindFixtureByJavaName ("android/content/Context"); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_Throwable_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Throwable"); + var model = BuildModel (new [] { peer }, "TypeMap"); - // Context has (IntPtr, JniHandleOwnership) ctor - if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "Android_Content_Context_Proxy"); + var proxy = FindProxy (model, "Java_Lang_Throwable_Proxy"); Assert.NotNull (proxy); - Assert.False (proxy!.IsAcw); + Assert.True (proxy!.HasActivation); + Assert.False (proxy.IsAcw); } - } - [Fact] - public void Fixture_View_HasActivation () - { - var peer = FindFixtureByJavaName ("android/view/View"); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_Exception_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Exception"); + var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "Android_Views_View_Proxy"); + var proxy = FindProxy (model, "Java_Lang_Exception_Proxy"); Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); } - } - - [Fact] - public void Fixture_Button_HasActivation () - { - var peer = FindFixtureByJavaName ("android/widget/Button"); - var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "Android_Widget_Button_Proxy"); - Assert.NotNull (proxy); + [Fact] + public void Fixture_Service_NoActivation_NoProxy () + { + // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object + // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor + var peer = FindFixtureByJavaName ("android/app/Service"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + Assert.Single (model.ProxyTypes); + } else { + Assert.Empty (model.ProxyTypes); + } } - } - // ---- User ACW types from fixtures ---- + [Fact] + public void Fixture_Context_HasActivation () + { + var peer = FindFixtureByJavaName ("android/content/Context"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // Context has (IntPtr, JniHandleOwnership) ctor + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Content_Context_Proxy"); + Assert.NotNull (proxy); + Assert.False (proxy!.IsAcw); + } + } - [Fact] - public void Fixture_MainActivity_IsAcw () - { - var peer = FindFixtureByJavaName ("my/app/MainActivity"); - Assert.False (peer.DoNotGenerateAcw); - Assert.NotEmpty (peer.MarshalMethods); - Assert.NotNull (peer.ActivationCtor); - - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.IsAcw); - Assert.True (proxy.ImplementsIAndroidCallableWrapper); - Assert.True (proxy.HasActivation); - } + [Fact] + public void Fixture_View_HasActivation () + { + var peer = FindFixtureByJavaName ("android/view/View"); + var model = BuildModel (new [] { peer }, "TypeMap"); - [Fact] - public void Fixture_MainActivity_UcoMethods () - { - var peer = FindFixtureByJavaName ("my/app/MainActivity"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; - - // Should have UCO wrappers for non-constructor marshal methods - var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); - Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); - - // Verify the onCreate wrapper - var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate"); - Assert.NotNull (onCreateUco); - Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature); - Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName); - } + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Views_View_Proxy"); + Assert.NotNull (proxy); + } + } - [Fact] - public void Fixture_MainActivity_NativeRegistrations () - { - var peer = FindFixtureByJavaName ("my/app/MainActivity"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; + [Fact] + public void Fixture_Button_HasActivation () + { + var peer = FindFixtureByJavaName ("android/widget/Button"); + var model = BuildModel (new [] { peer }, "TypeMap"); - Assert.NotEmpty (proxy.NativeRegistrations); + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Widget_Button_Proxy"); + Assert.NotNull (proxy); + } + } - // Should register n_OnCreate - var onCreateReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_OnCreate"); - Assert.NotNull (onCreateReg); - Assert.Equal ("(Landroid/os/Bundle;)V", onCreateReg!.JniSignature); } - [Fact] - public void Fixture_MyHelper_IsAcw () + public class FixtureAcwTypes { - var peer = FindFixtureByJavaName ("my/app/MyHelper"); - Assert.False (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_MainActivity_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.NotEmpty (peer.MarshalMethods); + Assert.NotNull (peer.ActivationCtor); - // MyHelper has marshal methods and is not DoNotGenerateAcw - // Whether it's ACW depends on: not interface, has marshal methods, not DoNotGenerateAcw - if (peer.MarshalMethods.Count > 0 && peer.ActivationCtor != null) { - var proxy = FindProxy (model, "MyApp_MyHelper_Proxy"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.True (proxy.ImplementsIAndroidCallableWrapper); + Assert.True (proxy.HasActivation); } - } - // ---- TouchHandler: various JNI types ---- + [Fact] + public void Fixture_MainActivity_UcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; + + // Should have UCO wrappers for non-constructor marshal methods + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); + + // Verify the onCreate wrapper + var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate"); + Assert.NotNull (onCreateUco); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature); + Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName); + } - [Fact] - public void Fixture_TouchHandler_AllUcoMethods () - { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy"); - Assert.NotNull (proxy); - - var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); - Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count); - - // onTouch: (Landroid/view/View;I)Z - var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch"); - Assert.NotNull (onTouchUco); - Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature); - - // onFocusChange: (Landroid/view/View;Z)V - var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange"); - Assert.NotNull (onFocusUco); - Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature); - - // onScroll: (IFJD)V - var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll"); - Assert.NotNull (onScrollUco); - Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature); - - // getText: ()Ljava/lang/String; - var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText"); - Assert.NotNull (getTextUco); - Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature); - - // setItems: ([Ljava/lang/String;)V - var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems"); - Assert.NotNull (setItemsUco); - Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature); - } + [Fact] + public void Fixture_MainActivity_NativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; - [Fact] - public void Fixture_TouchHandler_NativeRegistrationsMatchUcoMethods () - { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy")!; + Assert.NotEmpty (proxy.NativeRegistrations); - // Every UCO method should have a matching registration - foreach (var uco in proxy.UcoMethods) { - var reg = proxy.NativeRegistrations.FirstOrDefault (r => r.WrapperMethodName == uco.WrapperName); - Assert.NotNull (reg); - Assert.Equal (uco.JniSignature, reg!.JniSignature); + // Should register n_OnCreate + var onCreateReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_OnCreate"); + Assert.NotNull (onCreateReg); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateReg!.JniSignature); } - } - // ---- CustomView: registered constructors ---- + [Fact] + public void Fixture_MyHelper_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MyHelper"); + Assert.False (peer.DoNotGenerateAcw); - [Fact] - public void Fixture_CustomView_HasTwoConstructorWrappers () - { - var peer = FindFixtureByJavaName ("my/app/CustomView"); - Assert.Equal (2, peer.JavaConstructors.Count); + var model = BuildModel (new [] { peer }, "TypeMap"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); - Assert.NotNull (proxy); + // MyHelper has marshal methods and is not DoNotGenerateAcw + // Whether it's ACW depends on: not interface, has marshal methods, not DoNotGenerateAcw + if (peer.MarshalMethods.Count > 0 && peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_MyHelper_Proxy"); + Assert.NotNull (proxy); + } + } - if (proxy!.IsAcw) { - Assert.Equal (2, proxy.UcoConstructors.Count); - Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); - Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName); - Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); - Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); + } - // Constructor JNI signatures should be propagated - Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature); - Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature); + public class FixtureTouchHandler + { - // Constructor registrations must use the actual JNI signatures - var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); - Assert.Equal (2, ctorRegs.Count); - Assert.Equal ("()V", ctorRegs [0].JniSignature); - Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + [Fact] + public void Fixture_TouchHandler_AllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy"); + Assert.NotNull (proxy); + + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count); + + // onTouch: (Landroid/view/View;I)Z + var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch"); + Assert.NotNull (onTouchUco); + Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature); + + // onFocusChange: (Landroid/view/View;Z)V + var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange"); + Assert.NotNull (onFocusUco); + Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature); + + // onScroll: (IFJD)V + var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll"); + Assert.NotNull (onScrollUco); + Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature); + + // getText: ()Ljava/lang/String; + var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText"); + Assert.NotNull (getTextUco); + Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature); + + // setItems: ([Ljava/lang/String;)V + var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems"); + Assert.NotNull (setItemsUco); + Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature); } - } - // ---- Interface types ---- + [Fact] + public void Fixture_TouchHandler_NativeRegistrationsMatchUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy")!; + + // Every UCO method should have a matching registration + foreach (var uco in proxy.UcoMethods) { + var reg = proxy.NativeRegistrations.FirstOrDefault (r => r.WrapperMethodName == uco.WrapperName); + Assert.NotNull (reg); + Assert.Equal (uco.JniSignature, reg!.JniSignature); + } + } - [Fact] - public void Fixture_IOnClickListener_HasInvokerProxy () - { - var peers = ScanFixtures (); - var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); - Assert.NotNull (listener); - Assert.True (listener!.IsInterface); - Assert.NotNull (listener.InvokerTypeName); - - var model = BuildModel (new [] { listener }, "TypeMap"); - var proxy = model.ProxyTypes.FirstOrDefault (); - Assert.NotNull (proxy); - Assert.NotNull (proxy!.InvokerType); - Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - [Fact] - public void Fixture_IOnClickListener_IsNotAcw () + public class FixtureCustomView { - var peers = ScanFixtures (); - var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); - Assert.NotNull (listener); - var model = BuildModel (new [] { listener! }, "TypeMap"); + [Fact] + public void Fixture_CustomView_HasTwoConstructorWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + Assert.Equal (2, peer.JavaConstructors.Count); - // Interface → not ACW even though it has marshal methods - foreach (var proxy in model.ProxyTypes) { - Assert.False (proxy.IsAcw); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); + Assert.NotNull (proxy); + + if (proxy!.IsAcw) { + Assert.Equal (2, proxy.UcoConstructors.Count); + Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); + Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); + + // Constructor JNI signatures should be propagated + Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature); + + // Constructor registrations must use the actual JNI signatures + var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); + Assert.Equal (2, ctorRegs.Count); + Assert.Equal ("()V", ctorRegs [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature); + } } - } - // ---- Nested types ---- + } - [Fact] - public void Fixture_OuterInner_ProxyNaming () + public class FixtureInterfaces { - var peer = FindFixtureByJavaName ("my/app/Outer$Inner"); - var model = BuildModel (new [] { peer }, "TypeMap"); - // . and + get replaced with _ - var entry = FindEntry (model, "my/app/Outer$Inner"); - Assert.NotNull (entry); + [Fact] + public void Fixture_IOnClickListener_HasInvokerProxy () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + Assert.True (listener!.IsInterface); + Assert.NotNull (listener.InvokerTypeName); - if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "MyApp_Outer_Inner_Proxy"); + var model = BuildModel (new [] { listener }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); Assert.NotNull (proxy); - Assert.Equal ("MyApp.Outer+Inner", proxy!.TargetType.ManagedTypeName); + Assert.NotNull (proxy!.InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } + + [Fact] + public void Fixture_IOnClickListener_IsNotAcw () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + + var model = BuildModel (new [] { listener! }, "TypeMap"); + + // Interface → not ACW even though it has marshal methods + foreach (var proxy in model.ProxyTypes) { + Assert.False (proxy.IsAcw); + } + } + } - [Fact] - public void Fixture_ICallbackResult_ProxyNaming () + public class FixtureNestedTypes { - var peer = FindFixtureByJavaName ("my/app/ICallback$Result"); - var model = BuildModel (new [] { peer }, "TypeMap"); - var entry = FindEntry (model, "my/app/ICallback$Result"); - Assert.NotNull (entry); + [Fact] + public void Fixture_OuterInner_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/Outer$Inner"); + var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { - var proxy = FindProxy (model, "MyApp_ICallback_Result_Proxy"); - Assert.NotNull (proxy); - Assert.Equal ("MyApp.ICallback+Result", proxy!.TargetType.ManagedTypeName); + // . and + get replaced with _ + var entry = FindEntry (model, "my/app/Outer$Inner"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_Outer_Inner_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.Outer+Inner", proxy!.TargetType.ManagedTypeName); + } } - } - // ---- Duplicate JNI names across interface + invoker ---- + [Fact] + public void Fixture_ICallbackResult_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/ICallback$Result"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var entry = FindEntry (model, "my/app/ICallback$Result"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_ICallback_Result_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.ICallback+Result", proxy!.TargetType.ManagedTypeName); + } + } - [Fact] - public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () - { - var peers = ScanFixtures (); - // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" - var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); - Assert.Equal (2, clickPeers.Count); - - var model = BuildModel (clickPeers, "TypeMap"); - - // Invoker is excluded entirely — no TypeMap entry, no proxy. - // Only the interface gets a TypeMap entry and a proxy. - Assert.Single (model.Entries); - Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); - - // Only the interface proxy exists; the invoker type is referenced - // only as a TypeRef in the interface proxy's InvokerType property. - Assert.Single (model.ProxyTypes); - Assert.NotNull (model.ProxyTypes [0].InvokerType); - Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); } - [Fact] - public void Build_InvokerType_NoProxyNoEntry () + public class FixtureInvokers { - // Invoker types should never get their own proxy or TypeMap entry. - // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. - var ifacePeer = new JavaPeerInfo { - JavaName = "my/app/IFoo", - ManagedTypeName = "MyApp.IFoo", - AssemblyName = "App", - IsInterface = true, - InvokerTypeName = "MyApp.FooInvoker", - }; - var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App"); - invokerPeer.DoNotGenerateAcw = true; - var model = BuildModel (new [] { ifacePeer, invokerPeer }); + [Fact] + public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () + { + var peers = ScanFixtures (); + // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" + var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickPeers.Count); - // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy - Assert.Single (model.Entries); - Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference); + var model = BuildModel (clickPeers, "TypeMap"); - // Only the interface gets a proxy — the invoker is referenced, not proxied - Assert.Single (model.ProxyTypes); - var proxy = model.ProxyTypes [0]; - Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName); - Assert.NotNull (proxy.InvokerType); - Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName); + // Invoker is excluded entirely — no TypeMap entry, no proxy. + // Only the interface gets a TypeMap entry and a proxy. + Assert.Single (model.Entries); + Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); - // Interface proxy has activation because it will create the invoker - Assert.True (proxy.HasActivation); - } + // Only the interface proxy exists; the invoker type is referenced + // only as a TypeRef in the interface proxy's InvokerType property. + Assert.Single (model.ProxyTypes); + Assert.NotNull (model.ProxyTypes [0].InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); + } - // ---- GenericHolder ---- + [Fact] + public void Build_InvokerType_NoProxyNoEntry () + { + // Invoker types should never get their own proxy or TypeMap entry. + // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/IFoo", + ManagedTypeName = "MyApp.IFoo", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.FooInvoker", + }; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; + + var model = BuildModel (new [] { ifacePeer, invokerPeer }); + + // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy + Assert.Single (model.Entries); + Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference); + + // Only the interface gets a proxy — the invoker is referenced, not proxied + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName); + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName); - [Fact] - public void Fixture_GenericHolder_Entry () - { - var peer = FindFixtureByJavaName ("my/app/GenericHolder"); - Assert.True (peer.IsGenericDefinition); + // Interface proxy has activation because it will create the invoker + Assert.True (proxy.HasActivation); + } - var model = BuildModel (new [] { peer }, "TypeMap"); - var entry = FindEntry (model, "my/app/GenericHolder"); - Assert.NotNull (entry); } - // ---- AbstractBase ---- - - [Fact] - public void Fixture_AbstractBase_IsAcw () + public class FixtureGenericHolder { - var peer = FindFixtureByJavaName ("my/app/AbstractBase"); - Assert.True (peer.IsAbstract); - Assert.False (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_GenericHolder_Entry () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + Assert.True (peer.IsGenericDefinition); - // AbstractBase has marshal methods (doWork) and activation ctor - if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_AbstractBase_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.IsAcw); + var model = BuildModel (new [] { peer }, "TypeMap"); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); } - } - // ---- ClickableView: implements interface ---- + } - [Fact] - public void Fixture_ClickableView_IsAcw () + public class FixtureAbstractBase { - var peer = FindFixtureByJavaName ("my/app/ClickableView"); - Assert.False (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_AbstractBase_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/AbstractBase"); + Assert.True (peer.IsAbstract); + Assert.False (peer.DoNotGenerateAcw); - if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); - Assert.NotNull (proxy); - Assert.True (proxy!.IsAcw); - // Should have onClick UCO wrapper - var onClick = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); - Assert.NotNull (onClick); - Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // AbstractBase has marshal methods (doWork) and activation ctor + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_AbstractBase_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + } } - } - // ---- MultiInterfaceView ---- + } - [Fact] - public void Fixture_MultiInterfaceView_HasAllUcoMethods () + public class FixtureClickableView { - var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); - Assert.False (peer.DoNotGenerateAcw); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_ClickableView_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ClickableView"); + Assert.False (peer.DoNotGenerateAcw); - if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); - Assert.NotNull (proxy); + var model = BuildModel (new [] { peer }, "TypeMap"); - // Should have onClick and onLongClick UCO wrappers - Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); - Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + // Should have onClick UCO wrapper + var onClick = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + } } - } - // ---- ExportExample ---- + } - [Fact] - public void Fixture_ExportExample_IsAcw () + public class FixtureMultiInterfaceView { - var peer = FindFixtureByJavaName ("my/app/ExportExample"); - Assert.False (peer.DoNotGenerateAcw); - Assert.Single (peer.MarshalMethods); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_MultiInterfaceView_HasAllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); + Assert.False (peer.DoNotGenerateAcw); - if (peer.ActivationCtor != null) { - var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); - Assert.NotNull (proxy); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); + Assert.NotNull (proxy); + + // Should have onClick and onLongClick UCO wrappers + Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); + Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); + } } - } - // ---- Implementor types ---- + } - [Fact] - public void Fixture_Implementor_IsTrimmable_NotUnconditional () + public class FixtureExportExample { - var peer = FindFixtureByJavaName ("android/view/View_IOnClickListenerImplementor"); - Assert.False (peer.DoNotGenerateAcw); - Assert.False (peer.IsInterface); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_ExportExample_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + Assert.False (peer.DoNotGenerateAcw); + Assert.Single (peer.MarshalMethods); - // Implementor types should be trimmable (3-arg), NOT unconditional - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - Assert.False (entry!.IsUnconditional, "Implementor should NOT be unconditional"); - Assert.NotNull (entry.TargetTypeReference); - } + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); + Assert.NotNull (proxy); + } + } - // ---- EventDispatcher types ---- + } - [Fact] - public void Fixture_EventDispatcher_IsTrimmable_NotUnconditional () + public class FixtureImplementors { - var peer = FindFixtureByJavaName ("android/view/View_ClickEventDispatcher"); - Assert.False (peer.DoNotGenerateAcw); - Assert.False (peer.IsInterface); - var model = BuildModel (new [] { peer }, "TypeMap"); + [Fact] + public void Fixture_Implementor_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_IOnClickListenerImplementor"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); - // EventDispatcher types should be trimmable (3-arg), NOT unconditional - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - Assert.False (entry!.IsUnconditional, "EventDispatcher should NOT be unconditional"); - Assert.NotNull (entry.TargetTypeReference); - } + var model = BuildModel (new [] { peer }, "TypeMap"); - // ---- Name-based detection edge cases ---- + // Implementor types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "Implementor should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } - [Fact] - public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () - { - // Limitation: name-based heuristic means a user type ending in "Implementor" - // will be treated as trimmable even if it's genuinely a user ACW type. - // This test documents the known behavior. - var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); - var model = BuildModel (new [] { peer }); - - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - // The heuristic treats this as an Implementor → trimmable (not unconditional) - Assert.False (entry!.IsUnconditional, - "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); } - [Fact] - public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () + public class FixtureEventDispatchers { - // A type is only treated as an invoker when another peer's InvokerTypeName references it. - // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. - var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); - invokerPeer.DoNotGenerateAcw = true; - - // Without a referencing peer, it gets a normal entry - var model1 = BuildModel (new [] { invokerPeer }); - Assert.Single (model1.Entries); - - // When an interface references it as invoker, it is excluded - var ifacePeer = new JavaPeerInfo { - JavaName = "my/app/MyInvoker", - ManagedTypeName = "MyApp.IMyInterface", - AssemblyName = "App", - IsInterface = true, - InvokerTypeName = "MyApp.MyInvoker", - }; - var model2 = BuildModel (new [] { ifacePeer, invokerPeer }); - // Only the interface gets entries/proxies, the invoker is excluded - Assert.Single (model2.Entries); - Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); - } - // ---- Full pipeline: scan → model → emit → read back ---- + [Fact] + public void Fixture_EventDispatcher_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_ClickEventDispatcher"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); - [Fact] - public void FullPipeline_AllFixtures_ProducesLoadableAssembly () - { - var peers = ScanFixtures (); - var model = BuildModel (peers, "FullPipeline"); - - var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - Assert.True (File.Exists (outputPath)); - using var pe = new PEReader (File.OpenRead (outputPath)); - Assert.True (pe.HasMetadata); - - var reader = pe.GetMetadataReader (); - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name)); - - // Verify proxy types are present - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count); - - // Verify all proxy type names match - var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList (); - var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList (); - Assert.Equal (modelNames, proxyNames); - } finally { - var dir = Path.GetDirectoryName (outputPath); - if (dir != null && Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } + var model = BuildModel (new [] { peer }, "TypeMap"); + + // EventDispatcher types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "EventDispatcher should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); } + } - [Fact] - public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () + public class NameBasedDetection { - var peers = ScanFixtures (); - var model = BuildModel (peers, "AttrCount"); - - var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); + [Fact] + public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + { + // Limitation: name-based heuristic means a user type ending in "Implementor" + // will be treated as trimmable even if it's genuinely a user ACW type. + // This test documents the known behavior. + var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); + var model = BuildModel (new [] { peer }); - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - int totalAttrs = asmAttrs.Count (); + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + // The heuristic treats this as an Implementor → trimmable (not unconditional) + Assert.False (entry!.IsUnconditional, + "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); + } - // Assembly attrs = TypeMap entries + IgnoresAccessChecksTo entries - int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; - Assert.Equal (expected, totalAttrs); - } finally { - var dir = Path.GetDirectoryName (outputPath); - if (dir != null && Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } + [Fact] + public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () + { + // A type is only treated as an invoker when another peer's InvokerTypeName references it. + // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; + + // Without a referencing peer, it gets a normal entry + var model1 = BuildModel (new [] { invokerPeer }); + Assert.Single (model1.Entries); + + // When an interface references it as invoker, it is excluded + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/MyInvoker", + ManagedTypeName = "MyApp.IMyInterface", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.MyInvoker", + }; + var model2 = BuildModel (new [] { ifacePeer, invokerPeer }); + // Only the interface gets entries/proxies, the invoker is excluded + Assert.Single (model2.Entries); + Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); } + } - [Fact] - public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + public class PipelineTests { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); - var model = BuildModel (new [] { peer }, "UcoAttrTest"); - var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); + [Fact] + public void FullPipeline_AllFixtures_ProducesLoadableAssembly () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "FullPipeline"); - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); + var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); - var proxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); + Assert.True (File.Exists (outputPath)); + using var pe = new PEReader (File.OpenRead (outputPath)); + Assert.True (pe.HasMetadata); - var methods = proxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .ToList (); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name)); - var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList (); - Assert.NotEmpty (ucoMethods); + // Verify proxy types are present + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count); - // Each UCO method should have [UnmanagedCallersOnly] - foreach (var uco in ucoMethods) { - var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); - Assert.NotEmpty (attrs); + // Verify all proxy type names match + var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList (); + var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList (); + Assert.Equal (modelNames, proxyNames); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } } - } finally { - var dir = Path.GetDirectoryName (outputPath); - if (dir != null && Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } } - } - [Fact] - public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () - { - var peer = FindFixtureByJavaName ("my/app/CustomView"); - var model = BuildModel (new [] { peer }, "CtorTest"); + [Fact] + public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "AttrCount"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + + // Assembly attrs = TypeMap entries + IgnoresAccessChecksTo entries + int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "UcoAttrTest"); - var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); + var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); - var proxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy"); - var methodNames = proxy.GetMethods () - .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) - .ToList (); + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); - Assert.Contains (".ctor", methodNames); - Assert.Contains ("CreateInstance", methodNames); - Assert.Contains ("get_TargetType", methodNames); + var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList (); + Assert.NotEmpty (ucoMethods); - if (model.ProxyTypes [0].IsAcw) { - Assert.Contains ("RegisterNatives", methodNames); - Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + // Each UCO method should have [UnmanagedCallersOnly] + foreach (var uco in ucoMethods) { + var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); + Assert.NotEmpty (attrs); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } } - } finally { - var dir = Path.GetDirectoryName (outputPath); - if (dir != null && Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } } - } - [Fact] - public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () - { - var peer = FindFixtureByJavaName ("my/app/CustomView"); - var model = BuildModel (new [] { peer }, "CtorSigTest"); - - var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); - - var proxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); - - // Find UCO constructor wrappers (nctor_*_uco) - var ucoCtors = proxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco")) - .ToList (); - - Assert.NotEmpty (ucoCtors); - foreach (var uco in ucoCtors) { - // UCO constructor wrappers always take exactly 2 params (IntPtr jnienv, IntPtr self) - var sig = reader.GetBlobReader (uco.Signature); - var header = sig.ReadSignatureHeader (); - int paramCount = sig.ReadCompressedInteger (); - Assert.Equal (2, paramCount); + [Fact] + public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + var methodNames = proxy.GetMethods () + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); + + Assert.Contains (".ctor", methodNames); + Assert.Contains ("CreateInstance", methodNames); + Assert.Contains ("get_TargetType", methodNames); + + if (model.ProxyTypes [0].IsAcw) { + Assert.Contains ("RegisterNatives", methodNames); + Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } } - } finally { - CleanUpDir (outputPath); } + + [Fact] + public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorSigTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + // Find UCO constructor wrappers (nctor_*_uco) + var ucoCtors = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco")) + .ToList (); + + Assert.NotEmpty (ucoCtors); + foreach (var uco in ucoCtors) { + // UCO constructor wrappers always take exactly 2 params (IntPtr jnienv, IntPtr self) + var sig = reader.GetBlobReader (uco.Signature); + var header = sig.ReadSignatureHeader (); + int paramCount = sig.ReadCompressedInteger (); + Assert.Equal (2, paramCount); + } + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_GenericHolder_ProducesValidAssembly () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + var model = BuildModel (new [] { peer }, "GenericTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + // Verify the assembly is loadable and has entries + Assert.True (pe.HasMetadata); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + + // Verify assembly attributes were emitted + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (asmAttrs); + } finally { + CleanUpDir (outputPath); + } + } + } - [Fact] - public void FullPipeline_GenericHolder_ProducesValidAssembly () + public class PeBlobValidation { - var peer = FindFixtureByJavaName ("my/app/GenericHolder"); - var model = BuildModel (new [] { peer }, "GenericTest"); - var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); + [Fact] + public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () + { + // java/lang/Object → essential → 2-arg unconditional + var objectPeer = FindFixtureByJavaName ("java/lang/Object"); + // android/app/Activity → MCW → 3-arg trimmable + var activityPeer = FindFixtureByJavaName ("android/app/Activity"); - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); + var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); + Assert.Equal (2, model.Entries.Count); - // Verify the assembly is loadable and has entries - Assert.True (pe.HasMetadata); - var entry = FindEntry (model, "my/app/GenericHolder"); - Assert.NotNull (entry); + var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); - // Verify assembly attributes were emitted - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.NotEmpty (asmAttrs); - } finally { - CleanUpDir (outputPath); - } - } + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); - // ---- PE blob validation: 2-arg vs 3-arg TypeMap attributes ---- + var attrs = ReadAllTypeMapAttributeBlobs (reader); + Assert.Equal (2, attrs.Count); - [Fact] - public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () - { - // java/lang/Object → essential → 2-arg unconditional - var objectPeer = FindFixtureByJavaName ("java/lang/Object"); - // android/app/Activity → MCW → 3-arg trimmable - var activityPeer = FindFixtureByJavaName ("android/app/Activity"); - - var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); - Assert.Equal (2, model.Entries.Count); - - var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); - - var attrs = ReadAllTypeMapAttributeBlobs (reader); - Assert.Equal (2, attrs.Count); - - // Find the 2-arg (unconditional) entry - var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); - Assert.NotNull (unconditional.jniName); - Assert.Null (unconditional.targetRef); // 2-arg: no target - - // Find the 3-arg (trimmable) entry - var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); - Assert.NotNull (trimmable.jniName); - Assert.NotNull (trimmable.targetRef); // 3-arg: has target - Assert.Contains ("Android.App.Activity", trimmable.targetRef!); - } finally { - CleanUpDir (outputPath); + // Find the 2-arg (unconditional) entry + var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); + Assert.NotNull (unconditional.jniName); + Assert.Null (unconditional.targetRef); // 2-arg: no target + + // Find the 3-arg (trimmable) entry + var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); + Assert.NotNull (trimmable.jniName); + Assert.NotNull (trimmable.targetRef); // 3-arg: has target + Assert.Contains ("Android.App.Activity", trimmable.targetRef!); + } finally { + CleanUpDir (outputPath); + } } - } - [Fact] - public void FullPipeline_EssentialType_Emits2ArgAttribute () - { - // java/lang/Object is essential → unconditional 2-arg attribute - var peer = FindFixtureByJavaName ("java/lang/Object"); - var model = BuildModel (new [] { peer }, "Blob2Arg"); - Assert.Single (model.Entries); - Assert.True (model.Entries [0].IsUnconditional); - - var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); - var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); - - Assert.Equal ("java/lang/Object", jniName); - Assert.NotNull (proxyRef); - Assert.Contains ("Java_Lang_Object_Proxy", proxyRef!); - // 2-arg: no target type - Assert.Null (targetRef); - } finally { - CleanUpDir (outputPath); + [Fact] + public void FullPipeline_EssentialType_Emits2ArgAttribute () + { + // java/lang/Object is essential → unconditional 2-arg attribute + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "Blob2Arg"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("java/lang/Object", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("Java_Lang_Object_Proxy", proxyRef!); + // 2-arg: no target type + Assert.Null (targetRef); + } finally { + CleanUpDir (outputPath); + } } - } - [Fact] - public void FullPipeline_McwBinding_Emits3ArgAttribute () - { - // android/app/Activity is MCW → trimmable 3-arg attribute - var peer = FindFixtureByJavaName ("android/app/Activity"); - var model = BuildModel (new [] { peer }, "Blob3Arg"); - Assert.Single (model.Entries); - Assert.False (model.Entries [0].IsUnconditional); - - var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); - var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); - - Assert.Equal ("android/app/Activity", jniName); - Assert.NotNull (proxyRef); - Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); - // 3-arg: has target type - Assert.NotNull (targetRef); - Assert.Contains ("Android.App.Activity", targetRef!); - } finally { - CleanUpDir (outputPath); + [Fact] + public void FullPipeline_McwBinding_Emits3ArgAttribute () + { + // android/app/Activity is MCW → trimmable 3-arg attribute + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "Blob3Arg"); + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("android/app/Activity", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); + // 3-arg: has target type + Assert.NotNull (targetRef); + Assert.Contains ("Android.App.Activity", targetRef!); + } finally { + CleanUpDir (outputPath); + } } - } - [Fact] - public void FullPipeline_UserAcw_Emits2ArgAttribute () - { - // my/app/MainActivity is user ACW → unconditional 2-arg - var peer = FindFixtureByJavaName ("my/app/MainActivity"); - var model = BuildModel (new [] { peer }, "BlobAcw"); - Assert.Single (model.Entries); - Assert.True (model.Entries [0].IsUnconditional); - - var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); - try { - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - - using var pe = new PEReader (File.OpenRead (outputPath)); - var reader = pe.GetMetadataReader (); - var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); - - Assert.Equal ("my/app/MainActivity", jniName); - Assert.Null (targetRef); // unconditional → no target - } finally { - CleanUpDir (outputPath); + [Fact] + public void FullPipeline_UserAcw_Emits2ArgAttribute () + { + // my/app/MainActivity is user ACW → unconditional 2-arg + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "BlobAcw"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("my/app/MainActivity", jniName); + Assert.Null (targetRef); // unconditional → no target + } finally { + CleanUpDir (outputPath); + } } - } - // ---- Determinism ---- + } - [Fact] - public void Build_SameInput_ProducesDeterministicOutput () + public class DeterminismTests { - var peers = ScanFixtures (); - var model1 = BuildModel (peers, "DetTest"); - var model2 = BuildModel (peers, "DetTest"); + [Fact] + public void Build_SameInput_ProducesDeterministicOutput () + { + var peers = ScanFixtures (); + + var model1 = BuildModel (peers, "DetTest"); + var model2 = BuildModel (peers, "DetTest"); - Assert.Equal (model1.Entries.Count, model2.Entries.Count); - for (int i = 0; i < model1.Entries.Count; i++) { - Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); - Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); - Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); + Assert.Equal (model1.Entries.Count, model2.Entries.Count); + for (int i = 0; i < model1.Entries.Count; i++) { + Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); + Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); + Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); + } } - } - // ---- Blob reading helpers ---- + } /// /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef). @@ -1743,4 +1826,4 @@ static void CleanUpDir (string path) if (dir != null && Directory.Exists (dir)) try { Directory.Delete (dir, true); } catch { } } -} +} \ No newline at end of file From 6a16b6ac1da698efe1ab0799824cc59de5616787 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 13:33:51 +0100 Subject: [PATCH 22/43] Move Generator files to Microsoft.Android.Sdk.TrimmableTypeMap namespace After rebasing onto the renamed base branch, Generator source and test files landed under the old Microsoft.Android.Build.TypeMap paths. Move them to the correct Microsoft.Android.Sdk.TrimmableTypeMap directories and update all namespace references. --- .../Generator/JcwJavaSourceGenerator.cs | 2 +- .../Generator/JniSignatureHelper.cs | 2 +- .../Generator/Model/TypeMapAssemblyData.cs | 2 +- .../Generator/ModelBuilder.cs | 2 +- .../Generator/RootTypeMapAssemblyGenerator.cs | 2 +- .../Generator/TypeMapAssemblyEmitter.cs | 2 +- .../Generator/TypeMapAssemblyGenerator.cs | 2 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 2 +- .../Generator/RootTypeMapAssemblyGeneratorTests.cs | 2 +- .../Generator/TypeMapAssemblyGeneratorTests.cs | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/JcwJavaSourceGenerator.cs (99%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/JniSignatureHelper.cs (98%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/Model/TypeMapAssemblyData.cs (99%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/ModelBuilder.cs (99%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/RootTypeMapAssemblyGenerator.cs (99%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/TypeMapAssemblyEmitter.cs (99%) rename src/{Microsoft.Android.Build.TypeMap => Microsoft.Android.Sdk.TrimmableTypeMap}/Generator/TypeMapAssemblyGenerator.cs (96%) rename tests/{Microsoft.Android.Build.TypeMap.Tests => Microsoft.Android.Sdk.TrimmableTypeMap.Tests}/Generator/JcwJavaSourceGeneratorTests.cs (99%) rename tests/{Microsoft.Android.Build.TypeMap.Tests => Microsoft.Android.Sdk.TrimmableTypeMap.Tests}/Generator/RootTypeMapAssemblyGeneratorTests.cs (99%) rename tests/{Microsoft.Android.Build.TypeMap.Tests => Microsoft.Android.Sdk.TrimmableTypeMap.Tests}/Generator/TypeMapAssemblyGeneratorTests.cs (99%) rename tests/{Microsoft.Android.Build.TypeMap.Tests => Microsoft.Android.Sdk.TrimmableTypeMap.Tests}/Generator/TypeMapModelBuilderTests.cs (99%) diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs similarity index 99% rename from src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index b80dea6c1c6..9d94f1a3781 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Generates JCW (Java Callable Wrapper) .java source files from scanned records. diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs similarity index 98% rename from src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index b0ce11bdd0d..53733f69d14 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -3,7 +3,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// JNI primitive type kinds used for mapping JNI signatures → CLR types. enum JniParamKind diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs similarity index 99% rename from src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 214264edfaf..558de0f6014 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Data model for a single TypeMap output assembly. diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs similarity index 99% rename from src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index fa82092d170..28e8b5c38f6 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Builds a from scanned records. diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs similarity index 99% rename from src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index f9854ff8192..7d74767aa50 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -6,7 +6,7 @@ using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs similarity index 99% rename from src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index a2c9cd9589a..6df06f86ecc 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -6,7 +6,7 @@ using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Emits a TypeMap PE assembly from a . diff --git a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs similarity index 96% rename from src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs rename to src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 0373cd16ffe..927346fbf10 100644 --- a/src/Microsoft.Android.Build.TypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.Android.Build.TypeMap; +namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// High-level API: builds the model from peers, then emits the PE assembly. diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs similarity index 99% rename from tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs rename to tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 2d7bf0e375c..bed5cb07eeb 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -4,7 +4,7 @@ using System.Linq; using Xunit; -namespace Microsoft.Android.Build.TypeMap.Tests; +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class JcwJavaSourceGeneratorTests { diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs similarity index 99% rename from tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs rename to tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index bd376c46114..c63801bc8e8 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -6,7 +6,7 @@ using System.Reflection.PortableExecutable; using Xunit; -namespace Microsoft.Android.Build.TypeMap.Tests; +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests { diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs similarity index 99% rename from tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs rename to tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index ec361b15341..46eb3c3474c 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -7,7 +7,7 @@ using System.Reflection.PortableExecutable; using Xunit; -namespace Microsoft.Android.Build.TypeMap.Tests; +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests { diff --git a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs similarity index 99% rename from tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs rename to tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 8bec374aa34..d328c57b163 100644 --- a/tests/Microsoft.Android.Build.TypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -6,7 +6,7 @@ using System.Reflection.PortableExecutable; using Xunit; -namespace Microsoft.Android.Build.TypeMap.Tests; +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class ModelBuilderTests { From da5ae5845f586c6576606e8d8f34dc4111103abb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 14:09:27 +0100 Subject: [PATCH 23/43] Fix CreateInstance to use proper activation patterns, add TypeMapAssociation, fix IgnoresAccessChecksTo - Replace CreateManagedPeer with spec-compliant CreateInstance: - Leaf ctor: newobj T::.ctor(IntPtr, JniHandleOwnership) - Inherited ctor: GetUninitializedObject + call Base::.ctor - Interface: newobj TInvoker::.ctor(IntPtr, JniHandleOwnership) - Generic: throw new NotSupportedException() - Add TypeMapAssociationAttribute emission for alias groups - Include base ctor assembly in IgnoresAccessChecksTo - Add ActivationCtorData and IsGenericDefinition to proxy model - Add 5 new tests covering all CreateInstance paths, TypeMapAssociation, and IgnoresAccessChecksTo for inherited ctors --- .../Generator/Model/TypeMapAssemblyData.cs | 40 ++++ .../Generator/ModelBuilder.cs | 33 +++ .../Generator/TypeMapAssemblyEmitter.cs | 177 +++++++++++++--- .../TypeMapAssemblyGeneratorTests.cs | 192 +++++++++++++++++- 4 files changed, 412 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 558de0f6014..df18be1bab3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -22,6 +22,9 @@ sealed class TypeMapAssemblyData /// Proxy types to emit in the assembly. public List ProxyTypes { get; } = new (); + /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name). + public List Associations { get; } = new (); + /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. public List IgnoresAccessChecksTo { get; } = new (); } @@ -75,6 +78,15 @@ sealed class JavaPeerProxyData /// Whether this proxy has a CreateInstance that can actually create instances (has activation ctor). public bool HasActivation { get; set; } + /// + /// Activation constructor details. Determines how CreateInstance instantiates the managed peer. + /// Null when HasActivation is false. + /// + public ActivationCtorData? ActivationCtor { get; set; } + + /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. + public bool IsGenericDefinition { get; set; } + /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers). public bool IsAcw { get; set; } @@ -152,3 +164,31 @@ sealed class NativeRegistrationData /// Name of the UCO wrapper method whose function pointer to register. public string WrapperMethodName { get; set; } = ""; } + +/// +/// Describes how the proxy's CreateInstance should construct the managed peer. +/// +sealed class ActivationCtorData +{ + /// Type that declares the activation constructor (may be a base type). + public TypeRefData DeclaringType { get; set; } = new (); + + /// True when the leaf type itself declares the activation ctor. + public bool IsOnLeafType { get; set; } + + /// The style of activation ctor (XamarinAndroid or JavaInterop). + public ActivationCtorStyle Style { get; set; } +} + +/// +/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry. +/// Links a managed type to the proxy that holds its alias TypeMap entry. +/// +sealed class TypeMapAssociationData +{ + /// Assembly-qualified source type reference (the managed alias type). + public string SourceTypeReference { get; set; } = ""; + + /// Assembly-qualified proxy type reference (the alias holder proxy). + public string AliasProxyTypeReference { get; set; } = ""; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 28e8b5c38f6..35def42827c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -94,6 +94,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } // Compute IgnoresAccessChecksTo from actual cross-assembly references in UCO callback types + // and from base class activation ctors that need direct invocation var referencedAssemblies = new SortedSet (StringComparer.Ordinal); foreach (var proxy in model.ProxyTypes) { foreach (var uco in proxy.UcoMethods) { @@ -104,6 +105,12 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (proxy.TargetType != null && !string.Equals (proxy.TargetType.AssemblyName, assemblyName, StringComparison.Ordinal)) { referencedAssemblies.Add (proxy.TargetType.AssemblyName); } + // When calling a protected base class ctor directly (inherited activation ctor), + // we need IgnoresAccessChecksTo for the assembly containing that ctor + if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType && + !string.Equals (proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName, StringComparison.Ordinal)) { + referencedAssemblies.Add (proxy.ActivationCtor.DeclaringType.AssemblyName); + } } model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); @@ -129,6 +136,7 @@ static void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, { // First peer is the "primary" — it gets the base JNI name entry. // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... + JavaPeerProxyData? primaryProxy = null; for (int i = 0; i < peersForName.Count; i++) { var peer = peersForName [i]; string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; @@ -142,7 +150,19 @@ static void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } + if (i == 0) { + primaryProxy = proxy; + } + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); + + // Emit TypeMapAssociation linking this alias type to the primary proxy + if (i > 0 && primaryProxy != null) { + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}", + AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}", + }); + } } } @@ -205,6 +225,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) }, HasActivation = peer.ActivationCtor != null || peer.InvokerTypeName != null, IsAcw = isAcw, + IsGenericDefinition = peer.IsGenericDefinition, }; if (peer.InvokerTypeName != null) { @@ -214,6 +235,18 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) }; } + if (peer.ActivationCtor != null) { + bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal); + proxy.ActivationCtor = new ActivationCtorData { + DeclaringType = new TypeRefData { + ManagedTypeName = peer.ActivationCtor.DeclaringTypeName, + AssemblyName = peer.ActivationCtor.DeclaringAssemblyName, + }, + IsOnLeafType = isOnLeaf, + Style = peer.ActivationCtor.Style, + }; + } + if (isAcw) { BuildUcoMethods (peer, proxy); BuildUcoConstructors (peer, proxy); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 6df06f86ecc..fb2f49926b5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -32,15 +32,19 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _trimmableNativeRegistrationRef; + TypeReferenceHandle _notSupportedExceptionRef; + TypeReferenceHandle _runtimeHelpersRef; MemberReferenceHandle _baseCtorRef; MemberReferenceHandle _getTypeFromHandleRef; - MemberReferenceHandle _createManagedPeerRef; + MemberReferenceHandle _getUninitializedObjectRef; + MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; + MemberReferenceHandle _typeMapAssociationAttrCtorRef; /// /// Creates a new emitter. @@ -94,6 +98,10 @@ public void Emit (TypeMapAssemblyData model, string outputPath) EmitTypeMapAttribute (metadata, entry); } + foreach (var assoc in model.Associations) { + EmitTypeMapAssociationAttribute (metadata, assoc); + } + EmitIgnoresAccessChecksToAttribute (metadata, ilBuilder, model.IgnoresAccessChecksTo); WritePE (metadata, ilBuilder, outputPath); } @@ -148,6 +156,10 @@ void EmitTypeReferences (MetadataBuilder metadata) metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); _trimmableNativeRegistrationRef = metadata.AddTypeReference (_monoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); + _notSupportedExceptionRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); + _runtimeHelpersRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); } void EmitMemberReferences (MetadataBuilder metadata) @@ -160,14 +172,15 @@ void EmitMemberReferences (MetadataBuilder metadata) rt => rt.Type ().Type (_systemTypeRef, false), p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true))); - _createManagedPeerRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "CreateManagedPeer", - 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); - })); + _getUninitializedObjectRef = AddMemberRef (metadata, _runtimeHelpersRef, "GetUninitializedObject", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Object (), + p => p.AddParameter ().Type ().Type (_systemTypeRef, false))); + + _notSupportedExceptionCtorRef = AddMemberRef (metadata, _notSupportedExceptionRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); _activateInstanceRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "ActivateInstance", sig => sig.MethodSignature ().Parameters (2, @@ -194,6 +207,7 @@ void EmitMemberReferences (MetadataBuilder metadata) sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); EmitTypeMapAttributeCtorRef (metadata); + EmitTypeMapAssociationAttributeCtorRef (metadata); } void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) @@ -233,6 +247,23 @@ void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) })); } + void EmitTypeMapAssociationAttributeCtorRef (MetadataBuilder metadata) + { + // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args: + // TypeMapAssociation(Type sourceType, Type aliasProxyType) + var typeMapAssociationAttrRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAssociationAttribute")); + + _typeMapAssociationAttrCtorRef = AddMemberRef (metadata, typeMapAssociationAttrRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + void EmitModuleType (MetadataBuilder metadata) { metadata.AddTypeDefinition ( @@ -318,29 +349,110 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe return; } - // For interface proxies with an invoker type, CreateInstance instantiates the invoker - // (e.g., IOnClickListenerInvoker), not the interface itself. For regular types, it - // instantiates the target type directly. - var activatedType = proxy.InvokerType ?? proxy.TargetType; - var activatedTypeRef = ResolveTypeRef (metadata, activatedType); + // Generic type definitions cannot be instantiated + if (proxy.IsGenericDefinition) { + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + encoder.LoadString (metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); + return; + } - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) + if (proxy.InvokerType != null) { + var invokerTypeRef = ResolveTypeRef (metadata, proxy.InvokerType); + var invokerCtorRef = AddActivationCtorRef (metadata, invokerTypeRef); + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (invokerCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + return; + } + + // Non-interface type with activation ctor + var targetTypeRef = ResolveTypeRef (metadata, proxy.TargetType); + + if (proxy.ActivationCtor != null && proxy.ActivationCtor.IsOnLeafType) { + // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) + var ctorRef = AddActivationCtorRef (metadata, targetTypeRef); + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Ret); + }); + } else if (proxy.ActivationCtor != null) { + // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) + var baseCtorTypeRef = ResolveTypeRef (metadata, proxy.ActivationCtor.DeclaringType); + var baseActivationCtorRef = AddActivationCtorRef (metadata, baseCtorTypeRef); + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + encoder => { + // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + // obj.Base::.ctor(handle, transfer) — direct call to inherited ctor + encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseActivationCtorRef); + + // return obj; + encoder.OpCode (ILOpCode.Ret); + }); + } + } + + MemberReferenceHandle AddActivationCtorRef (MetadataBuilder metadata, EntityHandle declaringTypeRef) + { + return AddMemberRef (metadata, declaringTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), + rt => rt.Void (), p => { p.AddParameter ().Type ().IntPtr (); p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (activatedTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_createManagedPeerRef); - encoder.OpCode (ILOpCode.Ret); - }); + })); } void EmitTypeGetter (MetadataBuilder metadata, BlobBuilder ilBuilder, string methodName, @@ -487,6 +599,17 @@ void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) } } + void EmitTypeMapAssociationAttribute (MetadataBuilder metadata, TypeMapAssociationData assoc) + { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (0x0001); // Prolog + attrBlob.WriteSerializedString (assoc.SourceTypeReference); + attrBlob.WriteSerializedString (assoc.AliasProxyTypeReference); + attrBlob.WriteUInt16 (0x0000); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, + metadata.GetOrAddBlob (attrBlob)); + } + // ---- IgnoresAccessChecksTo ---- void EmitIgnoresAccessChecksToAttribute (MetadataBuilder metadata, BlobBuilder ilBuilder, List assemblyNames) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 46eb3c3474c..c05557aa942 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using Xunit; @@ -329,6 +330,11 @@ public void Generate_DuplicateJniNames_CreatesAliasEntries () ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate1", AssemblyName = "TestAssembly", + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.Duplicate1", + DeclaringAssemblyName = "TestAssembly", + Style = ActivationCtorStyle.XamarinAndroid, + }, }, new JavaPeerInfo { JavaName = "test/Duplicate", @@ -343,10 +349,56 @@ public void Generate_DuplicateJniNames_CreatesAliasEntries () try { var (pe, reader) = OpenAssembly (path); using (pe) { - // Neither peer has activation ctor → no proxies, but both get entries var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - // Should have 2 TypeMap entries + IgnoresAccessChecksTo entries - Assert.True (assemblyAttrs.Count () >= 2); + // Should have 2 TypeMap entries + TypeMapAssociation + IgnoresAccessChecksTo entries + Assert.True (assemblyAttrs.Count () >= 3); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate1", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate1", + AssemblyName = "TestAssembly", + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.Duplicate1", + DeclaringAssemblyName = "TestAssembly", + Style = ActivationCtorStyle.XamarinAndroid, + }, + }, + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate2", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate2", + AssemblyName = "TestAssembly", + }, + }; + + var path = GenerateAssembly (peers, "AliasAssocTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Look for TypeMapAssociationAttribute references + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + // There should be a TypeMapAssociationAttribute ctor reference + var typeNames = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); } } finally { CleanUp (path); @@ -502,6 +554,140 @@ public void Generate_NullOutputPath_ThrowsArgumentNull () } + public class CreateInstancePaths + { + + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () + { + // SimpleActivity has no own activation ctor — inherits from Activity + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Verify RuntimeHelpers type is referenced (for GetUninitializedObject) + var typeNames = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + Assert.Contains ("RuntimeHelpers", typeNames); + + // Verify no CreateManagedPeer reference exists + var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () + { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + Assert.NotNull (clickableView.ActivationCtor); + Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Should NOT have CreateManagedPeer — leaf ctor uses direct newobj + var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + + // Should have a .ctor MemberRef for the target type (direct newobj) + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_GenericType_ThrowsNotSupportedException () + { + var peers = ScanFixtures (); + var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + + var path = GenerateAssembly (new [] { generic }, "GenericTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // NotSupportedException should be referenced + var typeNames = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + Assert.Contains ("NotSupportedException", typeNames); + } + } finally { + CleanUp (path); + } + } + + } + + public class IgnoresAccessChecksToForBaseCtor + { + + [Fact] + public void Generate_InheritedCtor_IncludesBaseCtorAssembly () + { + // SimpleActivity inherits activation ctor from Activity — both in TestFixtures + // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be + // in IgnoresAccessChecksTo + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + + var path = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + // Find the IgnoresAccessChecksToAttribute type + var ignoresAttrType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); + Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); + + // Check assembly-level custom attributes include the base ctor's assembly + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + var attrBlobs = new List (); + foreach (var attrHandle in assemblyAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobBytes (attr.Value); + var blobStr = System.Text.Encoding.UTF8.GetString (blob); + attrBlobs.Add (blobStr); + } + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); + } + } finally { + CleanUp (path); + } + } + + } + static void CleanUp (string path) { var dir = Path.GetDirectoryName (path); From 0b130acb3a6e7d9faa54f7bbd930750b859b4167 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 16:36:34 +0100 Subject: [PATCH 24/43] Simplify generator code - Make HasActivation a computed property (derived from ActivationCtor/InvokerType) - Merge EmitSinglePeer into EmitPeers (handles both single and alias cases) - Extract EmitCreateInstanceBody to deduplicate method signature across 5 paths - Make BuildEntry's jniName parameter required (no longer optional) --- .../Generator/Model/TypeMapAssemblyData.cs | 5 +- .../Generator/ModelBuilder.cs | 31 +--- .../Generator/TypeMapAssemblyEmitter.cs | 140 +++++++----------- 3 files changed, 61 insertions(+), 115 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index df18be1bab3..e2f6217b957 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -75,12 +75,11 @@ sealed class JavaPeerProxyData /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. public TypeRefData? InvokerType { get; set; } - /// Whether this proxy has a CreateInstance that can actually create instances (has activation ctor). - public bool HasActivation { get; set; } + /// Whether this proxy has a CreateInstance that can actually create instances. + public bool HasActivation => ActivationCtor != null || InvokerType != null; /// /// Activation constructor details. Determines how CreateInstance instantiates the managed peer. - /// Null when HasActivation is false. /// public ActivationCtorData? ActivationCtor { get; set; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 35def42827c..869070896d3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -85,12 +85,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - if (peersForName.Count == 1) { - var peer = peersForName [0]; - EmitSinglePeer (model, peer, assemblyName); - } else { - EmitAliasedPeers (model, jniName, peersForName, assemblyName); - } + EmitPeers (model, jniName, peersForName, assemblyName); } // Compute IgnoresAccessChecksTo from actual cross-assembly references in UCO callback types @@ -117,21 +112,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri return model; } - static void EmitSinglePeer (TypeMapAssemblyData model, JavaPeerInfo peer, string assemblyName) - { - bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; - bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; - - JavaPeerProxyData? proxy = null; - if (hasProxy) { - proxy = BuildProxyType (peer, isAcw); - model.ProxyTypes.Add (proxy); - } - - model.Entries.Add (BuildEntry (peer, proxy, assemblyName)); - } - - static void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, + static void EmitPeers (TypeMapAssemblyData model, string jniName, List peersForName, string assemblyName) { // First peer is the "primary" — it gets the base JNI name entry. @@ -156,7 +137,7 @@ static void EmitAliasedPeers (TypeMapAssemblyData model, string jniName, model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); - // Emit TypeMapAssociation linking this alias type to the primary proxy + // Emit TypeMapAssociation linking alias types to the primary proxy if (i > 0 && primaryProxy != null) { model.Associations.Add (new TypeMapAssociationData { SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}", @@ -223,7 +204,6 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, - HasActivation = peer.ActivationCtor != null || peer.InvokerTypeName != null, IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, }; @@ -322,7 +302,7 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, - string outputAssemblyName, string? overrideJniName = null) + string outputAssemblyName, string jniName) { string proxyRef; if (proxy != null) { @@ -334,12 +314,11 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr bool isUnconditional = IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { - // Trimmable: the trimmer will preserve the proxy only if the target type is referenced. targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; } return new TypeMapAttributeData { - JniName = overrideJniName ?? peer.JavaName, + JniName = jniName, ProxyTypeReference = proxyRef, TargetTypeReference = targetRef, }; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index fb2f49926b5..a9ca65cc7b6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -334,59 +334,34 @@ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerPro void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy) { if (!proxy.HasActivation) { - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - encoder.OpCode (ILOpCode.Ldnull); - encoder.OpCode (ILOpCode.Ret); - }); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); return; } // Generic type definitions cannot be instantiated if (proxy.IsGenericDefinition) { - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - encoder.LoadString (metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (_notSupportedExceptionCtorRef); - encoder.OpCode (ILOpCode.Throw); - }); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.LoadString (metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); return; } // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) if (proxy.InvokerType != null) { - var invokerTypeRef = ResolveTypeRef (metadata, proxy.InvokerType); - var invokerCtorRef = AddActivationCtorRef (metadata, invokerTypeRef); - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (invokerCtorRef); - encoder.OpCode (ILOpCode.Ret); - }); + var invokerCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, proxy.InvokerType)); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (invokerCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); return; } @@ -396,54 +371,47 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe if (proxy.ActivationCtor != null && proxy.ActivationCtor.IsOnLeafType) { // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) var ctorRef = AddActivationCtorRef (metadata, targetTypeRef); - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Ret); - }); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Ret); + }); } else if (proxy.ActivationCtor != null) { // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) - var baseCtorTypeRef = ResolveTypeRef (metadata, proxy.ActivationCtor.DeclaringType); - var baseActivationCtorRef = AddActivationCtorRef (metadata, baseCtorTypeRef); - EmitBody (metadata, ilBuilder, "CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - encoder => { - // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (targetTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); - - // obj.Base::.ctor(handle, transfer) — direct call to inherited ctor - encoder.OpCode (ILOpCode.Dup); - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.Call (baseActivationCtorRef); - - // return obj; - encoder.OpCode (ILOpCode.Ret); - }); + var baseActivationCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, proxy.ActivationCtor.DeclaringType)); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseActivationCtorRef); + + encoder.OpCode (ILOpCode.Ret); + }); } } + void EmitCreateInstanceBody (MetadataBuilder metadata, BlobBuilder ilBuilder, Action emitIL) + { + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + emitIL); + } + MemberReferenceHandle AddActivationCtorRef (MetadataBuilder metadata, EntityHandle declaringTypeRef) { return AddMemberRef (metadata, declaringTypeRef, ".ctor", From 8dc353be57efcb6ea30a882ff64e264e37d54e04 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 16:40:06 +0100 Subject: [PATCH 25/43] Further simplify generator code - Remove ImplementsIAndroidCallableWrapper (was always == IsAcw) - Collapse EmitTypeMapAttribute 2-arg/3-arg into shared blob writing - Extract duplicated UCO signature lambda in EmitUcoMethod - Simplify invoker filtering with LINQ - Extract AddIfCrossAssembly helper for IgnoresAccessChecksTo --- .../Generator/Model/TypeMapAssemblyData.cs | 5 +- .../Generator/ModelBuilder.cs | 50 +++++++---------- .../Generator/TypeMapAssemblyEmitter.cs | 55 +++++++------------ .../Generator/TypeMapModelBuilderTests.cs | 3 - 4 files changed, 40 insertions(+), 73 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index e2f6217b957..3c47cd31a1b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -86,12 +86,9 @@ sealed class JavaPeerProxyData /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. public bool IsGenericDefinition { get; set; } - /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers). + /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). public bool IsAcw { get; set; } - /// Implements IAndroidCallableWrapper when IsAcw is true. - public bool ImplementsIAndroidCallableWrapper => IsAcw; - /// UCO method wrappers for marshal methods (non-constructor). public List UcoMethods { get; } = new (); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 869070896d3..19ba83374ce 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -47,28 +47,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri ModuleName = moduleName, }; - // Build a set of invoker type names referenced by interfaces/abstract types via [Register]'s - // third argument. Invoker types are NOT emitted as separate proxies or TypeMap entries — + // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. - var invokerTypeNames = new HashSet (StringComparer.Ordinal); - foreach (var peer in peers) { - if (peer.InvokerTypeName != null) { - invokerTypeNames.Add (peer.InvokerTypeName); - } - } - - // Exclude invoker types from further processing — they don't get TypeMap entries or proxies. - var nonInvokerPeers = new List (); - foreach (var peer in peers) { - if (!invokerTypeNames.Contains (peer.ManagedTypeName)) { - nonInvokerPeers.Add (peer); - } - } + var invokerTypeNames = new HashSet ( + peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!), + StringComparer.Ordinal); // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). // Use an ordered dictionary to ensure deterministic output across runs. var groups = new SortedDictionary> (StringComparer.Ordinal); - foreach (var peer in nonInvokerPeers) { + foreach (var peer in peers) { + if (invokerTypeNames.Contains (peer.ManagedTypeName)) { + continue; + } if (!groups.TryGetValue (peer.JavaName, out var list)) { list = new List (); groups [peer.JavaName] = list; @@ -88,23 +79,15 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri EmitPeers (model, jniName, peersForName, assemblyName); } - // Compute IgnoresAccessChecksTo from actual cross-assembly references in UCO callback types - // and from base class activation ctors that need direct invocation + // Compute IgnoresAccessChecksTo from cross-assembly references var referencedAssemblies = new SortedSet (StringComparer.Ordinal); foreach (var proxy in model.ProxyTypes) { + AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName); foreach (var uco in proxy.UcoMethods) { - if (!string.Equals (uco.CallbackType.AssemblyName, assemblyName, StringComparison.Ordinal)) { - referencedAssemblies.Add (uco.CallbackType.AssemblyName); - } + AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName); } - if (proxy.TargetType != null && !string.Equals (proxy.TargetType.AssemblyName, assemblyName, StringComparison.Ordinal)) { - referencedAssemblies.Add (proxy.TargetType.AssemblyName); - } - // When calling a protected base class ctor directly (inherited activation ctor), - // we need IgnoresAccessChecksTo for the assembly containing that ctor - if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType && - !string.Equals (proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName, StringComparison.Ordinal)) { - referencedAssemblies.Add (proxy.ActivationCtor.DeclaringType.AssemblyName); + if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { + AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } } model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); @@ -192,6 +175,13 @@ static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal); } + static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName) + { + if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) { + set.Add (asmName); + } + } + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) { // Use managed type name for proxy naming to guarantee uniqueness across aliases diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index a9ca65cc7b6..0dafb0084a6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -287,7 +287,7 @@ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerPro MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - if (proxy.ImplementsIAndroidCallableWrapper) { + if (proxy.IsAcw) { metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef); } @@ -449,28 +449,21 @@ MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBu int paramCount = 2 + jniParams.Count; bool isVoid = returnKind == JniParamKind.Void; - // Callback method reference + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }); + var callbackTypeHandle = ResolveTypeRef (metadata, uco.CallbackType); - var callbackRef = AddMemberRef (metadata, callbackTypeHandle, uco.CallbackMethodName, - sig => sig.MethodSignature ().Parameters (paramCount, - rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().IntPtr (); - for (int j = 0; j < jniParams.Count; j++) - JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); - })); + var callbackRef = AddMemberRef (metadata, callbackTypeHandle, uco.CallbackMethodName, encodeSig); var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - 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]); - }), + encodeSig, encoder => { for (int p = 0; p < paramCount; p++) encoder.LoadArgument (p); @@ -544,27 +537,17 @@ void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) { - // Per ECMA-335 §II.23.3, System.Type-typed constructor arguments are encoded - // as SerString (assembly-qualified type name), not as TypeDefOrRef tokens. var attrBlob = new BlobBuilder (); attrBlob.WriteUInt16 (0x0001); // Prolog - - if (entry.IsUnconditional) { - // 2-arg: TypeMap(jniName, proxyType) — always preserved - attrBlob.WriteSerializedString (entry.JniName); - attrBlob.WriteSerializedString (entry.ProxyTypeReference); - attrBlob.WriteUInt16 (0x0000); // NumNamed - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef2Arg, - metadata.GetOrAddBlob (attrBlob)); - } else { - // 3-arg: TypeMap(jniName, proxyType, targetType) — trimmable - attrBlob.WriteSerializedString (entry.JniName); - attrBlob.WriteSerializedString (entry.ProxyTypeReference); + attrBlob.WriteSerializedString (entry.JniName); + attrBlob.WriteSerializedString (entry.ProxyTypeReference); + if (!entry.IsUnconditional) { attrBlob.WriteSerializedString (entry.TargetTypeReference!); - attrBlob.WriteUInt16 (0x0000); // NumNamed - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAttrCtorRef3Arg, - metadata.GetOrAddBlob (attrBlob)); } + attrBlob.WriteUInt16 (0x0000); // NumNamed + + var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (attrBlob)); } void EmitTypeMapAssociationAttribute (MetadataBuilder metadata, TypeMapAssociationData assoc) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index d328c57b163..41b19202bdd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -361,7 +361,6 @@ public void Build_AcwType_IsAcwTrue () Assert.Single (model.ProxyTypes); Assert.True (model.ProxyTypes [0].IsAcw); - Assert.True (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); } [Fact] @@ -372,7 +371,6 @@ public void Build_McwType_IsAcwFalse () Assert.Single (model.ProxyTypes); Assert.False (model.ProxyTypes [0].IsAcw); - Assert.False (model.ProxyTypes [0].ImplementsIAndroidCallableWrapper); } [Fact] @@ -938,7 +936,6 @@ public void Fixture_MainActivity_IsAcw () var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); Assert.NotNull (proxy); Assert.True (proxy!.IsAcw); - Assert.True (proxy.ImplementsIAndroidCallableWrapper); Assert.True (proxy.HasActivation); } From 5bb64d613b40d675bf956111be2e75ccaaad7729 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 16:41:33 +0100 Subject: [PATCH 26/43] Remove redundant null checks in EmitCreateInstance --- .../Generator/TypeMapAssemblyEmitter.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 0dafb0084a6..1e714c63609 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -365,10 +365,11 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe return; } - // Non-interface type with activation ctor + // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null) + var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null"); var targetTypeRef = ResolveTypeRef (metadata, proxy.TargetType); - if (proxy.ActivationCtor != null && proxy.ActivationCtor.IsOnLeafType) { + if (activationCtor.IsOnLeafType) { // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) var ctorRef = AddActivationCtorRef (metadata, targetTypeRef); EmitCreateInstanceBody (metadata, ilBuilder, encoder => { @@ -378,9 +379,9 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe encoder.Token (ctorRef); encoder.OpCode (ILOpCode.Ret); }); - } else if (proxy.ActivationCtor != null) { + } else { // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) - var baseActivationCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, proxy.ActivationCtor.DeclaringType)); + var baseActivationCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, activationCtor.DeclaringType)); EmitCreateInstanceBody (metadata, ilBuilder, encoder => { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (targetTypeRef); From 9c0bac5cee93e90d2147b0a014994df3aba8af54 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 16:47:21 +0100 Subject: [PATCH 27/43] Reduce allocations in TypeMapAssemblyEmitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reuse 3 BlobBuilder instances (_sigBlob, _codeBlob, _attrBlob) instead of allocating a new one per method body, attribute, and member ref. - Pre-compute the UCO attribute BlobHandle (always same 4 bytes). - Use (string, string) tuple for type ref cache key instead of string interpolation — avoids allocation on every cache lookup. --- .../Generator/TypeMapAssemblyEmitter.cs | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 1e714c63609..2d181f7bb3c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -15,7 +15,13 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class TypeMapAssemblyEmitter { readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); - readonly Dictionary _typeRefCache = new (StringComparer.Ordinal); + readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new (); + + // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref. + // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant. + readonly BlobBuilder _sigBlob = new BlobBuilder (64); + readonly BlobBuilder _codeBlob = new BlobBuilder (256); + readonly BlobBuilder _attrBlob = new BlobBuilder (64); readonly Version _systemRuntimeVersion; @@ -42,6 +48,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; + BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; @@ -206,6 +213,12 @@ void EmitMemberReferences (MetadataBuilder metadata) _ucoAttrCtorRef = AddMemberRef (metadata, ucoAttrTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (1); + _attrBlob.WriteUInt16 (0); + _ucoAttrBlobHandle = metadata.GetOrAddBlob (_attrBlob); + EmitTypeMapAttributeCtorRef (metadata); EmitTypeMapAssociationAttributeCtorRef (metadata); } @@ -538,28 +551,28 @@ void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) { - var attrBlob = new BlobBuilder (); - attrBlob.WriteUInt16 (0x0001); // Prolog - attrBlob.WriteSerializedString (entry.JniName); - attrBlob.WriteSerializedString (entry.ProxyTypeReference); + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); // Prolog + _attrBlob.WriteSerializedString (entry.JniName); + _attrBlob.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { - attrBlob.WriteSerializedString (entry.TargetTypeReference!); + _attrBlob.WriteSerializedString (entry.TargetTypeReference!); } - attrBlob.WriteUInt16 (0x0000); // NumNamed + _attrBlob.WriteUInt16 (0x0000); // NumNamed var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (attrBlob)); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (_attrBlob)); } void EmitTypeMapAssociationAttribute (MetadataBuilder metadata, TypeMapAssociationData assoc) { - var attrBlob = new BlobBuilder (); - attrBlob.WriteUInt16 (0x0001); // Prolog - attrBlob.WriteSerializedString (assoc.SourceTypeReference); - attrBlob.WriteSerializedString (assoc.AliasProxyTypeReference); - attrBlob.WriteUInt16 (0x0000); // NumNamed + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); // Prolog + _attrBlob.WriteSerializedString (assoc.SourceTypeReference); + _attrBlob.WriteSerializedString (assoc.AliasProxyTypeReference); + _attrBlob.WriteUInt16 (0x0000); // NumNamed metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, - metadata.GetOrAddBlob (attrBlob)); + metadata.GetOrAddBlob (_attrBlob)); } // ---- IgnoresAccessChecksTo ---- @@ -595,11 +608,11 @@ void EmitIgnoresAccessChecksToAttribute (MetadataBuilder metadata, BlobBuilder i MetadataTokens.MethodDefinitionHandle (typeMethodStart)); foreach (var asmName in assemblyNames) { - var attrBlob = new BlobBuilder (); - attrBlob.WriteUInt16 (1); - attrBlob.WriteSerializedString (asmName); - attrBlob.WriteUInt16 (0); - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, metadata.GetOrAddBlob (attrBlob)); + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (1); + _attrBlob.WriteSerializedString (asmName); + _attrBlob.WriteUInt16 (0); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, metadata.GetOrAddBlob (_attrBlob)); } } @@ -623,18 +636,17 @@ AssemblyReferenceHandle FindOrAddAssemblyReference (MetadataBuilder metadata, st return AddAssemblyRef (metadata, assemblyName, new Version (0, 0, 0, 0)); } - static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, + MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, Action encodeSig) { - var blob = new BlobBuilder (); - encodeSig (new BlobEncoder (blob)); - return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob)); + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (_sigBlob)); } EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefData typeRef) { - // Cache key: "AssemblyName:ManagedTypeName" to avoid duplicate TypeRef rows - var cacheKey = $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}"; + var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName); if (_typeRefCache.TryGetValue (cacheKey, out var cached)) { return cached; } @@ -659,10 +671,7 @@ TypeReferenceHandle MakeTypeRefForManagedName (MetadataBuilder metadata, EntityH void AddUnmanagedCallersOnlyAttribute (MetadataBuilder metadata, MethodDefinitionHandle handle) { - var attrBlob = new BlobBuilder (); - attrBlob.WriteUInt16 (1); - attrBlob.WriteUInt16 (0); - metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, metadata.GetOrAddBlob (attrBlob)); + metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, _ucoAttrBlobHandle); } /// Emits a method body and definition in one call. @@ -670,11 +679,11 @@ MethodDefinitionHandle EmitBody (MetadataBuilder metadata, BlobBuilder ilBuilder string name, MethodAttributes attrs, Action encodeSig, Action emitIL) { - var sigBlob = new BlobBuilder (); - encodeSig (new BlobEncoder (sigBlob)); + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); - var codeBuilder = new BlobBuilder (); - var encoder = new InstructionEncoder (codeBuilder); + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob); emitIL (encoder); while (ilBuilder.Count % 4 != 0) { @@ -686,7 +695,7 @@ MethodDefinitionHandle EmitBody (MetadataBuilder metadata, BlobBuilder ilBuilder return metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, metadata.GetOrAddString (name), - metadata.GetOrAddBlob (sigBlob), + metadata.GetOrAddBlob (_sigBlob), bodyOffset, default); } From 7420df4afac5b570b214a3d877ec4b0ed0f153e8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 17:19:45 +0100 Subject: [PATCH 28/43] Add generator model types and scanner enrichment for JCW/UCO generation Add JniParameterInfo, JavaConstructorInfo, and additional properties to MarshalMethodInfo and JavaPeerInfo that generators require (JniReturnType, NativeCallbackName, Parameters, JavaConstructors, ManagedTypeNamespace, ManagedTypeShortName). Extend JniSignatureHelper with raw type string parsing. Enrich scanner output with these derived fields. --- .../Generator/JniSignatureHelper.cs | 39 +++++++++ .../Scanner/JavaPeerInfo.cs | 87 +++++++++++++++++++ .../Scanner/JavaPeerScanner.cs | 47 ++++++++++ 3 files changed, 173 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 53733f69d14..5d96ddb291c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -34,6 +34,26 @@ public static List ParseParameterTypes (string jniSignature) return result; } + /// Parses the raw JNI type descriptor strings from a JNI method signature. + public static List ParseParameterTypeStrings (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + int start = i; + SkipSingleType (jniSignature, ref i); + result.Add (jniSignature.Substring (start, i - start)); + } + return result; + } + + /// Extracts the return type descriptor from a JNI method signature. + public static string ParseReturnTypeString (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return jniSignature.Substring (i); + } + /// Parses the return type from a JNI method signature. public static JniParamKind ParseReturnType (string jniSignature) { @@ -65,6 +85,25 @@ static JniParamKind ParseSingleType (string sig, ref int i) } } + static void SkipSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': case 'Z': case 'B': case 'C': case 'S': + case 'I': case 'J': case 'F': case 'D': + i++; + break; + case 'L': + i = sig.IndexOf (';', i) + 1; + break; + case '[': + i++; + SkipSingleType (sig, ref i); + break; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } + /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 85472f1b3ba..1affb809b4f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -28,6 +28,16 @@ sealed record JavaPeerInfo /// public required string ManagedTypeName { get; init; } + /// + /// Managed type namespace, e.g., "Android.App". + /// + public string ManagedTypeNamespace { get; set; } = ""; + + /// + /// Managed type short name (without namespace), e.g., "Activity". + /// + public string ManagedTypeShortName { get; set; } = ""; + /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// @@ -70,6 +80,12 @@ sealed record JavaPeerInfo /// public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); + /// + /// Java constructors to emit in the JCW .java file. + /// Each has a JNI signature and an ordinal index for the nctor_N native method. + /// + public IReadOnlyList JavaConstructors { get; set; } = Array.Empty (); + /// /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. @@ -119,6 +135,33 @@ sealed record MarshalMethodInfo /// public required string ManagedMethodName { get; init; } + /// + /// Full name of the type that declares the managed method (may be a base type). + /// + public string DeclaringTypeName { get; set; } = ""; + + /// + /// Assembly name of the type that declares the managed method. + /// Needed for cross-assembly UCO wrapper generation. + /// + public string DeclaringAssemblyName { get; set; } = ""; + + /// + /// The native callback method name, e.g., "n_onCreate". + /// This is the actual method the UCO wrapper delegates to. + /// + public string NativeCallbackName { get; set; } = ""; + + /// + /// JNI parameter types for UCO generation. + /// + public IReadOnlyList Parameters { get; set; } = Array.Empty (); + + /// + /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". + /// + public string JniReturnType { get; set; } = ""; + /// /// True if this is a constructor registration. /// @@ -137,6 +180,50 @@ sealed record MarshalMethodInfo public string? SuperArgumentsString { get; init; } } +/// +/// Describes a JNI parameter for UCO method generation. +/// +sealed class JniParameterInfo +{ + /// + /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z". + /// + public string JniType { get; set; } = ""; + + /// + /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32". + /// + public string ManagedType { get; set; } = ""; +} + +/// +/// Describes a Java constructor to emit in the JCW .java source file. +/// +sealed class JavaConstructorInfo +{ + /// + /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". + /// + public string JniSignature { get; set; } = ""; + + /// + /// Ordinal index for the native constructor method (nctor_0, nctor_1, ...). + /// + public int ConstructorIndex { get; set; } + + /// + /// JNI parameter types parsed from the signature. + /// Used to generate the Java constructor parameter list. + /// + public IReadOnlyList Parameters { get; set; } = Array.Empty (); + + /// + /// For [Export] constructors: super constructor arguments string. + /// Null for [Register] constructors. + /// + public string? SuperArgumentsString { get; set; } +} + /// /// Describes how to call the activation constructor for a Java peer type. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index bfe158f24cc..020b06931ff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -218,6 +218,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results JavaName = jniName, CompatJniName = compatJniName, ManagedTypeName = fullName, + ManagedTypeNamespace = ExtractNamespace (fullName), + ManagedTypeShortName = ExtractShortName (fullName), AssemblyName = index.AssemblyName, BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, @@ -226,6 +228,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, MarshalMethods = marshalMethods, + JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -279,6 +282,9 @@ static void AddMarshalMethod (List methods, RegisterInfo regi JniSignature = registerInfo.Signature ?? "()V", Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), + NativeCallbackName = "n_" + index.Reader.GetString (methodDef.Name), + JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), + Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, @@ -732,4 +738,45 @@ static string GetCrc64PackageName (string ns, string assemblyName) var hash = System.IO.Hashing.Crc64.Hash (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } + + static string ExtractNamespace (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; + } + + static string ExtractShortName (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; + } + + static List ParseJniParameters (string jniSignature) + { + var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature); + var result = new List (typeStrings.Count); + foreach (var t in typeStrings) { + result.Add (new JniParameterInfo { JniType = t }); + } + return result; + } + + static List BuildJavaConstructors (List marshalMethods) + { + var ctors = new List (); + int ctorIndex = 0; + foreach (var mm in marshalMethods) { + if (!mm.IsConstructor) { + continue; + } + ctors.Add (new JavaConstructorInfo { + JniSignature = mm.JniSignature, + ConstructorIndex = ctorIndex, + Parameters = mm.Parameters, + SuperArgumentsString = mm.SuperArgumentsString, + }); + ctorIndex++; + } + return ctors; + } } From 70887776640d3bd4c0841d52789f64ecac4b9f2f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 17:56:49 +0100 Subject: [PATCH 29/43] Fix UCO constructor wrappers to include full JNI parameter list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UCO constructor wrapper signature must match the JNI native method signature (jnienv + self + constructor params) for correct ABI. Previously it only accepted (IntPtr, IntPtr), causing a calling convention mismatch when JNI dispatches constructors with parameters. The body still calls ActivateInstance(self, typeof(T)) — the constructor params are consumed but not forwarded, since peer activation uses the (IntPtr, JniHandleOwnership) activation ctor, not the user constructor. --- .../Generator/Model/TypeMapAssemblyData.cs | 2 ++ .../Generator/TypeMapAssemblyEmitter.cs | 22 ++++++++++++------- .../Generator/TypeMapModelBuilderTests.cs | 16 +++++++++++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 3c47cd31a1b..c61b5a9393b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -132,6 +132,8 @@ sealed class UcoMethodData /// /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. +/// Signature must match the full JNI native method signature (jnienv + self + ctor params) +/// so the ABI is correct when JNI dispatches the call. /// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). /// sealed class UcoConstructorData diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 2d181f7bb3c..e7dbad2f7c4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -493,19 +493,25 @@ MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder { var userTypeRef = ResolveTypeRef (metadata, uco.TargetType); - // UCO constructor wrappers always take exactly (IntPtr jnienv, IntPtr self) regardless - // of the actual JNI constructor signature. The JNI parameters are not forwarded — - // ActivateInstance only needs the jobject handle to create the managed peer. - // The correct JNI signature is still used in RegisterNatives so the JNI runtime - // dispatches to this wrapper for the right constructor overload. + // UCO constructor wrappers must match the JNI native method signature exactly. + // The Java JCW declares e.g. "private native void nctor_0(Context p0)" and calls + // it with arguments. JNI dispatches with (JNIEnv*, jobject, ), + // so the wrapper signature must include all parameters to match the ABI. + // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters + // are not forwarded because ActivateInstance creates the managed peer using the + // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + int paramCount = 2 + jniParams.Count; var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (2, + sig => sig.MethodSignature ().Parameters (paramCount, rt => rt.Void (), p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // self + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }), encoder => { encoder.LoadArgument (1); // self diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 41b19202bdd..64abdda8a6b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1550,7 +1550,7 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () } [Fact] - public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () + public void FullPipeline_CustomView_UcoConstructorMatchesJniSignature () { var peer = FindFixtureByJavaName ("my/app/CustomView"); var model = BuildModel (new [] { peer }, "CtorSigTest"); @@ -1574,12 +1574,22 @@ public void FullPipeline_CustomView_UcoConstructorHasExactlyTwoParams () .ToList (); Assert.NotEmpty (ucoCtors); + + // Match each UCO constructor to its model data to verify param count foreach (var uco in ucoCtors) { - // UCO constructor wrappers always take exactly 2 params (IntPtr jnienv, IntPtr self) + var name = reader.GetString (uco.Name); + var modelUco = model.ProxyTypes + .SelectMany (p => p.UcoConstructors) + .First (u => u.WrapperName == name); + + // UCO constructor signature must include jnienv + self + JNI params + int expectedJniParams = JniSignatureHelper.ParseParameterTypes (modelUco.JniSignature).Count; + int expectedTotal = 2 + expectedJniParams; + var sig = reader.GetBlobReader (uco.Signature); var header = sig.ReadSignatureHeader (); int paramCount = sig.ReadCompressedInteger (); - Assert.Equal (2, paramCount); + Assert.Equal (expectedTotal, paramCount); } } finally { CleanUpDir (outputPath); From 03e38b5ff8e0f599833156cd50bdf160b9f62724 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 17:19:45 +0100 Subject: [PATCH 30/43] Add generator model types and scanner enrichment for JCW/UCO generation Add JniParameterInfo, JavaConstructorInfo, and additional properties to MarshalMethodInfo and JavaPeerInfo that generators require (JniReturnType, NativeCallbackName, Parameters, JavaConstructors, ManagedTypeNamespace, ManagedTypeShortName). Extend JniSignatureHelper with raw type string parsing. Enrich scanner output with these derived fields. --- .../Scanner/JavaPeerScanner.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 020b06931ff..c57836e7b7c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -779,4 +779,45 @@ static List BuildJavaConstructors (List } return ctors; } + + static string ExtractNamespace (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; + } + + static string ExtractShortName (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; + } + + static List ParseJniParameters (string jniSignature) + { + var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature); + var result = new List (typeStrings.Count); + foreach (var t in typeStrings) { + result.Add (new JniParameterInfo { JniType = t }); + } + return result; + } + + static List BuildJavaConstructors (List marshalMethods) + { + var ctors = new List (); + int ctorIndex = 0; + foreach (var mm in marshalMethods) { + if (!mm.IsConstructor) { + continue; + } + ctors.Add (new JavaConstructorInfo { + JniSignature = mm.JniSignature, + ConstructorIndex = ctorIndex, + Parameters = mm.Parameters, + SuperArgumentsString = mm.SuperArgumentsString, + }); + ctorIndex++; + } + return ctors; + } } From f869d5c484da92503abe3bc934793fdd29b35315 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 18:14:10 +0100 Subject: [PATCH 31/43] Skip [Export] methods/constructors from UCO generation, fix JCW for [Export] ctors [Export] methods and constructors use a different activation pattern than [Register] methods. [Export] has no n_* callback on the declaring type, so UCO wrapper generation would produce invalid IL. Skip them. For JCW Java source: [Export] constructors now generate TypeManager.Activate() calls instead of nctor_N native methods. Propagate IsExport, ThrownNames, and ManagedType info from the scanner to the JCW generator. Add test fixtures: ExportsConstructors, ExportsThrowsConstructors, ExportMethodWithParams. Add scanner, model builder, and JCW generator tests verifying correct [Export] exclusion and output. --- .../Generator/JcwJavaSourceGenerator.cs | 61 +- .../Generator/ModelBuilder.cs | 21 + .../Scanner/JavaPeerInfo.cs | 12 + .../Scanner/JavaPeerScanner.cs | 56 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 94 +++ .../Generator/TypeMapModelBuilderTests.cs | 67 +- .../Scanner/JavaPeerScannerTests.cs | 751 +++++++++++++++++- .../TestFixtures/StubAttributes.cs | 2 +- .../TestFixtures/TestTypes.cs | 65 ++ 9 files changed, 1082 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 9d94f1a3781..8e6a45bd3ff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -126,7 +126,19 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) writer.Write (simpleClassName); writer.Write (" ("); WriteParameterList (ctor.Parameters, writer); - writer.WriteLine (')'); + writer.Write (')'); + + if (ctor.IsExport && ctor.ThrownNames != null && ctor.ThrownNames.Count > 0) { + writer.Write ("\n\t\tthrows "); + for (int i = 0; i < ctor.ThrownNames.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (ctor.ThrownNames [i]); + } + } + + writer.WriteLine (); writer.WriteLine ("\t{"); // super() call — use SuperArgumentsString if provided ([Export] constructors), @@ -143,18 +155,29 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) writer.Write ("\t\tif (getClass () == "); writer.Write (simpleClassName); writer.Write (".class) "); - writer.Write ("nctor_"); - writer.Write (ctor.ConstructorIndex); - writer.Write (" ("); - WriteArgumentList (ctor.Parameters, writer); - writer.WriteLine (");"); + + if (ctor.IsExport) { + // [Export] constructors use TypeManager.Activate + WriteTypeManagerActivate (type, ctor.Parameters, writer); + } else { + // [Register] constructors use native nctor_N methods + writer.Write ("nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteArgumentList (ctor.Parameters, writer); + writer.Write (')'); + } + writer.WriteLine (";"); writer.WriteLine ("\t}"); writer.WriteLine (); } - // Write native constructor declarations + // Write native constructor declarations (only for [Register] constructors) foreach (var ctor in type.JavaConstructors) { + if (ctor.IsExport) { + continue; + } writer.Write ("\tprivate native void nctor_"); writer.Write (ctor.ConstructorIndex); writer.Write (" ("); @@ -167,6 +190,30 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) } } + /// + /// Writes: mono.android.TypeManager.Activate ("ManagedType, Assembly", "param types", this, new java.lang.Object[] { p0, p1 }) + /// + static void WriteTypeManagerActivate (JavaPeerInfo type, IReadOnlyList parameters, TextWriter writer) + { + writer.Write ("mono.android.TypeManager.Activate (\""); + writer.Write (type.ManagedTypeName); + writer.Write (", "); + writer.Write (type.AssemblyName); + writer.Write ("\", \""); + + // Managed parameter type signature + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (parameters [i].ManagedType); + } + + writer.Write ("\", this, new java.lang.Object[] { "); + WriteArgumentList (parameters, writer); + writer.Write (" })"); + } + static void WriteMethods (JavaPeerInfo type, TextWriter writer) { foreach (var method in type.MarshalMethods) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 19ba83374ce..c03b89beeca 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -235,6 +235,12 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) continue; } + // [Export] methods have no n_* callback on the declaring type — skip them. + // TODO: generate full marshal method body for [Export] methods (parameter marshaling + managed call) + if (mm.Connector == null) { + continue; + } + proxy.UcoMethods.Add (new UcoMethodData { WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", CallbackMethodName = mm.NativeCallbackName, @@ -254,7 +260,22 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) return; } + // Build a set of [Register] constructor signatures (Connector != null). + // [Export] constructors (Connector == null) don't get UCO wrappers — + // they use TypeManager.Activate in the JCW instead. + // TODO: generate full marshal body for [Export] constructors + var registerCtorSignatures = new HashSet (StringComparer.Ordinal); + foreach (var mm in peer.MarshalMethods) { + if (mm.IsConstructor && mm.Connector != null) { + registerCtorSignatures.Add (mm.JniSignature); + } + } + foreach (var ctor in peer.JavaConstructors) { + if (!registerCtorSignatures.Contains (ctor.JniSignature)) { + continue; + } + proxy.UcoConstructors.Add (new UcoConstructorData { WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", JniSignature = ctor.JniSignature, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 1affb809b4f..f4646f341c8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -222,6 +222,18 @@ sealed class JavaConstructorInfo /// Null for [Register] constructors. /// public string? SuperArgumentsString { get; set; } + + /// + /// Whether this constructor is from [Export] attribute. + /// [Export] constructors use TypeManager.Activate instead of nctor_N. + /// + public bool IsExport { get; set; } + + /// + /// For [Export] constructors: Java exception types that the constructor declares it can throw. + /// Null for [Register] constructors. + /// + public IReadOnlyList? ThrownNames { get; set; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index c57836e7b7c..ed0449d9af8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -277,14 +277,26 @@ static void AddMarshalMethod (List methods, RegisterInfo regi return; } + var methodName = index.Reader.GetString (methodDef.Name); + var jniSignature = registerInfo.Signature ?? "()V"; + var parameters = ParseJniParameters (jniSignature); + + // For [Export] methods, populate ManagedType from the actual method signature + // (needed for TypeManager.Activate call in JCW) + if (registerInfo.Connector == null) { + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + for (int i = 0; i < parameters.Count && i < sig.ParameterTypes.Length; i++) { + parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i]); + } + } methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, JniSignature = registerInfo.Signature ?? "()V", Connector = registerInfo.Connector, - ManagedMethodName = index.Reader.GetString (methodDef.Name), - NativeCallbackName = "n_" + index.Reader.GetString (methodDef.Name), - JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), - Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), + ManagedMethodName = methodName, + NativeCallbackName = string.Concat ("n_", methodName), + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature), + Parameters = parameters, IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, @@ -451,6 +463,40 @@ static string ManagedTypeToJniDescriptor (string managedType) } } + /// + /// Maps a managed type name (from SignatureTypeProvider) to an assembly-qualified name + /// like "System.Int32, System.Private.CoreLib" used in TypeManager.Activate calls. + /// + static string ManagedTypeToAssemblyQualifiedName (string managedType) + { + // BCL types all live in System.Private.CoreLib + switch (managedType) { + case "System.Void": + case "System.Boolean": + 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.String": + case "System.Object": + case "System.IntPtr": + case "System.UIntPtr": + return managedType + ", System.Private.CoreLib"; + default: + // For non-BCL types, we don't know the assembly at this point. + // This is a best-effort mapping; full assembly resolution for + // arbitrary types is a follow-up. + return managedType; + } + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); @@ -815,6 +861,8 @@ static List BuildJavaConstructors (List ConstructorIndex = ctorIndex, Parameters = mm.Parameters, SuperArgumentsString = mm.SuperArgumentsString, + IsExport = mm.Connector == null, + ThrownNames = mm.ThrownNames, }); ctorIndex++; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index bed5cb07eeb..c5b7469127d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -426,4 +426,98 @@ public void Generate_ExportWithoutThrows_HasNoThrowsClause () Assert.DoesNotContain ("throws", java); } } + + public class ExportConstructor + { + + [Fact] + public void Generate_ExportConstructors_UsesTypeManagerActivate () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + var java = GenerateToString (peer); + + // [Export] constructors should use TypeManager.Activate + Assert.Contains ("mono.android.TypeManager.Activate (\"", java); + + // Should NOT have nctor_N native declarations + Assert.DoesNotContain ("nctor_", java); + } + + [Fact] + public void Generate_ExportConstructors_ParameterlessCtorHasEmptySignature () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + var java = GenerateToString (peer); + + // Parameterless [Export] ctor should have empty managed param signature + Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"\", this, new java.lang.Object[] { })", java); + } + + [Fact] + public void Generate_ExportConstructors_IntCtorHasIntSignature () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + var java = GenerateToString (peer); + + // int parameter [Export] ctor should have managed type signature + Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"System.Int32, System.Private.CoreLib\", this, new java.lang.Object[] { p0 })", java); + } + + [Fact] + public void Generate_ExportThrowsConstructors_HasThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsThrowsConstructors"); + var java = GenerateToString (peer); + + // [Export] constructors with ThrownNames should have throws clause + Assert.Contains ("throws java.lang.Throwable", java); + } + + [Fact] + public void Generate_MixedRegisterAndExportConstructors_HandledCorrectly () + { + // A type with both [Register] and [Export] constructors + var type = new JavaPeerInfo { + JavaName = "my/app/MixedCtors", + ManagedTypeName = "MyApp.MixedCtors", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MixedCtors", + AssemblyName = "App", + BaseJavaName = "java/lang/Object", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "()V", + ConstructorIndex = 0, + Parameters = new List (), + IsExport = false, // [Register] + }, + new JavaConstructorInfo { + JniSignature = "(I)V", + ConstructorIndex = 1, + Parameters = new List { + new JniParameterInfo { JniType = "I", ManagedType = "System.Int32, System.Private.CoreLib" }, + }, + IsExport = true, // [Export] + }, + }, + }; + + var java = GenerateToString (type); + + // [Register] ctor should use nctor_0 + Assert.Contains ("nctor_0 ()", java); + Assert.Contains ("private native void nctor_0 ()", java); + + // [Export] ctor should use TypeManager.Activate + Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.MixedCtors, App\"", java); + + // Only nctor_0 declaration (not nctor_1 for [Export]) + Assert.DoesNotContain ("nctor_1", java); + } + + } } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 64abdda8a6b..db4e5b15f3e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -78,6 +78,7 @@ public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () NativeCallbackName = "n_OnCreate", JniSignature = "(Landroid/os/Bundle;)V", IsConstructor = false, + Connector = "n_OnCreate_handler", DeclaringTypeName = "Android.App.Activity", DeclaringAssemblyName = "Mono.Android", }); @@ -638,10 +639,10 @@ public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); Assert.NotEmpty (acwProxies); - // ACW proxies should have registrations - foreach (var proxy in acwProxies) { - Assert.NotEmpty (proxy.NativeRegistrations); - } + // ACW proxies with [Register] marshal methods should have registrations. + // [Export]-only types don't generate UCO wrappers yet (TODO). + var proxiesWithRegistrations = acwProxies.Where (p => p.NativeRegistrations.Count > 0).ToList (); + Assert.NotEmpty (proxiesWithRegistrations); } } @@ -760,6 +761,7 @@ static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmN NativeCallbackName = "n_ctor", JniSignature = "()V", IsConstructor = true, + Connector = "", }, }; return peer; @@ -772,6 +774,9 @@ static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, NativeCallbackName = callbackName, JniSignature = jniSig, IsConstructor = isConstructor, + // [Register] methods always have a non-null Connector (handler name or "" for ctors). + // [Export] methods have null Connector. + Connector = isConstructor ? "" : callbackName + "_handler", }; } @@ -1317,6 +1322,60 @@ public void Fixture_ExportExample_IsAcw () } } + [Fact] + public void Fixture_ExportExample_ExportMethodNotInUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); + Assert.NotNull (proxy); + + // [Export] methods have no n_* callback → must NOT generate UCO wrappers + Assert.Empty (proxy!.UcoMethods); + } + + [Fact] + public void Fixture_ExportMethodWithParams_ExportMethodsNotInUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + Assert.Equal (2, peer.MarshalMethods.Count); + // Both methods should be [Export] (Connector == null) + Assert.All (peer.MarshalMethods, mm => Assert.Null (mm.Connector)); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + Assert.Empty (proxy!.UcoMethods); + } + + [Fact] + public void Fixture_ExportsConstructors_NoUcoConstructors () + { + var peer = FindFixtureByJavaName ("my/app/ExportsConstructors"); + // Should have [Export] constructors (Connector == null) + var exportCtors = peer.MarshalMethods.Where (m => m.IsConstructor && m.Connector == null).ToList (); + Assert.NotEmpty (exportCtors); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsConstructors_Proxy"); + Assert.NotNull (proxy); + + // [Export] constructors should NOT generate UCO constructor wrappers + Assert.Empty (proxy!.UcoConstructors); + } + + [Fact] + public void Fixture_ExportsThrowsConstructors_NoUcoConstructors () + { + var peer = FindFixtureByJavaName ("my/app/ExportsThrowsConstructors"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsThrowsConstructors_Proxy"); + Assert.NotNull (proxy); + + // All constructors are [Export] → no UCO constructors + Assert.Empty (proxy!.UcoConstructors); + } + } public class FixtureImplementors diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index bc2b6195f22..b48bf557815 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public partial class JavaPeerScannerTests +public class JavaPeerScannerTests { static string TestFixtureAssemblyPath { get { @@ -43,59 +44,166 @@ public void Scan_FindsAllJavaPeerTypes () { var peers = ScanFixtures (); Assert.NotEmpty (peers); + // MCW types with [Register] Assert.Contains (peers, p => p.JavaName == "java/lang/Object"); Assert.Contains (peers, p => p.JavaName == "android/app/Activity"); + // User type with JNI name from [Activity(Name="...")] Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity"); + // Exception/Throwable hierarchy + Assert.Contains (peers, p => p.JavaName == "java/lang/Throwable"); + Assert.Contains (peers, p => p.JavaName == "java/lang/Exception"); } - [Theory] - [InlineData ("android/app/Activity", true)] - [InlineData ("android/widget/Button", true)] - [InlineData ("my/app/MainActivity", false)] - public void Scan_DoNotGenerateAcw (string javaName, bool expected) + [Fact] + public void Scan_McwTypes_HaveDoNotGenerateAcw () + { + var peers = ScanFixtures (); + + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.True (activity.DoNotGenerateAcw, "Activity should be MCW (DoNotGenerateAcw=true)"); + + var button = FindByJavaName (peers, "android/widget/Button"); + Assert.True (button.DoNotGenerateAcw, "Button should be MCW"); + } + + [Fact] + public void Scan_UserTypes_DoNotGenerateAcwIsFalse () + { + var peers = ScanFixtures (); + + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw, "MainActivity should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_ActivityType_IsUnconditional () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.True (mainActivity.IsUnconditional, "MainActivity with [Activity] should be unconditional"); + } + + [Fact] + public void Scan_ServiceType_IsUnconditional () { var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.True (service.IsUnconditional, "MyService with [Service] should be unconditional"); } - [Theory] - [InlineData ("my/app/MainActivity", true)] - [InlineData ("my/app/MyService", true)] - [InlineData ("my/app/MyReceiver", true)] - [InlineData ("my/app/MyProvider", true)] - [InlineData ("my/app/MyApplication", true)] - [InlineData ("my/app/MyInstrumentation", true)] - [InlineData ("my/app/MyBackupAgent", true)] - [InlineData ("my/app/MyManageSpaceActivity", true)] - [InlineData ("my/app/MyHelper", false)] - [InlineData ("android/app/Activity", false)] - public void Scan_IsUnconditional (string javaName, bool expected) + [Fact] + public void Scan_BroadcastReceiverType_IsUnconditional () { var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.True (receiver.IsUnconditional, "MyReceiver with [BroadcastReceiver] should be unconditional"); } [Fact] - public void Scan_TypeMetadata_IsCorrect () + public void Scan_ContentProviderType_IsUnconditional () { var peers = ScanFixtures (); - Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract); - Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface); - Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.True (provider.IsUnconditional, "MyProvider with [ContentProvider] should be unconditional"); + } + [Fact] + public void Scan_TypeWithoutComponentAttribute_IsTrimmable () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "MyHelper without component attr should be trimmable"); + } + + [Fact] + public void Scan_McwBinding_IsTrimmable () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.False (activity.IsUnconditional, "MCW Activity should be trimmable (no component attr on MCW type)"); + } + + [Fact] + public void Scan_InterfaceType_IsMarkedAsInterface () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.True (listener.IsInterface, "IOnClickListener should be marked as interface"); + } + + [Fact] + public void Scan_InvokerTypes_AreIncluded () + { + var peers = ScanFixtures (); + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw, "Invoker should have DoNotGenerateAcw=true"); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_GenericType_IsGenericDefinition () + { + var peers = ScanFixtures (); var generic = FindByJavaName (peers, "my/app/GenericHolder"); - Assert.True (generic.IsGenericDefinition); - Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + Assert.True (generic.IsGenericDefinition, "GenericHolder should be marked as generic definition"); } [Fact] - public void Scan_InvokerAndInterface_ShareJavaName () + public void Scan_AbstractType_IsMarkedAbstract () { var peers = ScanFixtures (); - var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); - Assert.Equal (2, clickListenerPeers.Count); - Assert.Contains (clickListenerPeers, p => p.IsInterface); - Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + Assert.True (abstractBase.IsAbstract, "AbstractBase should be marked as abstract"); + } + + [Fact] + public void Scan_MarshalMethods_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotEmpty (activity.MarshalMethods); + } + + [Fact] + public void Scan_UserTypeOverride_CollectsMarshalMethods () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotEmpty (mainActivity.MarshalMethods); + + var onCreate = mainActivity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_ResolvesToSelf () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotNull (activity.ActivationCtor); + Assert.Equal ("Android.App.Activity", activity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, activity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithoutOwnActivationCtor_InheritsFromBase () + { + var peers = ScanFixtures (); + var simpleActivity = FindByJavaName (peers, "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", simpleActivity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, simpleActivity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_DoesNotLookAtBase () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotNull (mainActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", mainActivity.ActivationCtor.DeclaringTypeName); } [Fact] @@ -106,4 +214,585 @@ public void Scan_AllTypes_HaveAssemblyName () Assert.False (string.IsNullOrEmpty (peer.AssemblyName), $"Type {peer.ManagedTypeName} should have assembly name")); } + + [Fact] + public void Scan_InvokerSharesJavaNameWithInterface () + { + var peers = ScanFixtures (); + var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + // Interface + Invoker share the same JNI name (this is expected — they're aliases) + Assert.Equal (2, clickListenerPeers.Count); + Assert.Contains (clickListenerPeers, p => p.IsInterface); + Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + } + + [Fact] + public void Scan_ActivityBaseJavaName_IsJavaLangObject () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal ("java/lang/Object", activity.BaseJavaName); + } + + [Fact] + public void Scan_MainActivityBaseJavaName_IsActivity () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal ("android/app/Activity", mainActivity.BaseJavaName); + } + + [Fact] + public void Scan_JavaLangObjectBaseJavaName_IsNull () + { + var peers = ScanFixtures (); + var jlo = FindByJavaName (peers, "java/lang/Object"); + Assert.Null (jlo.BaseJavaName); + } + + [Fact] + public void Scan_TypeImplementingInterface_HasInterfaceJavaNames () + { + var peers = ScanFixtures (); + var clickable = FindByJavaName (peers, "my/app/ClickableView"); + Assert.Contains ("android/view/View$OnClickListener", clickable.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_TypeNotImplementingInterface_HasEmptyList () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.Empty (helper.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_TypeWithRegisteredCtors_HasConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var ctors = customView.MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + } + + [Fact] + public void Scan_TypeWithoutRegisteredCtors_HasNoConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.DoesNotContain (helper.MarshalMethods, m => m.IsConstructor); + } + + [Fact] + public void Scan_MarshalMethod_JniNameIsJavaMethodName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onCreate = activity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("onCreate", onCreate.JniName); + } + + [Fact] + public void Scan_UserTypeWithActivityName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw); + Assert.True (mainActivity.IsUnconditional, "Types with [Activity] are unconditional"); + } + + [Fact] + public void Scan_UserTypeWithServiceName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.False (service.DoNotGenerateAcw); + Assert.True (service.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithBroadcastReceiverName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.False (receiver.DoNotGenerateAcw); + Assert.True (receiver.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithContentProviderName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.False (provider.DoNotGenerateAcw); + Assert.True (provider.IsUnconditional); + } + + [Fact] + public void Scan_Throwable_IsMcwType () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + Assert.True (throwable.DoNotGenerateAcw); + Assert.Equal ("java/lang/Object", throwable.BaseJavaName); + } + + [Fact] + public void Scan_Exception_ExtendsThrowable () + { + var peers = ScanFixtures (); + var exception = FindByJavaName (peers, "java/lang/Exception"); + Assert.True (exception.DoNotGenerateAcw); + Assert.Equal ("java/lang/Throwable", exception.BaseJavaName); + } + + [Fact] + public void Scan_NestedType_IsDiscovered () + { + var peers = ScanFixtures (); + var inner = FindByJavaName (peers, "my/app/Outer$Inner"); + Assert.Equal ("MyApp.Outer+Inner", inner.ManagedTypeName); + } + + [Fact] + public void Scan_NestedTypeInInterface_IsDiscovered () + { + var peers = ScanFixtures (); + var result = FindByJavaName (peers, "my/app/ICallback$Result"); + Assert.Equal ("MyApp.ICallback+Result", result.ManagedTypeName); + } + + [Fact] + public void Scan_MarshalMethod_BoolReturn_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onTouch = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onTouch"); + Assert.NotNull (onTouch); + Assert.Equal ("(Landroid/view/View;I)Z", onTouch.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_BoolParam_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onFocus = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onFocusChange"); + Assert.NotNull (onFocus); + Assert.Equal ("(Landroid/view/View;Z)V", onFocus.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_MultiplePrimitiveParams () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onScroll = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onScroll"); + Assert.NotNull (onScroll); + Assert.Equal ("(IFJD)V", onScroll.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ArrayParam () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var setItems = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "setItems"); + Assert.NotNull (setItems); + Assert.Equal ("([Ljava/lang/String;)V", setItems.JniSignature); + } + + [Fact] + public void Scan_ExportMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var export = FindByJavaName (peers, "my/app/ExportExample"); + Assert.Single (export.MarshalMethods); + var method = export.MarshalMethods [0]; + Assert.Equal ("myExportedMethod", method.JniName); + Assert.Null (method.Connector); + } + + [Fact] + public void Scan_ExportConstructor_CollectedWithNullConnector () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + // [Export] on constructors: 2 exported ctors + 0 Register methods + var exportCtors = peer.MarshalMethods.Where (m => m.IsConstructor && m.Connector == null).ToList (); + Assert.Equal (2, exportCtors.Count); + // Verify one is parameterless, one takes int + Assert.Contains (exportCtors, c => c.JniSignature == "()V"); + Assert.Contains (exportCtors, c => c.JniSignature == "(I)V"); + } + + [Fact] + public void Scan_ExportMethodWithParams_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMethodWithParams"); + Assert.Equal (2, peer.MarshalMethods.Count); + // All should be [Export] (null connector) + Assert.All (peer.MarshalMethods, m => Assert.Null (m.Connector)); + // doWork(int) → (I)V + var doWork = peer.MarshalMethods.First (m => m.JniName == "doWork"); + Assert.Equal ("(I)V", doWork.JniSignature); + // computeName(String, int) → (Ljava/lang/String;I)Ljava/lang/String; + var computeName = peer.MarshalMethods.First (m => m.JniName == "computeName"); + Assert.Equal ("(Ljava/lang/String;I)Ljava/lang/String;", computeName.JniSignature); + } + + [Fact] + public void Scan_CustomView_DiscoveredAsRegularType () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + Assert.False (customView.IsUnconditional, "Custom views are not unconditional by attribute alone"); + Assert.Equal ("android/view/View", customView.BaseJavaName); + } + + [Fact] + public void Scan_ApplicationType_IsUnconditional () + { + var peers = ScanFixtures (); + var app = FindByJavaName (peers, "my/app/MyApplication"); + Assert.True (app.IsUnconditional, "[Application] should be unconditional"); + } + + [Fact] + public void Scan_InstrumentationType_IsUnconditional () + { + var peers = ScanFixtures (); + var instr = FindByJavaName (peers, "my/app/MyInstrumentation"); + Assert.True (instr.IsUnconditional, "[Instrumentation] should be unconditional"); + } + + [Fact] + public void Scan_BackupAgent_ForcedUnconditional () + { + var peers = ScanFixtures (); + var backupAgent = FindByJavaName (peers, "my/app/MyBackupAgent"); + Assert.True (backupAgent.IsUnconditional, "BackupAgent referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_ManageSpaceActivity_ForcedUnconditional () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "my/app/MyManageSpaceActivity"); + Assert.True (activity.IsUnconditional, "ManageSpaceActivity referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_BackupAgent_NotUnconditional_WhenNotReferenced () + { + // A type extending BackupAgent that is NOT referenced from [Application] + // should remain trimmable (not unconditional). + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "Unreferenced type should remain trimmable"); + } + + [Fact] + public void Scan_McwBinding_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal (activity.JavaName, activity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithRegister_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal (mainActivity.JavaName, mainActivity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithoutRegister_CompatJniNameUsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredHelper"); + Assert.NotNull (unregistered); + + // JavaName should use CRC64 package + Assert.StartsWith ("crc64", unregistered.JavaName); + + // CompatJniName should use the raw namespace + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + var widget = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.CustomWidget"); + Assert.NotNull (widget); + + // The custom attribute provides the JNI name via IJniNameProviderAttribute + Assert.Equal ("com/example/CustomWidget", widget.JavaName); + Assert.Equal ("com/example/CustomWidget", widget.CompatJniName); + } + + [Fact] + public void Scan_DeepHierarchy_ResolvesBaseJavaName () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.Equal ("android/widget/Button", myButton.BaseJavaName); + } + + [Fact] + public void Scan_DeepHierarchy_InheritsActivationCtor () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.NotNull (myButton.ActivationCtor); + // MyButton → Button → View → Java.Lang.Object — should find XI ctor from View or Object + Assert.Equal (ActivationCtorStyle.XamarinAndroid, myButton.ActivationCtor.Style); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + } + + [Fact] + public void Scan_AbstractMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + var doWork = abstractBase.MarshalMethods.FirstOrDefault (m => m.JniName == "doWork"); + Assert.NotNull (doWork); + Assert.Equal ("()V", doWork.JniSignature); + } + + [Fact] + public void Scan_PropertyRegister_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + var getMessage = throwable.MarshalMethods.FirstOrDefault (m => m.JniName == "getMessage"); + Assert.NotNull (getMessage); + Assert.Equal ("()Ljava/lang/String;", getMessage.JniSignature); + } + + [Fact] + public void Scan_MethodWithEmptyConnector_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onStart = activity.MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + } + + [Fact] + public void Scan_InvokerWithRegisterAndDoNotGenerateAcw_IsIncluded () + { + var peers = ScanFixtures (); + // IOnClickListenerInvoker has [Register("android/view/View$OnClickListener", DoNotGenerateAcw=true)] + // It should be included in the scanner output — generators will filter it later + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_Interface_HasInvokerTypeNameFromRegisterConnector () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.NotNull (listener.InvokerTypeName); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName); + } + + [Fact] + public void Scan_Interface_IsNotMarkedDoNotGenerateAcw () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + // Interfaces have [Register("name", "", "connector")] — the 3-arg form doesn't set DoNotGenerateAcw + Assert.False (listener.DoNotGenerateAcw, "Interfaces should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_InterfaceMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + } + + [Fact] + public void Scan_GenericType_HasCorrectManagedTypeName () + { + var peers = ScanFixtures (); + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + } + + [Fact] + public void Scan_Context_IsDiscovered () + { + var peers = ScanFixtures (); + var context = FindByJavaName (peers, "android/content/Context"); + Assert.True (context.DoNotGenerateAcw, "Context is MCW"); + Assert.Equal ("java/lang/Object", context.BaseJavaName); + } + + // ================================================================ + // Edge case tests — discovered during side-by-side testing + // ================================================================ + + [Fact] + public void Scan_GenericBaseType_ResolvesViaTypeSpecification () + { + // ConcreteFromGeneric extends GenericBase. The base type + // is a TypeSpecification (generic instantiation). The scanner must + // decode the blob to resolve the underlying TypeDef/TypeRef. + var peers = ScanFixtures (); + var concrete = FindByJavaName (peers, "my/app/ConcreteFromGeneric"); + Assert.Equal ("my/app/GenericBase", concrete.BaseJavaName); + } + + [Fact] + public void Scan_GenericInterface_ResolvesViaTypeSpecification () + { + // GenericCallbackImpl implements IGenericCallback. The interface + // is a TypeSpecification. The scanner must decode the blob to resolve it. + var peers = ScanFixtures (); + var impl = FindByJavaName (peers, "my/app/GenericCallbackImpl"); + Assert.Contains ("my/app/IGenericCallback", impl.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ComponentOnlyBase_DerivedTypeIsDiscovered () + { + // BaseActivityNoRegister has [Activity] but no [Register]. + // DerivedFromComponentBase extends it. ExtendsJavaPeerCore must + // detect the component attribute on the base to include the derived type. + var peers = ScanFixtures (); + var derived = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.DerivedFromComponentBase"); + Assert.NotNull (derived); + // Should get a CRC64-computed JNI name + Assert.StartsWith ("crc64", derived.JavaName); + } + + [Fact] + public void Scan_ComponentOnlyBase_BaseTypeIsDiscovered () + { + // BaseActivityNoRegister has [Activity(Name = "...")] — should be discovered + // even without [Register]. + var peers = ScanFixtures (); + var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + Assert.True (baseType.IsUnconditional, "[Activity] makes it unconditional"); + Assert.Equal ("android/app/Activity", baseType.BaseJavaName); + } + + [Fact] + public void Scan_UnregisteredNestedType_UsesParentJniPrefix () + { + // UnregisteredChild has no [Register] but its parent RegisteredParent does. + // ComputeTypeNameParts should use parent's JNI name as prefix. + var peers = ScanFixtures (); + var child = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.RegisteredParent+UnregisteredChild"); + Assert.NotNull (child); + Assert.Equal ("my/app/RegisteredParent_UnregisteredChild", child.JavaName); + } + + [Fact] + public void Scan_EmptyNamespace_RegisteredType_Discovered () + { + // GlobalType has [Register] and no namespace — should work normally. + var peers = ScanFixtures (); + var global = FindByJavaName (peers, "my/app/GlobalType"); + Assert.Equal ("GlobalType", global.ManagedTypeName); + } + + [Fact] + public void Scan_EmptyNamespace_UnregisteredType_CompatJniHasNoSlash () + { + // GlobalUnregisteredType has no namespace and no [Register]. + // CompatJniName should just be the type name (no leading slash). + var peers = ScanFixtures (); + var global = peers.FirstOrDefault (p => p.ManagedTypeName == "GlobalUnregisteredType"); + Assert.NotNull (global); + Assert.Equal ("GlobalUnregisteredType", global.CompatJniName); + Assert.DoesNotContain ("/", global.CompatJniName); + } + + [Fact] + public void Scan_DeepNestedType_ThreeLevelNesting () + { + // DeepOuter.Middle.DeepInner — 3-level nesting. + // ComputeTypeNameParts walks multiple declaring-type levels. + var peers = ScanFixtures (); + var deep = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.DeepOuter+Middle+DeepInner"); + Assert.NotNull (deep); + Assert.Equal ("my/app/DeepOuter_Middle_DeepInner", deep.JavaName); + } + + [Fact] + public void Scan_PlainActivitySubclass_DiscoveredWithCrc64Name () + { + // PlainActivitySubclass extends Activity with no [Register], no [Activity]. + // ExtendsJavaPeer detects it via the base type chain, gets CRC64 name. + var peers = ScanFixtures (); + var plain = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.PlainActivitySubclass"); + Assert.NotNull (plain); + Assert.StartsWith ("crc64", plain.JavaName); + Assert.Equal ("android/app/Activity", plain.BaseJavaName); + } + + [Fact] + public void Scan_ComponentAttributeWithoutName_DiscoveredWithCrc64Name () + { + // UnnamedActivity has [Activity(Label="Unnamed")] but no Name property. + // HasComponentAttribute = true, ComponentAttributeJniName should be null, + // and the type should still get a CRC64-based JNI name. + var peers = ScanFixtures (); + var unnamed = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnnamedActivity"); + Assert.NotNull (unnamed); + Assert.StartsWith ("crc64", unnamed.JavaName); + Assert.True (unnamed.IsUnconditional, "[Activity] makes it unconditional"); + } + + [Fact] + public void Scan_InterfaceOnUnregisteredType_InterfacesResolved () + { + // UnregisteredClickListener has no [Register] but implements IOnClickListener. + // Type gets CRC64 name, interfaces still resolved. + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredClickListener"); + Assert.NotNull (listener); + Assert.StartsWith ("crc64", listener.JavaName); + Assert.Contains ("android/view/View$OnClickListener", listener.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + // UnregisteredExporter has [Export("doExportedWork")] on a type without [Register]. + // Type gets CRC64 name, [Export] method is in MarshalMethods. + var peers = ScanFixtures (); + var exporter = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredExporter"); + Assert.NotNull (exporter); + Assert.StartsWith ("crc64", exporter.JavaName); + var exportMethod = exporter.MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index e8b892815db..5f4ab12a45e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -98,7 +98,7 @@ public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameP namespace Java.Interop { - [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + [AttributeUsage (AttributeTargets.Method | AttributeTargets.Constructor, AllowMultiple = false)] public sealed class ExportAttribute : Attribute { public string? Name { get; set; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a8ef855a0d8..95056828b22 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -676,3 +676,68 @@ protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer public class GlobalUnregisteredType : Java.Lang.Object { } + +// ================================================================ +// [Export] constructor scenarios — ported from legacy SupportDeclarations.cs +// ================================================================ +namespace MyApp +{ + /// + /// Type with [Export] constructors (no [Register] on ctors). + /// Legacy JCW: TypeManager.Activate pattern, not nctor_N. + /// + [Register ("my/app/ExportsConstructors")] + public class ExportsConstructors : Java.Lang.Object + { + protected ExportsConstructors (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export] + public ExportsConstructors () { } + + [Java.Interop.Export] + public ExportsConstructors (int value) { } + } + + /// + /// Type with [Export] constructors that throw. + /// + [Register ("my/app/ExportsThrowsConstructors")] + public class ExportsThrowsConstructors : Java.Lang.Object + { + protected ExportsThrowsConstructors (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public ExportsThrowsConstructors () { } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public ExportsThrowsConstructors (int value) { } + + [Java.Interop.Export] + public ExportsThrowsConstructors (string value) { } + } + + /// + /// Type with [Export] methods with parameters (not just parameterless). + /// Ported from legacy ExportsMembers. + /// + [Register ("my/app/ExportMethodWithParams")] + public class ExportMethodWithParams : Java.Lang.Object + { + protected ExportMethodWithParams (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export ("doWork")] + public void DoWork (int count) { } + + [Java.Interop.Export ("computeName")] + public string ComputeName (string prefix, int index) { return ""; } + } +} From f1c6cd12f7cf6c3afc3972c75263dbc30000dfa1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 18:25:50 +0100 Subject: [PATCH 32/43] Port legacy [Export] tests: full output comparison, name override, throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test fixtures and tests ported from legacy JavaCallableWrapperGeneratorTests: - GenerateConstructors → full JCW output comparison for [Export] ctors - GenerateConstructors_WithThrows → throws clause on [Export] ctors - GenerateExportedMembers → name override, throws, empty throws array - ExportCtorWithSuperArgs → SuperArgumentsString in super() call New scanner tests verify ThrownNames, name override callback names, and SuperArgumentsString propagation. --- .../Generator/JcwJavaSourceGeneratorTests.cs | 146 ++++++++++++++++++ .../Scanner/JavaPeerScannerTests.cs | 34 ++++ .../TestFixtures/TestTypes.cs | 41 +++++ 3 files changed, 221 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index c5b7469127d..c6a1fe077b7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -466,6 +466,55 @@ public void Generate_ExportConstructors_IntCtorHasIntSignature () Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"System.Int32, System.Private.CoreLib\", this, new java.lang.Object[] { p0 })", java); } + /// + /// Full output comparison — ported from legacy GenerateConstructors. + /// Verifies the complete JCW for [Export] constructors matches the + /// TypeManager.Activate pattern with correct activation guard. + /// + [Fact] + public void Generate_ExportConstructors_FullOutput () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + var java = GenerateToString (peer); + + // Parameterless ctor: super(), then TypeManager.Activate with empty sig + Assert.Contains ("\tpublic ExportsConstructors ()\n\t{\n\t\tsuper ();\n\t\tif (getClass () == ExportsConstructors.class) mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"\", this, new java.lang.Object[] { });\n\t}\n", java); + + // int ctor: super(p0), then TypeManager.Activate with int sig + Assert.Contains ("\tpublic ExportsConstructors (int p0)\n\t{\n\t\tsuper (p0);\n\t\tif (getClass () == ExportsConstructors.class) mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"System.Int32, System.Private.CoreLib\", this, new java.lang.Object[] { p0 });\n\t}\n", java); + + // No nctor native declarations + Assert.DoesNotContain ("private native void nctor_", java); + } + + /// + /// Full output comparison — ported from legacy GenerateConstructors_WithThrows. + /// Verifies throws clauses appear on ctors that have ThrownNames. + /// + [Fact] + public void Generate_ExportThrowsConstructors_FullOutput () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsThrowsConstructors"); + var java = GenerateToString (peer); + + // Parameterless ctor with throws + Assert.Contains ("\tpublic ExportsThrowsConstructors ()\n\t\tthrows java.lang.Throwable\n\t{\n\t\tsuper ();\n", java); + + // int ctor with throws + Assert.Contains ("\tpublic ExportsThrowsConstructors (int p0)\n\t\tthrows java.lang.Throwable\n\t{\n\t\tsuper (p0);\n", java); + + // string ctor WITHOUT throws (empty ThrownNames in legacy means [Export] with no Throws) + Assert.Contains ("\tpublic ExportsThrowsConstructors (java.lang.String p0)\n\t{\n\t\tsuper (p0);\n", java); + + // String ctor should use TypeManager.Activate with String sig + Assert.Contains ("\"System.String, System.Private.CoreLib\"", java); + + // No nctor native declarations + Assert.DoesNotContain ("private native void nctor_", java); + } + [Fact] public void Generate_ExportThrowsConstructors_HasThrowsClause () { @@ -519,5 +568,102 @@ public void Generate_MixedRegisterAndExportConstructors_HandledCorrectly () Assert.DoesNotContain ("nctor_1", java); } + [Fact] + public void Generate_ExportCtorWithSuperArgs_UsesCustomSuperArgs () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportCtorWithSuperArgs"); + var java = GenerateToString (peer); + + // SuperArgumentsString = "" means super() with no args, not super(p0) + Assert.Contains ("super ();", java); + Assert.DoesNotContain ("super (p0);", java); + + // Should still use TypeManager.Activate + Assert.Contains ("mono.android.TypeManager.Activate (\"", java); + } + + } + + public class ExportMethodJcw + { + + /// + /// Ported from legacy GenerateExportedMembers — [Export] with name override. + /// The Java method name should be the export name, not the C# method name. + /// + [Fact] + public void Generate_ExportWithNameOverride_UsesExportName () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + // [Export("attributeOverridesNames")] on CompletelyDifferentName + // Java method uses export name, native callback uses n_ + C# method name + Assert.Contains ("public java.lang.String attributeOverridesNames (java.lang.String p0, int p1)", java); + Assert.Contains ("n_CompletelyDifferentName (p0, p1)", java); + } + + /// + /// Ported from legacy GenerateExportedMembers — [Export] method keeps C# name. + /// + [Fact] + public void Generate_ExportWithoutNameOverride_UsesMethodName () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + // [Export] without name arg uses the C# method name as-is + Assert.Contains ("public void methodNamesNotMangled ()", java); + Assert.Contains ("n_methodNamesNotMangled ()", java); + } + + /// + /// Ported from legacy GenerateExportedMembers — [Export] with throws. + /// + [Fact] + public void Generate_ExportMethodWithThrows_HasThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + // throws clause appears on the line after the method signature + Assert.Contains ("methodThatThrows ()\n\t\tthrows java.lang.Throwable\n", java); + } + + /// + /// Ported from legacy GenerateExportedMembers — [Export] with empty throws array. + /// Should NOT generate a throws clause. + /// + [Fact] + public void Generate_ExportMethodWithEmptyThrows_NoThrowsClause () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + // methodThatThrowsEmptyArray should NOT have throws clause + // It should appear as a plain method declaration + Assert.Contains ("public void methodThatThrowsEmptyArray ()", java); + + // Make sure the throws clause is NOT on this specific method + // (it might be on methodThatThrows, but not on methodThatThrowsEmptyArray) + var lines = java.Split ('\n'); + for (int i = 0; i < lines.Length; i++) { + if (lines [i].Contains ("methodThatThrowsEmptyArray")) { + // The line with the method should not have throws, + // and neither should the next line + Assert.DoesNotContain ("throws", lines [i]); + if (i + 1 < lines.Length) { + Assert.DoesNotContain ("throws", lines [i + 1]); + } + break; + } + } + } + } } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index b48bf557815..47098aacade 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -445,6 +445,40 @@ public void Scan_ExportMethodWithParams_HasCorrectJniSignature () Assert.Equal ("(Ljava/lang/String;I)Ljava/lang/String;", computeName.JniSignature); } + [Fact] + public void Scan_ExportMembersComprehensive_NameOverrideAndThrows () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + Assert.Equal (4, peer.MarshalMethods.Count); + + // [Export("attributeOverridesNames")] on CompletelyDifferentName + var renamed = peer.MarshalMethods.First (m => m.JniName == "attributeOverridesNames"); + Assert.Null (renamed.Connector); + Assert.Equal ("n_CompletelyDifferentName", renamed.NativeCallbackName); + + // [Export(ThrownNames = new [] { "java.lang.Throwable" })] + var throwing = peer.MarshalMethods.First (m => m.JniName == "methodThatThrows"); + Assert.NotNull (throwing.ThrownNames); + Assert.Single (throwing.ThrownNames!); + Assert.Equal ("java.lang.Throwable", throwing.ThrownNames! [0]); + + // [Export(ThrownNames = new string [0])] + var emptyThrows = peer.MarshalMethods.First (m => m.JniName == "methodThatThrowsEmptyArray"); + // Empty array should result in null or empty ThrownNames + Assert.True (emptyThrows.ThrownNames == null || emptyThrows.ThrownNames.Count == 0); + } + + [Fact] + public void Scan_ExportCtorWithSuperArgs_HasSuperArgumentsString () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportCtorWithSuperArgs"); + var ctor = peer.MarshalMethods.FirstOrDefault (m => m.IsConstructor); + Assert.NotNull (ctor); + Assert.Equal ("", ctor!.SuperArgumentsString); + } + [Fact] public void Scan_CustomView_DiscoveredAsRegularType () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 95056828b22..0c7f0aae1c2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -740,4 +740,45 @@ public void DoWork (int count) { } [Java.Interop.Export ("computeName")] public string ComputeName (string prefix, int index) { return ""; } } + + /// + /// Comprehensive [Export] member scenarios ported from legacy ExportsMembers. + /// Tests: name override, throws, empty throws, static methods. + /// + [Register ("my/app/ExportMembersComprehensive")] + public class ExportMembersComprehensive : Java.Lang.Object + { + protected ExportMembersComprehensive (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export] + public void methodNamesNotMangled () { } + + [Java.Interop.Export ("attributeOverridesNames")] + public string CompletelyDifferentName (string value, int count) { return value; } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public void methodThatThrows () { } + + [Java.Interop.Export (ThrownNames = new string [0])] + public void methodThatThrowsEmptyArray () { } + } + + /// + /// [Export] constructor with SuperArgumentsString. + /// The super() call should use the custom args, not forward all params. + /// + [Register ("my/app/ExportCtorWithSuperArgs")] + public class ExportCtorWithSuperArgs : Java.Lang.Object + { + protected ExportCtorWithSuperArgs (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export (SuperArgumentsString = "")] + public ExportCtorWithSuperArgs (int value) { } + } } From a67f4a38e836ea4f969c9c4335dacdc4419dbedf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:05:11 +0100 Subject: [PATCH 33/43] [Export] marshal method body generation Generate full marshal method bodies for [Export] methods and constructors instead of skipping them or using TypeManager.Activate. For [Export] methods, the UCO wrapper emits: - BeginMarshalMethod guard - GetObject to retrieve the managed peer - Parameter unmarshaling (primitives pass through, strings via JNIEnv.GetString, objects via GetObject) - Managed method call - Return marshaling (strings via JNIEnv.NewString, objects via JNIEnv.ToLocalJniHandle) - catch/finally with OnUserUnhandledException and EndMarshalMethod For [Export] constructors, the wrapper additionally calls ActivateInstance before GetObject to create the peer, then calls the user constructor body. JCW Java output now uses nctor_N native methods for ALL constructors (both [Register] and [Export]), removing the TypeManager.Activate codepath entirely. Changes: - TypeMapAssemblyEmitter: EmitExportMarshalMethod with full IL try/catch/finally using ControlFlowBuilder - ModelBuilder: [Export] methods/ctors populate ExportMarshalMethods list with managed parameter and return type info - JcwJavaSourceGenerator: Removed WriteTypeManagerActivate, unified constructor handling - Scanner: Captures ManagedReturnType for [Export] methods - 5 new emitter-level tests verifying export assembly generation - Updated JCW and model tests for new patterns --- .../Generator/JcwJavaSourceGenerator.cs | 47 +- .../Generator/JniSignatureHelper.cs | 7 + .../Generator/Model/TypeMapAssemblyData.cs | 55 ++- .../Generator/ModelBuilder.cs | 119 +++-- .../Generator/TypeMapAssemblyEmitter.cs | 433 +++++++++++++++++- .../Scanner/JavaPeerInfo.cs | 6 + .../Scanner/JavaPeerScanner.cs | 7 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 77 +--- .../TypeMapAssemblyGeneratorTests.cs | 136 ++++++ .../Generator/TypeMapModelBuilderTests.cs | 45 +- 10 files changed, 801 insertions(+), 131 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 8e6a45bd3ff..b09a9aca576 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -156,28 +156,21 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) writer.Write (simpleClassName); writer.Write (".class) "); - if (ctor.IsExport) { - // [Export] constructors use TypeManager.Activate - WriteTypeManagerActivate (type, ctor.Parameters, writer); - } else { - // [Register] constructors use native nctor_N methods - writer.Write ("nctor_"); - writer.Write (ctor.ConstructorIndex); - writer.Write (" ("); - WriteArgumentList (ctor.Parameters, writer); - writer.Write (')'); - } + // Both [Register] and [Export] constructors use native nctor_N methods. + // The .NET side generates a UCO wrapper with the full marshal body. + writer.Write ("nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteArgumentList (ctor.Parameters, writer); + writer.Write (')'); writer.WriteLine (";"); writer.WriteLine ("\t}"); writer.WriteLine (); } - // Write native constructor declarations (only for [Register] constructors) + // Write native constructor declarations foreach (var ctor in type.JavaConstructors) { - if (ctor.IsExport) { - continue; - } writer.Write ("\tprivate native void nctor_"); writer.Write (ctor.ConstructorIndex); writer.Write (" ("); @@ -190,30 +183,6 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) } } - /// - /// Writes: mono.android.TypeManager.Activate ("ManagedType, Assembly", "param types", this, new java.lang.Object[] { p0, p1 }) - /// - static void WriteTypeManagerActivate (JavaPeerInfo type, IReadOnlyList parameters, TextWriter writer) - { - writer.Write ("mono.android.TypeManager.Activate (\""); - writer.Write (type.ManagedTypeName); - writer.Write (", "); - writer.Write (type.AssemblyName); - writer.Write ("\", \""); - - // Managed parameter type signature - for (int i = 0; i < parameters.Count; i++) { - if (i > 0) { - writer.Write (", "); - } - writer.Write (parameters [i].ManagedType); - } - - writer.Write ("\", this, new java.lang.Object[] { "); - WriteArgumentList (parameters, writer); - writer.Write (" })"); - } - static void WriteMethods (JavaPeerInfo type, TextWriter writer) { foreach (var method in type.MarshalMethods) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 5d96ddb291c..66aa6ff12e9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -85,6 +85,13 @@ static JniParamKind ParseSingleType (string sig, ref int i) } } + /// Parses a standalone JNI type descriptor like "I", "Ljava/lang/String;", "[B". + public static JniParamKind ParseSingleTypeFromDescriptor (string descriptor) + { + int i = 0; + return ParseSingleType (descriptor, ref i); + } + static void SkipSingleType (string sig, ref int i) { switch (sig [i]) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index c61b5a9393b..56edd9ea7b9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -89,12 +89,15 @@ sealed class JavaPeerProxyData /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). public bool IsAcw { get; set; } - /// UCO method wrappers for marshal methods (non-constructor). + /// UCO method wrappers for marshal methods (non-constructor) with [Register]. public List UcoMethods { get; } = new (); - /// UCO constructor wrappers. + /// UCO constructor wrappers for [Register] constructors. public List UcoConstructors { get; } = new (); + /// Export marshal method wrappers — full marshal body for [Export] methods and constructors. + public List ExportMarshalMethods { get; } = new (); + /// RegisterNatives registrations (method name, JNI signature, wrapper name). public List NativeRegistrations { get; } = new (); } @@ -148,6 +151,54 @@ sealed class UcoConstructorData public string JniSignature { get; set; } = "()V"; } +/// +/// An [UnmanagedCallersOnly] static wrapper for an [Export] method or constructor. +/// Unlike which just forwards to an existing n_* callback, +/// this generates the full marshal method body: BeginMarshalMethod, GetObject, param +/// unmarshaling, managed method call, return marshaling, exception handling, EndMarshalMethod. +/// +sealed class ExportMarshalMethodData +{ + /// Name of the generated wrapper method, e.g., "n_myMethod_uco_0" or "nctor_0_uco". + public string WrapperName { get; set; } = ""; + + /// Name of the managed method to call, e.g., "MyMethod" or ".ctor". + public string ManagedMethodName { get; set; } = ""; + + /// Type containing the managed method (the user's type). + public TypeRefData DeclaringType { get; set; } = new (); + + /// JNI method signature, e.g., "(Ljava/lang/String;I)V". + public string JniSignature { get; set; } = ""; + + /// True if this is a constructor. + public bool IsConstructor { get; set; } + + /// + /// Managed parameter types for the managed method call. + /// Each entry is the assembly-qualified managed type name. + /// + public List ManagedParameters { get; } = new (); + + /// Managed return type (assembly-qualified). Null/empty for void or constructors. + public string? ManagedReturnType { get; set; } +} + +/// +/// Describes a parameter for an [Export] marshal method, with both JNI and managed type info. +/// +sealed class ExportParamData +{ + /// JNI type descriptor, e.g., "Ljava/lang/String;", "I". + public string JniType { get; set; } = ""; + + /// Managed type name (assembly-qualified), e.g., "System.String, System.Private.CoreLib". + public string ManagedTypeName { get; set; } = ""; + + /// Assembly containing the managed type. + public string AssemblyName { get; set; } = ""; +} + /// /// One JNI native method registration in RegisterNatives. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index c03b89beeca..cd8f2627834 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -86,6 +86,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri foreach (var uco in proxy.UcoMethods) { AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName); } + foreach (var export in proxy.ExportMarshalMethods) { + AddIfCrossAssembly (referencedAssemblies, export.DeclaringType.AssemblyName, assemblyName); + } if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } @@ -235,21 +238,24 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) continue; } - // [Export] methods have no n_* callback on the declaring type — skip them. - // TODO: generate full marshal method body for [Export] methods (parameter marshaling + managed call) + string wrapperName = $"n_{mm.JniName}_uco_{ucoIndex}"; + if (mm.Connector == null) { - continue; + // [Export] method — generate full marshal body + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, isConstructor: false); + proxy.ExportMarshalMethods.Add (exportData); + } else { + // [Register] method — forward to existing n_* callback + proxy.UcoMethods.Add (new UcoMethodData { + WrapperName = wrapperName, + CallbackMethodName = mm.NativeCallbackName, + CallbackType = new TypeRefData { + ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, + AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + }); } - - proxy.UcoMethods.Add (new UcoMethodData { - WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", - CallbackMethodName = mm.NativeCallbackName, - CallbackType = new TypeRefData { - ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, - AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, - }, - JniSignature = mm.JniSignature, - }); ucoIndex++; } } @@ -260,30 +266,36 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) return; } - // Build a set of [Register] constructor signatures (Connector != null). - // [Export] constructors (Connector == null) don't get UCO wrappers — - // they use TypeManager.Activate in the JCW instead. - // TODO: generate full marshal body for [Export] constructors - var registerCtorSignatures = new HashSet (StringComparer.Ordinal); + // Index marshal methods by JNI signature for lookup + var marshalMethodsBySignature = new Dictionary (StringComparer.Ordinal); foreach (var mm in peer.MarshalMethods) { - if (mm.IsConstructor && mm.Connector != null) { - registerCtorSignatures.Add (mm.JniSignature); + if (mm.IsConstructor) { + marshalMethodsBySignature [mm.JniSignature] = mm; } } foreach (var ctor in peer.JavaConstructors) { - if (!registerCtorSignatures.Contains (ctor.JniSignature)) { + if (!marshalMethodsBySignature.TryGetValue (ctor.JniSignature, out var mm)) { continue; } - proxy.UcoConstructors.Add (new UcoConstructorData { - WrapperName = $"nctor_{ctor.ConstructorIndex}_uco", - JniSignature = ctor.JniSignature, - TargetType = new TypeRefData { - ManagedTypeName = peer.ManagedTypeName, - AssemblyName = peer.AssemblyName, - }, - }); + string wrapperName = $"nctor_{ctor.ConstructorIndex}_uco"; + + if (mm.Connector == null) { + // [Export] constructor — generate full marshal body + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, isConstructor: true); + proxy.ExportMarshalMethods.Add (exportData); + } else { + // [Register] constructor — ActivateInstance pattern + proxy.UcoConstructors.Add (new UcoConstructorData { + WrapperName = wrapperName, + JniSignature = ctor.JniSignature, + TargetType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + }); + } } } @@ -310,6 +322,55 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) WrapperMethodName = uco.WrapperName, }); } + + foreach (var export in proxy.ExportMarshalMethods) { + string jniName = export.WrapperName; + int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); + if (ucoSuffix >= 0) { + jniName = jniName.Substring (0, ucoSuffix); + } + + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = jniName, + JniSignature = export.JniSignature, + WrapperMethodName = export.WrapperName, + }); + } + } + + static ExportMarshalMethodData BuildExportMarshalMethod (MarshalMethodInfo mm, JavaPeerInfo peer, + string wrapperName, bool isConstructor) + { + var data = new ExportMarshalMethodData { + WrapperName = wrapperName, + ManagedMethodName = mm.ManagedMethodName, + DeclaringType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + IsConstructor = isConstructor, + ManagedReturnType = mm.ManagedReturnType, + }; + + foreach (var param in mm.Parameters) { + // Parse assembly name from assembly-qualified name "TypeName, AssemblyName" + string managedTypeName = param.ManagedType; + string assemblyName = peer.AssemblyName; + int commaIndex = managedTypeName.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex >= 0) { + assemblyName = managedTypeName.Substring (commaIndex + 2); + managedTypeName = managedTypeName.Substring (0, commaIndex); + } + + data.ManagedParameters.Add (new ExportParamData { + JniType = param.JniType, + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + } + + return data; } static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index e7dbad2f7c4..3053c73d5ca 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -40,6 +40,13 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; + TypeReferenceHandle _jniEnvironmentRef; + TypeReferenceHandle _jniTransitionRef; + TypeReferenceHandle _jniRuntimeRef; + TypeReferenceHandle _javaLangObjectRef; + TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _systemExceptionRef; + TypeReferenceHandle _iJavaObjectRef; MemberReferenceHandle _baseCtorRef; MemberReferenceHandle _getTypeFromHandleRef; @@ -52,6 +59,12 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; + MemberReferenceHandle _beginMarshalMethodRef; + MemberReferenceHandle _endMarshalMethodRef; + MemberReferenceHandle _onUserUnhandledExceptionRef; + MemberReferenceHandle _jniEnvGetStringRef; + MemberReferenceHandle _jniEnvNewStringRef; + MemberReferenceHandle _jniEnvToLocalJniHandleRef; /// /// Creates a new emitter. @@ -167,6 +180,20 @@ void EmitTypeReferences (MetadataBuilder metadata) metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_systemRuntimeRef, metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + _jniEnvironmentRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniEnvironment")); + _jniTransitionRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniTransition")); + _jniRuntimeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime")); + _javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + _jniEnvRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); + _systemExceptionRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); + _iJavaObjectRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaObject")); } void EmitMemberReferences (MetadataBuilder metadata) @@ -219,6 +246,53 @@ void EmitMemberReferences (MetadataBuilder metadata) _attrBlob.WriteUInt16 (0); _ucoAttrBlobHandle = metadata.GetOrAddBlob (_attrBlob); + // Marshal method support: BeginMarshalMethod, EndMarshalMethod, GetObject, GetString, etc. + // BeginMarshalMethod(IntPtr jnienv, out JniTransition, out JniRuntime) : bool + _beginMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "BeginMarshalMethod", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Boolean (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type (isByRef: true).Type (_jniRuntimeRef, false); + })); + + // EndMarshalMethod(ref JniTransition) + _endMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "EndMarshalMethod", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true))); + + // JniRuntime.OnUserUnhandledException(ref JniTransition, Exception) — virtual instance method + _onUserUnhandledExceptionRef = AddMemberRef (metadata, _jniRuntimeRef, "OnUserUnhandledException", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type ().Type (_systemExceptionRef, false); + })); + + // JNIEnv.GetString(IntPtr, JniHandleOwnership) : string + _jniEnvGetStringRef = AddMemberRef (metadata, _jniEnvRef, "GetString", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + // JNIEnv.NewString(string) : IntPtr + _jniEnvNewStringRef = AddMemberRef (metadata, _jniEnvRef, "NewString", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().String ())); + + // JNIEnv.ToLocalJniHandle(IJavaObject) : IntPtr + _jniEnvToLocalJniHandleRef = AddMemberRef (metadata, _jniEnvRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (_iJavaObjectRef, false))); + EmitTypeMapAttributeCtorRef (metadata); EmitTypeMapAssociationAttributeCtorRef (metadata); } @@ -338,6 +412,12 @@ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerPro wrapperHandles [uco.WrapperName] = handle; } + // Export marshal method wrappers (full marshal body) + foreach (var export in proxy.ExportMarshalMethods) { + var handle = EmitExportMarshalMethod (metadata, ilBuilder, export); + wrapperHandles [export.WrapperName] = handle; + } + // RegisterNatives if (proxy.IsAcw) { EmitRegisterNatives (metadata, ilBuilder, proxy.NativeRegistrations, wrapperHandles); @@ -526,7 +606,358 @@ MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder return handle; } - // ---- RegisterNatives ---- + // ---- Export marshal method wrappers ---- + + /// + /// Emits a full marshal method body for an [Export] method or constructor. + /// Pattern: + /// static RetType n_Method(IntPtr jnienv, IntPtr native__this, ) { + /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return default; + /// try { + /// var __this = Object.GetObject<T>(jnienv, native__this, DoNotTransfer); + /// // unmarshal params, call managed method, marshal return + /// } catch (Exception __e) { + /// __r.OnUserUnhandledException(ref __envp, __e); + /// return default; + /// } finally { + /// JniEnvironment.EndMarshalMethod(ref __envp); + /// } + /// } + /// + MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, ExportMarshalMethodData export) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (export.JniSignature); + var returnKind = export.IsConstructor ? JniParamKind.Void : JniSignatureHelper.ParseReturnType (export.JniSignature); + bool isVoid = returnKind == JniParamKind.Void; + int jniParamCount = 2 + jniParams.Count; // jnienv + self + method params + + // Build the method signature + Action encodeSig = sig => sig.MethodSignature ().Parameters (jniParamCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // native__this + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }); + + // Build the locals signature: JniTransition __envp (0), JniRuntime __r (1), Exception __e (2) + var localsBlob = new BlobBuilder (32); + var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (3); + localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0: JniTransition __envp + localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1: JniRuntime __r + localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2: Exception __e + var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob)); + + // Resolve managed type references + var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType); + + // Build GetObject method spec — generic instantiation of Object.GetObject + var getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef); + + // Resolve managed method to call + MemberReferenceHandle managedMethodRef; + if (export.IsConstructor) { + managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef); + } else { + managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef); + } + + // Build the IL with ControlFlowBuilder for try/catch/finally + var cfBuilder = new ControlFlowBuilder (); + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob, cfBuilder); + + // Define labels + var tryStartLabel = encoder.DefineLabel (); + var tryEndLabel = encoder.DefineLabel (); + var catchStartLabel = encoder.DefineLabel (); + var catchEndLabel = encoder.DefineLabel (); + var finallyStartLabel = encoder.DefineLabel (); + var finallyEndLabel = encoder.DefineLabel (); + var returnLabel = encoder.DefineLabel (); + + // --- if (!BeginMarshalMethod(jnienv, out __envp, out __r)) return default; --- + encoder.LoadArgument (0); // jnienv + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // out __envp + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (1); // out __r + encoder.Call (_beginMarshalMethodRef); + encoder.Branch (ILOpCode.Brtrue_s, tryStartLabel); + // return default + if (!isVoid) { + EmitDefaultReturnValue (encoder, returnKind); + } + encoder.OpCode (ILOpCode.Ret); + + // --- try { --- + encoder.MarkLabel (tryStartLabel); + + if (export.IsConstructor) { + // For constructors: ActivateInstance first, then get the managed object and call ctor + // ActivateInstance(native__this, typeof(T)) + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (declaringTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_activateInstanceRef); + } + + // var __this = Object.GetObject(jnienv, native__this, DoNotTransfer); + encoder.LoadArgument (0); // jnienv + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 + encoder.Call (getObjectRef); + + // Unmarshal each parameter + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); + } + + // Call managed method + if (export.IsConstructor) { + encoder.Call (managedMethodRef); + } else { + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (managedMethodRef); + } + + // Marshal return value + if (!isVoid) { + EmitReturnMarshal (encoder, returnKind, export.ManagedReturnType); + } + + // leave to after the handler + encoder.Branch (ILOpCode.Leave_s, returnLabel); + encoder.MarkLabel (tryEndLabel); + + // --- } catch (Exception __e) { --- + encoder.MarkLabel (catchStartLabel); + encoder.OpCode (ILOpCode.Stloc_2); // store exception in local 2 + encoder.OpCode (ILOpCode.Ldloc_1); // __r + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp + encoder.OpCode (ILOpCode.Ldloc_2); // __e + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (_onUserUnhandledExceptionRef); + if (!isVoid) { + EmitDefaultReturnValue (encoder, returnKind); + } + encoder.Branch (ILOpCode.Leave_s, returnLabel); + encoder.MarkLabel (catchEndLabel); + + // --- } finally { --- + encoder.MarkLabel (finallyStartLabel); + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp + encoder.Call (_endMarshalMethodRef); + encoder.OpCode (ILOpCode.Endfinally); + encoder.MarkLabel (finallyEndLabel); + + // --- return --- + encoder.MarkLabel (returnLabel); + encoder.OpCode (ILOpCode.Ret); + + // Add exception regions + cfBuilder.AddCatchRegion (tryStartLabel, tryEndLabel, catchStartLabel, catchEndLabel, _systemExceptionRef); + cfBuilder.AddFinallyRegion (tryStartLabel, catchEndLabel, finallyStartLabel, finallyEndLabel); + + // Emit the method with fat body (locals + exception handlers) + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + + while (ilBuilder.Count % 4 != 0) { + ilBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); + int bodyOffset = bodyEncoder.AddMethodBody (encoder, maxStack: 8, localsSigHandle, MethodBodyAttributes.InitLocals); + + var handle = metadata.AddMethodDefinition ( + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + MethodImplAttributes.IL, + metadata.GetOrAddString (export.WrapperName), + metadata.GetOrAddBlob (_sigBlob), + bodyOffset, default); + + AddUnmanagedCallersOnlyAttribute (metadata, handle); + return handle; + } + + /// + /// Builds a MethodSpec for Object.GetObject<T>(IntPtr, IntPtr, JniHandleOwnership). + /// + EntityHandle BuildGetObjectMethodSpec (MetadataBuilder metadata, EntityHandle managedTypeRef) + { + // Object.GetObject(IntPtr jnienv, IntPtr handle, JniHandleOwnership transfer) : T + var openGetObjectRef = AddMemberRef (metadata, _javaLangObjectRef, "GetObject", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (3, + rt => rt.Type ().GenericMethodTypeParameter (0), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }); + }); + + // Build generic instantiation blob: GetObject + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (for method) + instBlob.WriteCompressedInteger (1); // 1 type argument + instBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (managedTypeRef)); + + return metadata.AddMethodSpecification (openGetObjectRef, metadata.GetOrAddBlob (instBlob)); + } + + MemberReferenceHandle BuildExportCtorRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef) + { + int paramCount = export.ManagedParameters.Count; + return AddMemberRef (metadata, declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (paramCount, + rt => rt.Void (), + p => { + foreach (var param in export.ManagedParameters) + EncodeExportParamType (p, metadata, param); + })); + } + + MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef) + { + int paramCount = export.ManagedParameters.Count; + var returnKind = JniSignatureHelper.ParseReturnType (export.JniSignature); + bool isVoid = returnKind == JniParamKind.Void; + + return AddMemberRef (metadata, declaringTypeRef, export.ManagedMethodName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (paramCount, + rt => { + if (isVoid) { + rt.Void (); + } else { + EncodeExportReturnType (rt, metadata, export.ManagedReturnType, returnKind); + } + }, + p => { + foreach (var param in export.ManagedParameters) + EncodeExportParamType (p, metadata, param); + })); + } + + void EncodeExportParamType (ParametersEncoder p, MetadataBuilder metadata, ExportParamData param) + { + var jniKind = JniSignatureHelper.ParseSingleTypeFromDescriptor (param.JniType); + if (jniKind != JniParamKind.Object) { + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniKind); + } else if (param.ManagedTypeName == "System.String") { + p.AddParameter ().Type ().String (); + } else { + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = param.ManagedTypeName, + AssemblyName = param.AssemblyName, + }); + p.AddParameter ().Type ().Type (typeRef, false); + } + } + + void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, string? managedReturnType, JniParamKind returnKind) + { + if (returnKind != JniParamKind.Object) { + JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); + } else if (managedReturnType == "System.String") { + rt.Type ().String (); + } else { + // For object returns, the JNI return type is IntPtr + rt.Type ().IntPtr (); + } + } + + void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, JniParamKind jniKind, int argIndex) + { + if (jniKind != JniParamKind.Object) { + // Primitives: just load the argument directly + encoder.LoadArgument (argIndex); + return; + } + + if (param.ManagedTypeName == "System.String") { + // String: JNIEnv.GetString(handle, DoNotTransfer) + encoder.LoadArgument (argIndex); + encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer + encoder.Call (_jniEnvGetStringRef); + return; + } + + // Java object: Object.GetObject(handle, DoNotTransfer) + // Use the 2-arg overload (without jnienv) + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = param.ManagedTypeName, + AssemblyName = param.AssemblyName, + }); + var getObjectRef2 = AddMemberRef (metadata, _javaLangObjectRef, "GetObject", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (2, + rt => rt.Type ().GenericMethodTypeParameter (0), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }); + }); + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); + instBlob.WriteCompressedInteger (1); + instBlob.WriteByte (0x12); + instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef)); + var methodSpec = metadata.AddMethodSpecification (getObjectRef2, metadata.GetOrAddBlob (instBlob)); + + encoder.LoadArgument (argIndex); + encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer + encoder.Call (methodSpec); + } + + void EmitReturnMarshal (InstructionEncoder encoder, JniParamKind returnKind, string? managedReturnType) + { + if (returnKind != JniParamKind.Object) { + // Primitives: return directly (value is already on the stack) + return; + } + + if (managedReturnType == "System.String") { + // String: JNIEnv.NewString(result) + encoder.Call (_jniEnvNewStringRef); + return; + } + + // Java object: JNIEnv.ToLocalJniHandle(result) + encoder.Call (_jniEnvToLocalJniHandleRef); + } + + static void EmitDefaultReturnValue (InstructionEncoder encoder, JniParamKind kind) + { + switch (kind) { + case JniParamKind.Boolean: + case JniParamKind.Byte: + case JniParamKind.Char: + case JniParamKind.Short: + case JniParamKind.Int: + encoder.OpCode (ILOpCode.Ldc_i4_0); + break; + case JniParamKind.Long: + encoder.OpCode (ILOpCode.Ldc_i8); + encoder.CodeBuilder.WriteInt64 (0); + break; + case JniParamKind.Float: + encoder.OpCode (ILOpCode.Ldc_r4); + encoder.CodeBuilder.WriteSingle (0); + break; + case JniParamKind.Double: + encoder.OpCode (ILOpCode.Ldc_r8); + encoder.CodeBuilder.WriteDouble (0); + break; + case JniParamKind.Object: + encoder.OpCode (ILOpCode.Ldc_i4_0); + encoder.OpCode (ILOpCode.Conv_i); // IntPtr.Zero + break; + } + } void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, List registrations, Dictionary wrapperHandles) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index f4646f341c8..f758780c68c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -178,6 +178,12 @@ sealed record MarshalMethodInfo /// Null for [Register] methods. /// public string? SuperArgumentsString { get; init; } + + /// + /// For [Export] methods: managed return type name, e.g., "System.String". + /// Null for [Register] methods and constructors. + /// + public string? ManagedReturnType { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index ed0449d9af8..806560d9918 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -282,12 +282,16 @@ static void AddMarshalMethod (List methods, RegisterInfo regi var parameters = ParseJniParameters (jniSignature); // For [Export] methods, populate ManagedType from the actual method signature - // (needed for TypeManager.Activate call in JCW) + // (needed for the generated marshal method body) + string? managedReturnType = null; if (registerInfo.Connector == null) { var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); for (int i = 0; i < parameters.Count && i < sig.ParameterTypes.Length; i++) { parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i]); } + if (sig.ReturnType != "System.Void") { + managedReturnType = sig.ReturnType; + } } methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, @@ -300,6 +304,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, + ManagedReturnType = managedReturnType, }); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index c6a1fe077b7..04281f06884 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -431,46 +431,22 @@ public class ExportConstructor { [Fact] - public void Generate_ExportConstructors_UsesTypeManagerActivate () + public void Generate_ExportConstructors_UsesNativeCtorMethods () { var peers = ScanFixtures (); var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); var java = GenerateToString (peer); - // [Export] constructors should use TypeManager.Activate - Assert.Contains ("mono.android.TypeManager.Activate (\"", java); - - // Should NOT have nctor_N native declarations - Assert.DoesNotContain ("nctor_", java); - } - - [Fact] - public void Generate_ExportConstructors_ParameterlessCtorHasEmptySignature () - { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); - var java = GenerateToString (peer); - - // Parameterless [Export] ctor should have empty managed param signature - Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"\", this, new java.lang.Object[] { })", java); - } - - [Fact] - public void Generate_ExportConstructors_IntCtorHasIntSignature () - { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); - var java = GenerateToString (peer); + // [Export] constructors should use nctor_N native methods (same as [Register]) + Assert.Contains ("nctor_0 ()", java); + Assert.Contains ("nctor_1 (int p0)", java); + Assert.Contains ("private native void nctor_0 ()", java); + Assert.Contains ("private native void nctor_1 (int p0)", java); - // int parameter [Export] ctor should have managed type signature - Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"System.Int32, System.Private.CoreLib\", this, new java.lang.Object[] { p0 })", java); + // Should NOT use TypeManager.Activate + Assert.DoesNotContain ("TypeManager.Activate", java); } - /// - /// Full output comparison — ported from legacy GenerateConstructors. - /// Verifies the complete JCW for [Export] constructors matches the - /// TypeManager.Activate pattern with correct activation guard. - /// [Fact] public void Generate_ExportConstructors_FullOutput () { @@ -478,14 +454,11 @@ public void Generate_ExportConstructors_FullOutput () var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); var java = GenerateToString (peer); - // Parameterless ctor: super(), then TypeManager.Activate with empty sig - Assert.Contains ("\tpublic ExportsConstructors ()\n\t{\n\t\tsuper ();\n\t\tif (getClass () == ExportsConstructors.class) mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"\", this, new java.lang.Object[] { });\n\t}\n", java); - - // int ctor: super(p0), then TypeManager.Activate with int sig - Assert.Contains ("\tpublic ExportsConstructors (int p0)\n\t{\n\t\tsuper (p0);\n\t\tif (getClass () == ExportsConstructors.class) mono.android.TypeManager.Activate (\"MyApp.ExportsConstructors, TestFixtures\", \"System.Int32, System.Private.CoreLib\", this, new java.lang.Object[] { p0 });\n\t}\n", java); + // Parameterless ctor: super(), then nctor_0 + Assert.Contains ("\tpublic ExportsConstructors ()\n\t{\n\t\tsuper ();\n\t\tif (getClass () == ExportsConstructors.class) nctor_0 ();\n\t}\n", java); - // No nctor native declarations - Assert.DoesNotContain ("private native void nctor_", java); + // int ctor: super(p0), then nctor_1 + Assert.Contains ("\tpublic ExportsConstructors (int p0)\n\t{\n\t\tsuper (p0);\n\t\tif (getClass () == ExportsConstructors.class) nctor_1 (p0);\n\t}\n", java); } /// @@ -508,11 +481,11 @@ public void Generate_ExportThrowsConstructors_FullOutput () // string ctor WITHOUT throws (empty ThrownNames in legacy means [Export] with no Throws) Assert.Contains ("\tpublic ExportsThrowsConstructors (java.lang.String p0)\n\t{\n\t\tsuper (p0);\n", java); - // String ctor should use TypeManager.Activate with String sig - Assert.Contains ("\"System.String, System.Private.CoreLib\"", java); - - // No nctor native declarations - Assert.DoesNotContain ("private native void nctor_", java); + // All ctors should use nctor_N, not TypeManager.Activate + Assert.Contains ("nctor_0 ()", java); + Assert.Contains ("nctor_1 (int p0)", java); + Assert.Contains ("nctor_2 (java.lang.String p0)", java); + Assert.DoesNotContain ("TypeManager.Activate", java); } [Fact] @@ -557,15 +530,14 @@ public void Generate_MixedRegisterAndExportConstructors_HandledCorrectly () var java = GenerateToString (type); - // [Register] ctor should use nctor_0 + // Both [Register] and [Export] ctors should use nctor_N Assert.Contains ("nctor_0 ()", java); + Assert.Contains ("nctor_1 (int p0)", java); Assert.Contains ("private native void nctor_0 ()", java); + Assert.Contains ("private native void nctor_1 (int p0)", java); - // [Export] ctor should use TypeManager.Activate - Assert.Contains ("mono.android.TypeManager.Activate (\"MyApp.MixedCtors, App\"", java); - - // Only nctor_0 declaration (not nctor_1 for [Export]) - Assert.DoesNotContain ("nctor_1", java); + // No TypeManager.Activate + Assert.DoesNotContain ("TypeManager.Activate", java); } [Fact] @@ -579,8 +551,9 @@ public void Generate_ExportCtorWithSuperArgs_UsesCustomSuperArgs () Assert.Contains ("super ();", java); Assert.DoesNotContain ("super (p0);", java); - // Should still use TypeManager.Activate - Assert.Contains ("mono.android.TypeManager.Activate (\"", java); + // Should use nctor_N, not TypeManager.Activate + Assert.Contains ("nctor_0 (int p0)", java); + Assert.DoesNotContain ("TypeManager.Activate", java); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index c05557aa942..3f88d9e3610 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -290,6 +290,142 @@ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () } + public class ExportMarshalMethods + { + + [Fact] + public void Generate_ExportMethod_ProducesValidAssembly () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams"); + var path = GenerateAssembly (new [] { peer }, "ExportMethodTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + Assert.True (reader.TypeDefinitions.Count > 0); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportMethod_HasWrapperMethods () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams"); + var path = GenerateAssembly (new [] { peer }, "ExportMethodWrappers"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMethodWithParams_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + // Export methods should produce UCO wrappers + Assert.Contains (methods, m => m.StartsWith ("n_") && m.Contains ("_uco")); + Assert.Contains ("RegisterNatives", methods); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportConstructor_HasWrapperMethods () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportsConstructors"); + var path = GenerateAssembly (new [] { peer }, "ExportCtorWrappers"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportsConstructors_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + // Export constructors should produce nctor_N_uco wrappers + Assert.Contains (methods, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + Assert.Contains ("RegisterNatives", methods); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportMethod_HasMethodBody () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams"); + var path = GenerateAssembly (new [] { peer }, "ExportMethodBody"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMethodWithParams_Proxy"); + + // Export marshal methods should have non-zero RVA (they have a real body) + var exportWrappers = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => { + var name = reader.GetString (m.Name); + return name.StartsWith ("n_") && name.Contains ("_uco"); + }) + .ToList (); + + Assert.NotEmpty (exportWrappers); + foreach (var wrapper in exportWrappers) { + Assert.True (wrapper.RelativeVirtualAddress > 0, + $"Export marshal method '{reader.GetString (wrapper.Name)}' should have a method body (non-zero RVA)"); + } + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportMembersComprehensive_ProducesValidAssembly () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive"); + var path = GenerateAssembly (new [] { peer }, "ExportComprehensive"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + // Should have multiple export wrappers + var exportWrappers = methods.Where (m => m.StartsWith ("n_") && m.Contains ("_uco")).ToList (); + Assert.True (exportWrappers.Count >= 2, + $"Expected at least 2 export wrappers, got {exportWrappers.Count}: [{string.Join (", ", exportWrappers)}]"); + } + } finally { + CleanUp (path); + } + } + + } + public class Ignoresaccesschecksto { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index db4e5b15f3e..bb390e1d5f8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1323,19 +1323,20 @@ public void Fixture_ExportExample_IsAcw () } [Fact] - public void Fixture_ExportExample_ExportMethodNotInUcoMethods () + public void Fixture_ExportExample_ExportMethodInExportMarshalMethods () { var peer = FindFixtureByJavaName ("my/app/ExportExample"); var model = BuildModel (new [] { peer }, "TypeMap"); var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); Assert.NotNull (proxy); - // [Export] methods have no n_* callback → must NOT generate UCO wrappers + // [Export] methods go into ExportMarshalMethods, not UcoMethods Assert.Empty (proxy!.UcoMethods); + Assert.NotEmpty (proxy.ExportMarshalMethods); } [Fact] - public void Fixture_ExportMethodWithParams_ExportMethodsNotInUcoMethods () + public void Fixture_ExportMethodWithParams_ExportMethodsInExportMarshalMethods () { var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); Assert.Equal (2, peer.MarshalMethods.Count); @@ -1346,10 +1347,11 @@ public void Fixture_ExportMethodWithParams_ExportMethodsNotInUcoMethods () var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); Assert.NotNull (proxy); Assert.Empty (proxy!.UcoMethods); + Assert.Equal (2, proxy.ExportMarshalMethods.Count); } [Fact] - public void Fixture_ExportsConstructors_NoUcoConstructors () + public void Fixture_ExportsConstructors_ExportConstructorsInExportMarshalMethods () { var peer = FindFixtureByJavaName ("my/app/ExportsConstructors"); // Should have [Export] constructors (Connector == null) @@ -1360,20 +1362,49 @@ public void Fixture_ExportsConstructors_NoUcoConstructors () var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsConstructors_Proxy"); Assert.NotNull (proxy); - // [Export] constructors should NOT generate UCO constructor wrappers + // [Export] constructors go into ExportMarshalMethods, not UcoConstructors Assert.Empty (proxy!.UcoConstructors); + Assert.Equal (exportCtors.Count, proxy.ExportMarshalMethods.Count (e => e.IsConstructor)); } [Fact] - public void Fixture_ExportsThrowsConstructors_NoUcoConstructors () + public void Fixture_ExportsThrowsConstructors_ExportConstructorsInExportMarshalMethods () { var peer = FindFixtureByJavaName ("my/app/ExportsThrowsConstructors"); var model = BuildModel (new [] { peer }, "TypeMap"); var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsThrowsConstructors_Proxy"); Assert.NotNull (proxy); - // All constructors are [Export] → no UCO constructors + // All constructors are [Export] → in ExportMarshalMethods, not UcoConstructors Assert.Empty (proxy!.UcoConstructors); + Assert.NotEmpty (proxy.ExportMarshalMethods); + Assert.All (proxy.ExportMarshalMethods, e => Assert.True (e.IsConstructor)); + } + + [Fact] + public void Fixture_ExportMethodWithParams_HasNativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + + // [Export] methods should generate NativeRegistrations entries + Assert.Equal (2, proxy!.NativeRegistrations.Count); + } + + [Fact] + public void Fixture_ExportMarshalMethod_HasCorrectManagedParameters () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + + // The method with (String, int) params should have correct managed types + var exportMethod = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedParameters.Count == 2); + Assert.NotNull (exportMethod); + Assert.False (exportMethod!.IsConstructor); } } From 7917bc251e90d81d30ddb79925cc1dd76976bfd4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:16:54 +0100 Subject: [PATCH 34/43] Fix return value loss in export marshal methods The 'leave' IL instruction clears the evaluation stack, so non-void export methods were losing their return values. Add a local variable (local 3) to store the return value before 'leave' and load it before 'ret'. Void methods are unaffected (3 locals as before). --- .../Generator/TypeMapAssemblyEmitter.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 3053c73d5ca..30095723cd8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -641,12 +641,20 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); - // Build the locals signature: JniTransition __envp (0), JniRuntime __r (1), Exception __e (2) + // Build the locals signature: + // local 0: JniTransition __envp + // local 1: JniRuntime __r + // local 2: Exception __e + // local 3: __ret (only for non-void methods) + int localCount = isVoid ? 3 : 4; var localsBlob = new BlobBuilder (32); - var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (3); - localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0: JniTransition __envp - localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1: JniRuntime __r - localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2: Exception __e + var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (localCount); + localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0 + localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1 + localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2 + if (!isVoid) { + JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3 + } var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob)); // Resolve managed type references @@ -721,9 +729,10 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.Token (managedMethodRef); } - // Marshal return value + // Marshal return value and store in local 3 if (!isVoid) { EmitReturnMarshal (encoder, returnKind, export.ManagedReturnType); + encoder.OpCode (ILOpCode.Stloc_3); } // leave to after the handler @@ -740,6 +749,7 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.Token (_onUserUnhandledExceptionRef); if (!isVoid) { EmitDefaultReturnValue (encoder, returnKind); + encoder.OpCode (ILOpCode.Stloc_3); } encoder.Branch (ILOpCode.Leave_s, returnLabel); encoder.MarkLabel (catchEndLabel); @@ -753,6 +763,9 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu // --- return --- encoder.MarkLabel (returnLabel); + if (!isVoid) { + encoder.OpCode (ILOpCode.Ldloc_3); + } encoder.OpCode (ILOpCode.Ret); // Add exception regions From 64842263e4de31e40df89585d23f1c539246a398 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:20:59 +0100 Subject: [PATCH 35/43] Fix managed return type encoding in export method refs The managed method ref for [Export] methods with object return types incorrectly encoded the return type as IntPtr instead of the actual managed type. The CLR would fail to find the method at runtime. Also assembly-qualify the ManagedReturnType in the scanner (matching how parameters are already handled) and add a test assertion for it. --- .../Generator/TypeMapAssemblyEmitter.cs | 20 ++++++++++++++++++- .../Scanner/JavaPeerScanner.cs | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 3 +++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 30095723cd8..e31ba9feac5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -876,8 +876,26 @@ void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, str JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); } else if (managedReturnType == "System.String") { rt.Type ().String (); + } else if (managedReturnType != null) { + // Resolve the managed return type for the method ref signature + string typeName = managedReturnType; + string assemblyName = ""; + int commaIndex = managedReturnType.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex >= 0) { + assemblyName = managedReturnType.Substring (commaIndex + 2); + typeName = managedReturnType.Substring (0, commaIndex); + } + if (assemblyName.Length > 0) { + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = typeName, + AssemblyName = assemblyName, + }); + rt.Type ().Type (typeRef, false); + } else { + // Fallback: no assembly info available, use IntPtr + rt.Type ().IntPtr (); + } } else { - // For object returns, the JNI return type is IntPtr rt.Type ().IntPtr (); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 806560d9918..1b0a30ab858 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -290,7 +290,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i]); } if (sig.ReturnType != "System.Void") { - managedReturnType = sig.ReturnType; + managedReturnType = ManagedTypeToAssemblyQualifiedName (sig.ReturnType); } } methods.Add (new MarshalMethodInfo { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index bb390e1d5f8..cbd070584ac 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1405,6 +1405,9 @@ public void Fixture_ExportMarshalMethod_HasCorrectManagedParameters () var exportMethod = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedParameters.Count == 2); Assert.NotNull (exportMethod); Assert.False (exportMethod!.IsConstructor); + + // Verify the string return type is assembly-qualified + Assert.Equal ("System.String, System.Private.CoreLib", exportMethod.ManagedReturnType); } } From e9b13739486827c2cc6537784af63bde8550f16e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:26:23 +0100 Subject: [PATCH 36/43] Fix JNI registration name mismatch for [Export] methods RegisterNatives must use the name matching the JCW native method declaration. For methods, JCW declares 'n_' but RegisterNatives was using 'n_' (stripped from wrapper name). When the export name differs from the managed name (e.g., [Export("attributeOverridesNames")] CompletelyDifferentName), the JNI runtime would fail to connect them. Fix: pass NativeCallbackName explicitly through ExportMarshalMethodData. For methods: 'n_' (matches JCW). For constructors: 'nctor_N' (matches JCW). --- .../Generator/Model/TypeMapAssemblyData.cs | 6 ++++++ .../Generator/ModelBuilder.cs | 16 ++++++-------- .../Generator/TypeMapModelBuilderTests.cs | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 56edd9ea7b9..f81cd7fd213 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -162,6 +162,12 @@ sealed class ExportMarshalMethodData /// Name of the generated wrapper method, e.g., "n_myMethod_uco_0" or "nctor_0_uco". public string WrapperName { get; set; } = ""; + /// + /// JNI method name for RegisterNatives, e.g., "n_DoWork" or "nctor_0". + /// Must match the native method declaration in the Java JCW. + /// + public string NativeCallbackName { get; set; } = ""; + /// Name of the managed method to call, e.g., "MyMethod" or ".ctor". public string ManagedMethodName { get; set; } = ""; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index cd8f2627834..3fe819e81ac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -242,7 +242,7 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) if (mm.Connector == null) { // [Export] method — generate full marshal body - var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, isConstructor: false); + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, mm.NativeCallbackName, isConstructor: false); proxy.ExportMarshalMethods.Add (exportData); } else { // [Register] method — forward to existing n_* callback @@ -280,10 +280,11 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) } string wrapperName = $"nctor_{ctor.ConstructorIndex}_uco"; + string nativeCallbackName = $"nctor_{ctor.ConstructorIndex}"; if (mm.Connector == null) { // [Export] constructor — generate full marshal body - var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, isConstructor: true); + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, nativeCallbackName, isConstructor: true); proxy.ExportMarshalMethods.Add (exportData); } else { // [Register] constructor — ActivateInstance pattern @@ -324,14 +325,8 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } foreach (var export in proxy.ExportMarshalMethods) { - string jniName = export.WrapperName; - int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); - if (ucoSuffix >= 0) { - jniName = jniName.Substring (0, ucoSuffix); - } - proxy.NativeRegistrations.Add (new NativeRegistrationData { - JniMethodName = jniName, + JniMethodName = export.NativeCallbackName, JniSignature = export.JniSignature, WrapperMethodName = export.WrapperName, }); @@ -339,10 +334,11 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } static ExportMarshalMethodData BuildExportMarshalMethod (MarshalMethodInfo mm, JavaPeerInfo peer, - string wrapperName, bool isConstructor) + string wrapperName, string nativeCallbackName, bool isConstructor) { var data = new ExportMarshalMethodData { WrapperName = wrapperName, + NativeCallbackName = nativeCallbackName, ManagedMethodName = mm.ManagedMethodName, DeclaringType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index cbd070584ac..a74659e5fb3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1391,6 +1391,27 @@ public void Fixture_ExportMethodWithParams_HasNativeRegistrations () // [Export] methods should generate NativeRegistrations entries Assert.Equal (2, proxy!.NativeRegistrations.Count); + + // Registration names must match the JCW native method declarations (n_) + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_DoWork"); + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_ComputeName"); + } + + [Fact] + public void Fixture_ExportWithNameOverride_NativeRegistrationUsesCallbackName () + { + // [Export("attributeOverridesNames")] public string CompletelyDifferentName(...) + // JCW declares: native String n_CompletelyDifferentName(...) + // RegisterNatives must use "n_CompletelyDifferentName", NOT "n_attributeOverridesNames" + var peer = FindFixtureByJavaName ("my/app/ExportMembersComprehensive"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMembersComprehensive_Proxy"); + Assert.NotNull (proxy); + + Assert.Contains (proxy!.NativeRegistrations, + r => r.JniMethodName == "n_CompletelyDifferentName"); + Assert.DoesNotContain (proxy.NativeRegistrations, + r => r.JniMethodName == "n_attributeOverridesNames"); } [Fact] From 0b6a063c0b95101487a23c90b1af762160099ac6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:30:15 +0100 Subject: [PATCH 37/43] Fix JCW: no @Override on [Export] methods, use private native [Export] methods are new declarations, not overrides of base class methods. The @Override annotation should only appear on [Register] methods. Also change native method declarations from 'public native' to 'private native' to match legacy JCW output. Added tests: @Override suppression for [Export], private native visibility assertions. --- .../Generator/JcwJavaSourceGenerator.cs | 9 ++++-- .../Generator/JcwJavaSourceGeneratorTests.cs | 32 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index b09a9aca576..2eb7a7f264a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -192,9 +192,12 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) string javaReturnType = JniTypeToJava (method.JniReturnType); bool isVoid = method.JniReturnType == "V"; + bool isExport = method.Connector == null; - // Public override wrapper - writer.Write ("\t@Override\n"); + // Public wrapper method + if (!isExport) { + writer.Write ("\t@Override\n"); + } writer.Write ("\tpublic "); writer.Write (javaReturnType); writer.Write (' '); @@ -230,7 +233,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) writer.Write ("\t}\n"); // Native method declaration - writer.Write ("\tpublic native "); + writer.Write ("\tprivate native "); writer.Write (javaReturnType); writer.Write (' '); writer.Write (method.NativeCallbackName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 04281f06884..2836346f70b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -319,7 +319,7 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () Assert.Contains ("@Override\n", java); Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); Assert.Contains ("n_OnCreate (p0);\n", java); - Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + Assert.Contains ("private native void n_OnCreate (android.os.Bundle p0);\n", java); } [Fact] @@ -638,5 +638,35 @@ public void Generate_ExportMethodWithEmptyThrows_NoThrowsClause () } } + [Fact] + public void Generate_ExportMethod_NoOverrideAnnotation () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + // [Export] methods should NOT have @Override — they are new declarations, not overrides + var lines = java.Split ('\n'); + for (int i = 0; i < lines.Length; i++) { + if (lines [i].Contains ("methodNamesNotMangled ()")) { + // The line before should NOT be @Override + Assert.True (i > 0); + Assert.DoesNotContain ("@Override", lines [i - 1]); + break; + } + } + } + + [Fact] + public void Generate_ExportMethod_NativeDeclarationIsPrivate () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + var java = GenerateToString (peer); + + Assert.Contains ("private native void n_methodNamesNotMangled ()", java); + Assert.Contains ("private native java.lang.String n_CompletelyDifferentName (java.lang.String p0, int p1)", java); + } + } } \ No newline at end of file From 636bfb9f113032f184e7bc326660d424a492f1b6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 19:55:40 +0100 Subject: [PATCH 38/43] Add static [Export] methods and [ExportField] support Static [Export] methods: - Added IsStatic to MarshalMethodInfo (scanner) and ExportMarshalMethodData (model) - Scanner captures MethodAttributes.Static for all methods - JCW emits 'public static' wrapper and 'private static native' declaration - IL emitter: static methods skip GetObject/callvirt, use direct call - Method signature uses isInstanceMethod: false for static exports [ExportField]: - Added ExportFieldInfo data type with FieldName, MethodName, IsStatic - Scanner: ExportFieldAttribute parsed, method registered as marshal method (Connector = null, like [Export]) and field info collected separately - JCW: field declarations emitted after static initializer, before ctors Format: 'public [static] FIELD_NAME = MethodName ();' 17 new tests across scanner, model, emitter, and JCW layers. 299 tests pass. --- .../Generator/JcwJavaSourceGenerator.cs | 32 ++- .../Generator/Model/TypeMapAssemblyData.cs | 3 + .../Generator/ModelBuilder.cs | 1 + .../Generator/TypeMapAssemblyEmitter.cs | 44 +-- .../Scanner/JavaPeerInfo.cs | 96 +++++-- .../Scanner/JavaPeerScanner.cs | 260 ++++++++++++------ .../Generator/JcwJavaSourceGeneratorTests.cs | 101 +++++++ .../TypeMapAssemblyGeneratorTests.cs | 63 +++++ .../Generator/TypeMapModelBuilderTests.cs | 65 +++++ .../Scanner/JavaPeerScannerTests.cs | 59 ++++ .../TestFixtures/StubAttributes.cs | 11 + .../TestFixtures/TestTypes.cs | 32 +++ 12 files changed, 630 insertions(+), 137 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 2eb7a7f264a..2494924a232 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -52,6 +52,7 @@ internal void Generate (JavaPeerInfo type, TextWriter writer) WritePackageDeclaration (type, writer); WriteClassDeclaration (type, writer); WriteStaticInitializer (type, writer); + WriteExportFields (type, writer); WriteConstructors (type, writer); WriteMethods (type, writer); WriteClassClose (writer); @@ -116,6 +117,28 @@ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) writer.WriteLine (); } + static void WriteExportFields (JavaPeerInfo type, TextWriter writer) + { + foreach (var field in type.ExportFields) { + string javaType = JniTypeToJava (field.JniReturnType); + + writer.Write ("\tpublic "); + if (field.IsStatic) { + writer.Write ("static "); + } + writer.Write (javaType); + writer.Write (' '); + writer.Write (field.FieldName); + writer.Write (" = "); + writer.Write (field.MethodName); + writer.WriteLine (" ();"); + } + + if (type.ExportFields.Count > 0) { + writer.WriteLine (); + } + } + static void WriteConstructors (JavaPeerInfo type, TextWriter writer) { string simpleClassName = GetJavaSimpleName (type.JavaName); @@ -199,6 +222,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) writer.Write ("\t@Override\n"); } writer.Write ("\tpublic "); + if (method.IsStatic) { + writer.Write ("static "); + } writer.Write (javaReturnType); writer.Write (' '); writer.Write (method.JniName); @@ -233,7 +259,11 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) writer.Write ("\t}\n"); // Native method declaration - writer.Write ("\tprivate native "); + writer.Write ("\tprivate "); + if (method.IsStatic) { + writer.Write ("static "); + } + writer.Write ("native "); writer.Write (javaReturnType); writer.Write (' '); writer.Write (method.NativeCallbackName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index f81cd7fd213..fbb21ff9267 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -180,6 +180,9 @@ sealed class ExportMarshalMethodData /// True if this is a constructor. public bool IsConstructor { get; set; } + /// True if this is a static method. + public bool IsStatic { get; set; } + /// /// Managed parameter types for the managed method call. /// Each entry is the assembly-qualified managed type name. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 3fe819e81ac..8ed331b73d3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -346,6 +346,7 @@ static ExportMarshalMethodData BuildExportMarshalMethod (MarshalMethodInfo mm, J }, JniSignature = mm.JniSignature, IsConstructor = isConstructor, + IsStatic = mm.IsStatic, ManagedReturnType = mm.ManagedReturnType, }; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index e31ba9feac5..3d6df72e9dd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -661,7 +661,11 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType); // Build GetObject method spec — generic instantiation of Object.GetObject - var getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef); + // Not needed for static methods (no 'this' object to unmarshal) + EntityHandle getObjectRef = default; + if (!export.IsStatic) { + getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef); + } // Resolve managed method to call MemberReferenceHandle managedMethodRef; @@ -710,23 +714,29 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.Call (_activateInstanceRef); } - // var __this = Object.GetObject(jnienv, native__this, DoNotTransfer); - encoder.LoadArgument (0); // jnienv - encoder.LoadArgument (1); // native__this - encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 - encoder.Call (getObjectRef); - - // Unmarshal each parameter - for (int i = 0; i < export.ManagedParameters.Count; i++) { - EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); - } - - // Call managed method - if (export.IsConstructor) { + if (export.IsStatic) { + // Static methods: unmarshal params, then call static managed method directly + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); + } encoder.Call (managedMethodRef); } else { - encoder.OpCode (ILOpCode.Callvirt); - encoder.Token (managedMethodRef); + // Instance methods: GetObject, unmarshal params, then callvirt + encoder.LoadArgument (0); // jnienv + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 + encoder.Call (getObjectRef); + + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); + } + + if (export.IsConstructor) { + encoder.Call (managedMethodRef); + } else { + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (managedMethodRef); + } } // Marshal return value and store in local 3 @@ -840,7 +850,7 @@ MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMars bool isVoid = returnKind == JniParamKind.Void; return AddMemberRef (metadata, declaringTypeRef, export.ManagedMethodName, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (paramCount, + sig => sig.MethodSignature (isInstanceMethod: !export.IsStatic).Parameters (paramCount, rt => { if (isVoid) { rt.Void (); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index f758780c68c..e54ff594fc5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -8,25 +8,25 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). /// Generators consume this data model — they never touch PEReader/MetadataReader. /// -sealed record JavaPeerInfo +sealed class JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". /// Extracted from the [Register] attribute. /// - public required string JavaName { get; init; } + public string JavaName { get; set; } = ""; /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). /// For MCW binding types (with [Register]), this equals . /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. /// - public required string CompatJniName { get; init; } + public string CompatJniName { get; set; } = ""; /// /// Full managed type name, e.g., "Android.App.Activity". /// - public required string ManagedTypeName { get; init; } + public string ManagedTypeName { get; set; } = ""; /// /// Managed type namespace, e.g., "Android.App". @@ -41,36 +41,36 @@ sealed record JavaPeerInfo /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// - public required string AssemblyName { get; init; } + public string AssemblyName { get; set; } = ""; /// /// JNI name of the base Java type, e.g., "android/app/Activity" for a type /// that extends Activity. Null for java/lang/Object or types without a Java base. /// Needed by JCW Java source generation ("extends" clause). /// - public string? BaseJavaName { get; init; } + public string? BaseJavaName { get; set; } /// /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. /// Needed by JCW Java source generation ("implements" clause). /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); + public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); - public bool IsInterface { get; init; } - public bool IsAbstract { get; init; } + public bool IsInterface { get; set; } + public bool IsAbstract { get; set; } /// /// If true, this is a Managed Callable Wrapper (MCW) binding type. /// No JCW or RegisterNatives will be generated for it. /// - public bool DoNotGenerateAcw { get; init; } + public bool DoNotGenerateAcw { get; set; } /// /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). /// - public bool IsUnconditional { get; init; } + public bool IsUnconditional { get; set; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or @@ -78,7 +78,7 @@ sealed record JavaPeerInfo /// Constructors are identified by . /// Ordered — the index in this list is the method's ordinal for RegisterNatives. /// - public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); + public IReadOnlyList MarshalMethods { get; set; } = Array.Empty (); /// /// Java constructors to emit in the JCW .java file. @@ -90,19 +90,25 @@ sealed record JavaPeerInfo /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. /// - public ActivationCtorInfo? ActivationCtor { get; init; } + public ActivationCtorInfo? ActivationCtor { get; set; } + + /// + /// Java fields generated from [ExportField] attributes. + /// Each field is initialized by calling the associated managed method. + /// + public IReadOnlyList ExportFields { get; set; } = Array.Empty (); /// /// For interfaces and abstract types, the name of the invoker type /// used to instantiate instances from Java. /// - public string? InvokerTypeName { get; init; } + public string? InvokerTypeName { get; set; } /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// - public bool IsGenericDefinition { get; init; } + public bool IsGenericDefinition { get; set; } } /// @@ -110,30 +116,30 @@ sealed record JavaPeerInfo /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, /// and a RegisterNatives call. /// -sealed record MarshalMethodInfo +sealed class MarshalMethodInfo { /// /// JNI method name, e.g., "onCreate". /// This is the Java method name (without n_ prefix). /// - public required string JniName { get; init; } + public string JniName { get; set; } = ""; /// /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". /// Contains both parameter types and return type. /// - public required string JniSignature { get; init; } + public string JniSignature { get; set; } = ""; /// /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". /// Null for [Export] methods. /// - public string? Connector { get; init; } + public string? Connector { get; set; } /// /// Name of the managed method this maps to, e.g., "OnCreate". /// - public required string ManagedMethodName { get; init; } + public string ManagedMethodName { get; set; } = ""; /// /// Full name of the type that declares the managed method (may be a base type). @@ -165,25 +171,59 @@ sealed record MarshalMethodInfo /// /// True if this is a constructor registration. /// - public bool IsConstructor { get; init; } + public bool IsConstructor { get; set; } /// /// For [Export] methods: Java exception types that the method declares it can throw. /// Null for [Register] methods. /// - public IReadOnlyList? ThrownNames { get; init; } + public IReadOnlyList? ThrownNames { get; set; } /// /// For [Export] methods: super constructor arguments string. /// Null for [Register] methods. /// - public string? SuperArgumentsString { get; init; } + public string? SuperArgumentsString { get; set; } /// /// For [Export] methods: managed return type name, e.g., "System.String". /// Null for [Register] methods and constructors. /// - public string? ManagedReturnType { get; init; } + public string? ManagedReturnType { get; set; } + + /// + /// True if the method is static. Relevant for [Export] static methods. + /// + public bool IsStatic { get; set; } +} + +/// +/// Describes a Java field generated from a method annotated with [ExportField]. +/// The Java side declares a field initialized by calling the method. +/// +sealed class ExportFieldInfo +{ + /// + /// The Java field name, e.g., "STATIC_INSTANCE". + /// + public string FieldName { get; set; } = ""; + + /// + /// Name of the managed method that initializes the field. + /// Used both as the Java initializer method name and the native callback method name. + /// + public string MethodName { get; set; } = ""; + + /// + /// JNI return type descriptor, e.g., "Ljava/lang/String;". + /// Determines the Java field type. + /// + public string JniReturnType { get; set; } = ""; + + /// + /// Whether the method (and thus the field) is static. + /// + public bool IsStatic { get; set; } } /// @@ -245,23 +285,23 @@ sealed class JavaConstructorInfo /// /// Describes how to call the activation constructor for a Java peer type. /// -sealed record ActivationCtorInfo +sealed class ActivationCtorInfo { /// /// The type that declares the activation constructor. /// May be the type itself or a base type. /// - public required string DeclaringTypeName { get; init; } + public string DeclaringTypeName { get; set; } = ""; /// /// The assembly containing the declaring type. /// - public required string DeclaringAssemblyName { get; init; } + public string DeclaringAssemblyName { get; set; } = ""; /// /// The style of activation constructor found. /// - public required ActivationCtorStyle Style { get; init; } + public ActivationCtorStyle Style { get; set; } } enum ActivationCtorStyle diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 1b0a30ab858..58f76d21e2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -17,20 +16,20 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class JavaPeerScanner : IDisposable { readonly Dictionary assemblyCache = new (StringComparer.Ordinal); - readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + readonly Dictionary activationCtorCache = new (StringComparer.Ordinal); /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. /// Checks the specified assembly (by name) in the assembly cache. /// - bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex) { - if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) && resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { return true; } handle = default; - resolvedIndex = null; + resolvedIndex = null!; return false; } @@ -48,16 +47,16 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan switch (scope.Kind) { case HandleKind.AssemblyReference: { var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); - var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; + var fullName = ns.Length > 0 ? ns + "." + name : name; return (fullName, index.Reader.GetString (asmRef.Name)); } case HandleKind.TypeReference: { // Nested type: recurse to get the declaring type's full name and assembly var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); - return ($"{parentFullName}+{name}", assemblyName); + return (parentFullName + "+" + name, assemblyName); } default: { - var fullName = ns.Length > 0 ? $"{ns}.{name}" : name; + var fullName = ns.Length > 0 ? ns + "." + name : name; return (fullName, index.AssemblyName); } } @@ -87,6 +86,17 @@ public List Scan (IReadOnlyList assemblyPaths) assemblyCache [index.AssemblyName] = index; } + // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies + // and re-classify any attributes that weren't recognized in the initial pass + // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). + var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); + foreach (var index in assemblyCache.Values) { + mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); + } + foreach (var index in assemblyCache.Values) { + index.ReclassifyAttributes (mergedJniNameProviders); + } + // Phase 2: Analyze types using cached indices var resultsByManagedName = new Dictionary (StringComparer.Ordinal); @@ -109,41 +119,27 @@ static void ForceUnconditionalCrossReferences (Dictionary { foreach (var index in assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { - if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); - } + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); } } } static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) { - if (managedTypeName is null) { - return; - } - - managedTypeName = managedTypeName.Trim (); - if (managedTypeName.Length == 0) { - return; - } - - // Try exact match first (handles both plain and assembly-qualified names) - if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { - resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; + if (managedTypeName == null) { return; } // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." // Strip to just the type name for lookup var commaIndex = managedTypeName.IndexOf (','); - if (commaIndex <= 0) { - return; + if (commaIndex > 0) { + managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); } - var typeName = managedTypeName.Substring (0, commaIndex).Trim (); - if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { - resultsByManagedName [typeName] = peer with { IsUnconditional = true }; + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + peer.IsUnconditional = true; } } @@ -170,13 +166,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); - if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; - } else if (attrInfo?.JniName is not null) { + } else if (attrInfo?.ComponentAttributeJniName != null) { // User type with [Activity(Name = "...")] but no [Register] - jniName = attrInfo.JniName; + jniName = attrInfo.ComponentAttributeJniName; compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. @@ -188,13 +184,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; - var isUnconditional = attrInfo is not null; + var isUnconditional = attrInfo?.HasComponentAttribute ?? false; string? invokerTypeName = null; // Resolve base Java type name @@ -206,6 +202,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // Collect marshal methods (including constructors) in a single pass over methods var marshalMethods = CollectMarshalMethods (typeDef, index); + // Collect [ExportField] declarations + var exportFields = CollectExportFields (typeDef, index); // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -230,6 +228,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results MarshalMethods = marshalMethods, JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, + ExportFields = exportFields, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, }; @@ -245,18 +244,19 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI // Single pass over methods: collect marshal methods (including constructors) foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + var registerInfo = TryGetMethodRegisterInfo (methodDef, index); + if (registerInfo == null) { continue; } - AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + AddMarshalMethod (methods, registerInfo, methodDef, index); } // Collect [Register] from properties (attribute is on the property, not the getter) foreach (var propHandle in typeDef.GetProperties ()) { var propDef = index.Reader.GetPropertyDefinition (propHandle); var propRegister = TryGetPropertyRegisterInfo (propDef, index); - if (propRegister is null) { + if (propRegister == null) { continue; } @@ -270,16 +270,17 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI return methods; } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) { // Skip methods that are just the JNI name (type-level [Register]) - if (registerInfo.Signature is null && registerInfo.Connector is null) { + if (registerInfo.Signature == null && registerInfo.Connector == null) { return; } var methodName = index.Reader.GetString (methodDef.Name); var jniSignature = registerInfo.Signature ?? "()V"; var parameters = ParseJniParameters (jniSignature); + bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0; // For [Export] methods, populate ManagedType from the actual method signature // (needed for the generated marshal method body) @@ -302,8 +303,9 @@ static void AddMarshalMethod (List methods, RegisterInfo regi JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature), Parameters = parameters, IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", - ThrownNames = exportInfo?.ThrownNames, - SuperArgumentsString = exportInfo?.SuperArgumentsString, + IsStatic = isStatic, + ThrownNames = registerInfo.ThrownNames, + SuperArgumentsString = registerInfo.SuperArgumentsString, ManagedReturnType = managedReturnType, }); } @@ -311,7 +313,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) { var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is null) { + if (baseInfo == null) { return null; } @@ -319,7 +321,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi // First try [Register] attribute var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); - if (registerJniName is not null) { + if (registerJniName != null) { return registerJniName; } @@ -339,7 +341,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem foreach (var implHandle in interfaceImpls) { var impl = index.Reader.GetInterfaceImplementation (implHandle); var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); - if (ifaceJniName is not null) { + if (ifaceJniName != null) { result.Add (ifaceJniName); } } @@ -350,28 +352,30 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) { var resolved = ResolveEntityHandle (interfaceHandle, index); - return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) { - exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); if (attrName == "RegisterAttribute") { - registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); - return true; + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); } if (attrName == "ExportAttribute") { - (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); - return true; + return ParseExportAttribute (ca, methodDef, index); + } + + if (attrName == "ExportFieldAttribute") { + // [ExportField] methods are registered like [Export] — they need a native callback. + // The method name is used as the export name (not the field name). + return ParseExportFieldAsRegisterInfo (methodDef, index); } } - registerInfo = null; - return false; + return null; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) @@ -387,7 +391,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex return null; } - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = ca.DecodeValue (index.customAttributeTypeProvider); @@ -404,31 +408,106 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex foreach (var named in value.NamedArguments) { if (named.Name == "Name" && named.Value is string name) { exportName = name; - } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { - thrownNames = new List (names.Length); - foreach (var item in names) { - if (item.Value is string s) { - thrownNames.Add (s); - } - } + } else if (named.Name == "ThrownNames") { + thrownNames = ExtractStringArray (named.Value); } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { superArguments = superArgs; } } - if (string.IsNullOrEmpty (exportName)) { + if (exportName == null || exportName.Length == 0) { exportName = index.Reader.GetString (methodDef.Name); } - string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); // Build JNI signature from method signature var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); var jniSig = BuildJniSignatureFromManaged (sig); - return ( - new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } - ); + return new RegisterInfo (exportName, jniSig, null, false, + thrownNames: thrownNames, superArgumentsString: superArguments); + } + + /// + /// Creates a RegisterInfo for an [ExportField] method. + /// The method is registered like [Export] (Connector = null) so it gets a full marshal body. + /// + static RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + { + var methodName = index.Reader.GetString (methodDef.Name); + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + return new RegisterInfo (methodName, jniSig, null, false); + } + + /// + /// Collects [ExportField] declarations from methods on a type. + /// Returns field info (field name, method name, return type, static). + /// + static List CollectExportFields (TypeDefinition typeDef, AssemblyIndex index) + { + var fields = new List (); + + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName != "ExportFieldAttribute") { + continue; + } + + var value = ca.DecodeValue (index.customAttributeTypeProvider); + if (value.FixedArguments.Length == 0) { + continue; + } + + string? fieldName = (string?)value.FixedArguments [0].Value; + if (fieldName == null || fieldName.Length == 0) { + continue; + } + + var methodName = index.Reader.GetString (methodDef.Name); + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0; + + fields.Add (new ExportFieldInfo { + FieldName = fieldName, + MethodName = methodName, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig), + IsStatic = isStatic, + }); + } + } + + return fields; + } + + /// + /// Extracts a string array from a decoded custom attribute value. + /// SRM decodes string[] as ImmutableArray<CustomAttributeTypedArgument<string>>. + /// + static List? ExtractStringArray (object? value) + { + if (value is string[] directArray) { + return new List (directArray); + } + + if (value is ImmutableArray> typedArray) { + var result = new List (typedArray.Length); + foreach (var item in typedArray) { + if (item.Value is string s) { + result.Add (s); + } + } + if (result.Count > 0) { + return result; + } + } + + return null; } static string BuildJniSignatureFromManaged (MethodSignature sig) @@ -462,7 +541,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case "System.String": return "Ljava/lang/String;"; default: if (managedType.EndsWith ("[]")) { - return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); } return "Ljava/lang/Object;"; } @@ -504,28 +583,27 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { - var cacheKey = (typeName, index.AssemblyName); - if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { + if (activationCtorCache.TryGetValue (typeName, out var cached)) { return cached; } // Check this type's constructors var ownCtor = FindActivationCtorOnType (typeDef, index); - if (ownCtor is not null) { + if (ownCtor != null) { var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; - activationCtorCache [cacheKey] = info; + activationCtorCache [typeName] = info; return info; } // Walk base type hierarchy var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is not null) { + if (baseInfo != null) { var (baseTypeName, baseAssemblyName) = baseInfo.Value; if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); - if (result is not null) { - activationCtorCache [cacheKey] = result; + if (result != null) { + activationCtorCache [typeName] = result; } return result; } @@ -593,7 +671,7 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) case 0: { // TypeDef var handle = MetadataTokens.TypeDefinitionHandle (row); var baseDef = index.Reader.GetTypeDefinition (handle); - return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); + return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName); } case 1: // TypeRef return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); @@ -611,7 +689,7 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) switch (handle.Kind) { case HandleKind.TypeDefinition: { var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); - return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); + return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName); } case HandleKind.TypeReference: return ResolveTypeReference ((TypeReferenceHandle)handle, index); @@ -632,7 +710,7 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) // First, check the [Register] attribute's connector arg (3rd arg). // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] // where the connector contains the assembly-qualified invoker type name. - if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) { var connector = registerInfo.Connector; // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." // We want just the type name (before the first comma, if any) @@ -646,7 +724,7 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" - var invokerName = $"{typeName}Invoker"; + var invokerName = typeName + "Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { return invokerName; } @@ -669,8 +747,8 @@ public void Dispose () /// bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var key = $"{index.AssemblyName}:{fullName}"; + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); + var key = index.AssemblyName + ":" + fullName; if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; @@ -680,7 +758,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = false; var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is null) { + if (baseInfo == null) { return false; } @@ -695,7 +773,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = true; return true; } - if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { + if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { extendsJavaPeerCache [key] = true; return true; } @@ -718,17 +796,17 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); - if (parentJniName is not null) { - var name = $"{parentJniName}_{typeName}"; + if (parentJniName != null) { + var name = parentJniName + "_" + typeName; return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - var jniName = $"{packageName}/{typeName}"; + var jniName = packageName + "/" + typeName; string compatName = ns.Length == 0 ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName; return (jniName, compatName); } @@ -763,8 +841,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) parentJniName = parentRegister.JniName; break; } - if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { - parentJniName = parentAttr.JniName; + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) { + parentJniName = parentAttr.ComponentAttributeJniName; break; } @@ -785,9 +863,9 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); + var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName); var hash = System.IO.Hashing.Crc64.Hash (data); - return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); } static string ExtractNamespace (string fullName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 2836346f70b..6a0fd89e5d5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -669,4 +669,105 @@ public void Generate_ExportMethod_NativeDeclarationIsPrivate () } } + + public class StaticExportAndExportField : JcwJavaSourceGeneratorTests + { + [Fact] + public void Generate_StaticExportMethod_HasStaticKeyword () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + Assert.Contains ("public static void staticMethodNotMangled ()", java); + Assert.Contains ("private static native void n_staticMethodNotMangled ()", java); + } + + [Fact] + public void Generate_InstanceExportMethod_NoStaticKeyword () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // Instance method should NOT have static + Assert.Contains ("public void instanceMethod ()", java); + Assert.Contains ("private native void n_instanceMethod ()", java); + } + + [Fact] + public void Generate_ExportField_StaticFieldDeclaration () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // Static [ExportField] → "public static FIELD_NAME = MethodName ();" + Assert.Contains ("public static", java); + Assert.Contains ("STATIC_INSTANCE = GetInstance ();", java); + } + + [Fact] + public void Generate_ExportField_InstanceFieldDeclaration () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // Instance [ExportField] → "public FIELD_NAME = MethodName ();" + // Should NOT contain "static" for the VALUE field + var lines = java.Split ('\n'); + bool foundValue = false; + foreach (var line in lines) { + if (line.Contains ("VALUE = GetValue ()")) { + foundValue = true; + Assert.DoesNotContain ("static", line); + break; + } + } + Assert.True (foundValue, "VALUE field declaration not found"); + } + + [Fact] + public void Generate_ExportField_MethodHasNativeCallback () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // [ExportField] methods should have corresponding wrapper + native method + Assert.Contains ("n_GetInstance ()", java); + Assert.Contains ("n_GetValue ()", java); + } + + [Fact] + public void Generate_ExportField_StaticMethodIsStatic () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // Static [ExportField] method: both wrapper and native should be static + Assert.Contains ("public static", java); + Assert.Contains ("private static native", java); + } + + [Fact] + public void Generate_ExportField_FieldsAppearBeforeConstructors () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var java = GenerateToString (peer); + + // Fields should appear before constructors (after static initializer) + int fieldsPos = java.IndexOf ("STATIC_INSTANCE"); + int ctorPos = java.IndexOf ("ExportStaticAndFields ("); + Assert.True (fieldsPos > 0, "STATIC_INSTANCE field not found"); + + // Constructors may or may not be present, but if so fields come first + if (ctorPos > 0) { + Assert.True (fieldsPos < ctorPos, "Fields should appear before constructors"); + } + } + } } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 3f88d9e3610..1e3cb2fcdc2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -424,6 +424,69 @@ public void Generate_ExportMembersComprehensive_ProducesValidAssembly () } } + [Fact] + public void Generate_StaticExportMethod_ProducesValidAssembly () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields"); + var path = GenerateAssembly (new [] { peer }, "StaticExportTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + // Should have export wrappers for static methods, instance methods, and ExportField methods + var exportWrappers = methods.Where (m => m.StartsWith ("n_") && m.Contains ("_uco")).ToList (); + Assert.True (exportWrappers.Count >= 3, + $"Expected at least 3 export wrappers (static, instance, ExportField), got {exportWrappers.Count}: [{string.Join (", ", exportWrappers)}]"); + + Assert.Contains ("RegisterNatives", methods); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_StaticExportMethod_HasMethodBodyWithCorrectPattern () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields"); + var path = GenerateAssembly (new [] { peer }, "StaticExportBody"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy"); + + // All export wrappers should have non-zero RVA + var exportWrappers = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => { + var name = reader.GetString (m.Name); + return name.StartsWith ("n_") && name.Contains ("_uco"); + }) + .ToList (); + + Assert.NotEmpty (exportWrappers); + foreach (var wrapper in exportWrappers) { + Assert.True (wrapper.RelativeVirtualAddress > 0, + $"Export marshal method '{reader.GetString (wrapper.Name)}' should have a method body"); + } + } + } finally { + CleanUp (path); + } + } + } public class Ignoresaccesschecksto diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index a74659e5fb3..a2e691cb35e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1433,6 +1433,71 @@ public void Fixture_ExportMarshalMethod_HasCorrectManagedParameters () } + public class FixtureStaticExportAndExportField + { + + [Fact] + public void Fixture_StaticExport_IsStaticInModel () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + var staticExport = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "staticMethodNotMangled"); + Assert.NotNull (staticExport); + Assert.True (staticExport!.IsStatic); + } + + [Fact] + public void Fixture_InstanceExport_IsNotStaticInModel () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + var instanceExport = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "instanceMethod"); + Assert.NotNull (instanceExport); + Assert.False (instanceExport!.IsStatic); + } + + [Fact] + public void Fixture_ExportField_MethodInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + // [ExportField] methods are [Export]-style and should be in ExportMarshalMethods + var getInstance = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "GetInstance"); + Assert.NotNull (getInstance); + Assert.True (getInstance!.IsStatic); + Assert.False (getInstance.IsConstructor); + + var getValue = proxy.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "GetValue"); + Assert.NotNull (getValue); + Assert.False (getValue!.IsStatic); + } + + [Fact] + public void Fixture_ExportField_NativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + // All export methods should have native registrations + var getInstanceReg = proxy!.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_GetInstance"); + Assert.NotNull (getInstanceReg); + + var staticReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_staticMethodNotMangled"); + Assert.NotNull (staticReg); + } + } + public class FixtureImplementors { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 47098aacade..df468190a5c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -829,4 +829,63 @@ public void Scan_ExportOnUnregisteredType_MethodDiscovered () Assert.NotNull (exportMethod); Assert.Null (exportMethod.Connector); } + + [Fact] + public void Scan_StaticExportMethod_IsStaticSetTrue () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var staticMethod = peer.MarshalMethods.FirstOrDefault (m => m.JniName == "staticMethodNotMangled"); + Assert.NotNull (staticMethod); + Assert.True (staticMethod.IsStatic); + Assert.Null (staticMethod.Connector); + } + + [Fact] + public void Scan_InstanceExportMethod_IsStaticSetFalse () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var instanceMethod = peer.MarshalMethods.FirstOrDefault (m => m.JniName == "instanceMethod"); + Assert.NotNull (instanceMethod); + Assert.False (instanceMethod.IsStatic); + } + + [Fact] + public void Scan_ExportField_CollectedAsExportFieldInfo () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + + Assert.NotNull (peer.ExportFields); + Assert.Equal (2, peer.ExportFields.Count); + + var staticField = peer.ExportFields.FirstOrDefault (f => f.FieldName == "STATIC_INSTANCE"); + Assert.NotNull (staticField); + Assert.Equal ("GetInstance", staticField.MethodName); + Assert.True (staticField.IsStatic); + + var instanceField = peer.ExportFields.FirstOrDefault (f => f.FieldName == "VALUE"); + Assert.NotNull (instanceField); + Assert.Equal ("GetValue", instanceField.MethodName); + Assert.False (instanceField.IsStatic); + } + + [Fact] + public void Scan_ExportField_MethodAlsoInMarshalMethods () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + + // [ExportField] methods should also be in MarshalMethods as export methods + var getInstance = peer.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "GetInstance"); + Assert.NotNull (getInstance); + Assert.Null (getInstance.Connector); // Export, not Register + Assert.True (getInstance.IsStatic); + + var getValue = peer.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "GetValue"); + Assert.NotNull (getValue); + Assert.Null (getValue.Connector); + Assert.False (getValue.IsStatic); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 5f4ab12a45e..d0490dd2d62 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -110,6 +110,17 @@ public sealed class ExportAttribute : Attribute public ExportAttribute () { } public ExportAttribute (string name) => Name = name; } + + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportFieldAttribute : Attribute + { + public string Name { get; set; } + + public ExportFieldAttribute (string name) + { + Name = name; + } + } } namespace MyApp diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 0c7f0aae1c2..cedf6831f10 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -781,4 +781,36 @@ protected ExportCtorWithSuperArgs (IntPtr handle, Android.Runtime.JniHandleOwner [Java.Interop.Export (SuperArgumentsString = "")] public ExportCtorWithSuperArgs (int value) { } } + + /// + /// Static [Export] method and [ExportField] declarations. + /// + [Register ("my/app/ExportStaticAndFields")] + public class ExportStaticAndFields : Java.Lang.Object + { + protected ExportStaticAndFields (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.ExportField ("STATIC_INSTANCE")] + public static ExportStaticAndFields GetInstance () + { + return null!; + } + + [Java.Interop.ExportField ("VALUE")] + public string GetValue () + { + return "value"; + } + + [Java.Interop.Export] + public static void staticMethodNotMangled () + { + } + + [Java.Interop.Export] + public void instanceMethod () { } + } } From d13646bd0aa6f4289f27cbde47301ab9b816c4fb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 22:38:01 +0100 Subject: [PATCH 39/43] Simplify scanner and generator code - Merge CollectMarshalMethods + CollectExportFields into single-pass CollectMarshalMethodsAndExportFields - Inline TryGetMethodRegisterInfo into merged method - Replace CollectExportFields with ParseExportFieldName helper - Extract WriteThrowsClause helper in JCW generator - Deduplicate parameter unmarshal loop in emitter - Fix stale IsExport comment --- .../Generator/JcwJavaSourceGenerator.cs | 41 +++--- .../Generator/TypeMapAssemblyEmitter.cs | 30 ++--- .../Scanner/JavaPeerInfo.cs | 1 - .../Scanner/JavaPeerScanner.cs | 126 ++++++++---------- 4 files changed, 85 insertions(+), 113 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 2494924a232..61e18460a82 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -149,19 +149,10 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) writer.Write (simpleClassName); writer.Write (" ("); WriteParameterList (ctor.Parameters, writer); - writer.Write (')'); + writer.Write (")\n"); - if (ctor.IsExport && ctor.ThrownNames != null && ctor.ThrownNames.Count > 0) { - writer.Write ("\n\t\tthrows "); - for (int i = 0; i < ctor.ThrownNames.Count; i++) { - if (i > 0) { - writer.Write (", "); - } - writer.Write (ctor.ThrownNames [i]); - } - } + WriteThrowsClause (ctor.IsExport ? ctor.ThrownNames : null, writer); - writer.WriteLine (); writer.WriteLine ("\t{"); // super() call — use SuperArgumentsString if provided ([Export] constructors), @@ -232,17 +223,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) WriteParameterList (method.Parameters, writer); writer.Write (")\n"); - // throws clause for [Export] methods - if (method.ThrownNames != null && method.ThrownNames.Count > 0) { - writer.Write ("\t\tthrows "); - for (int i = 0; i < method.ThrownNames.Count; i++) { - if (i > 0) { - writer.Write (", "); - } - writer.Write (method.ThrownNames [i]); - } - writer.Write ('\n'); - } + WriteThrowsClause (method.ThrownNames, writer); writer.Write ("\t{\n"); @@ -303,6 +284,22 @@ static void WriteArgumentList (IReadOnlyList parameters, TextW } } + static void WriteThrowsClause (IReadOnlyList? thrownNames, TextWriter writer) + { + if (thrownNames == null || thrownNames.Count == 0) { + return; + } + + writer.Write ("\t\tthrows "); + for (int i = 0; i < thrownNames.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (thrownNames [i]); + } + writer.Write ('\n'); + } + /// /// Converts a JNI type name to a Java source type name. /// e.g., "android/app/Activity" → "android.app.Activity" diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 3d6df72e9dd..6d944d96eff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -714,29 +714,25 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.Call (_activateInstanceRef); } - if (export.IsStatic) { - // Static methods: unmarshal params, then call static managed method directly - for (int i = 0; i < export.ManagedParameters.Count; i++) { - EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); - } - encoder.Call (managedMethodRef); - } else { - // Instance methods: GetObject, unmarshal params, then callvirt + if (!export.IsStatic) { + // Instance methods/constructors: get managed object from JNI handle encoder.LoadArgument (0); // jnienv encoder.LoadArgument (1); // native__this encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 encoder.Call (getObjectRef); + } - for (int i = 0; i < export.ManagedParameters.Count; i++) { - EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); - } + // Unmarshal each parameter + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); + } - if (export.IsConstructor) { - encoder.Call (managedMethodRef); - } else { - encoder.OpCode (ILOpCode.Callvirt); - encoder.Token (managedMethodRef); - } + // Call managed method: static → call, instance ctor → call, instance method → callvirt + if (export.IsStatic || export.IsConstructor) { + encoder.Call (managedMethodRef); + } else { + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (managedMethodRef); } // Marshal return value and store in local 3 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index e54ff594fc5..e3b0617ed66 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -271,7 +271,6 @@ sealed class JavaConstructorInfo /// /// Whether this constructor is from [Export] attribute. - /// [Export] constructors use TypeManager.Activate instead of nctor_N. /// public bool IsExport { get; set; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 58f76d21e2c..cf5c478109d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -199,11 +199,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // Resolve implemented Java interface names var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); - // Collect marshal methods (including constructors) in a single pass over methods - var marshalMethods = CollectMarshalMethods (typeDef, index); + // Collect marshal methods (including constructors) and [ExportField] declarations + var (marshalMethods, exportFields) = CollectMarshalMethodsAndExportFields (typeDef, index); - // Collect [ExportField] declarations - var exportFields = CollectExportFields (typeDef, index); // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -237,19 +235,55 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } - List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + (List, List) CollectMarshalMethodsAndExportFields (TypeDefinition typeDef, AssemblyIndex index) { var methods = new List (); + var exportFields = new List (); - // Single pass over methods: collect marshal methods (including constructors) + // Single pass over methods: collect marshal methods, constructors, and export fields foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - var registerInfo = TryGetMethodRegisterInfo (methodDef, index); - if (registerInfo == null) { - continue; + + string? exportFieldName = null; + RegisterInfo? registerInfo = null; + + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + break; + } + + if (attrName == "ExportAttribute") { + registerInfo = ParseExportAttribute (ca, methodDef, index); + break; + } + + if (attrName == "ExportFieldAttribute") { + registerInfo = ParseExportFieldAsRegisterInfo (methodDef, index); + exportFieldName = ParseExportFieldName (ca, index); + break; + } } - AddMarshalMethod (methods, registerInfo, methodDef, index); + if (registerInfo != null) { + AddMarshalMethod (methods, registerInfo, methodDef, index); + } + + if (exportFieldName != null) { + var methodName = index.Reader.GetString (methodDef.Name); + var jniSig = registerInfo!.Signature ?? "()V"; + bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0; + + exportFields.Add (new ExportFieldInfo { + FieldName = exportFieldName, + MethodName = methodName, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig), + IsStatic = isStatic, + }); + } } // Collect [Register] from properties (attribute is on the property, not the getter) @@ -267,7 +301,7 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI } } - return methods; + return (methods, exportFields); } static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) @@ -355,29 +389,6 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) - { - foreach (var caHandle in methodDef.GetCustomAttributes ()) { - var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); - - if (attrName == "RegisterAttribute") { - return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); - } - - if (attrName == "ExportAttribute") { - return ParseExportAttribute (ca, methodDef, index); - } - - if (attrName == "ExportFieldAttribute") { - // [ExportField] methods are registered like [Export] — they need a native callback. - // The method name is used as the export name (not the field name). - return ParseExportFieldAsRegisterInfo (methodDef, index); - } - } - return null; - } - static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) { foreach (var caHandle in propDef.GetCustomAttributes ()) { @@ -440,49 +451,18 @@ static RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, } /// - /// Collects [ExportField] declarations from methods on a type. - /// Returns field info (field name, method name, return type, static). + /// Extracts the field name from an [ExportField("FIELD_NAME")] attribute. + /// Returns null if the field name is empty or missing. /// - static List CollectExportFields (TypeDefinition typeDef, AssemblyIndex index) + static string? ParseExportFieldName (CustomAttribute ca, AssemblyIndex index) { - var fields = new List (); - - foreach (var methodHandle in typeDef.GetMethods ()) { - var methodDef = index.Reader.GetMethodDefinition (methodHandle); - - foreach (var caHandle in methodDef.GetCustomAttributes ()) { - var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); - - if (attrName != "ExportFieldAttribute") { - continue; - } - - var value = ca.DecodeValue (index.customAttributeTypeProvider); - if (value.FixedArguments.Length == 0) { - continue; - } - - string? fieldName = (string?)value.FixedArguments [0].Value; - if (fieldName == null || fieldName.Length == 0) { - continue; - } - - var methodName = index.Reader.GetString (methodDef.Name); - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var jniSig = BuildJniSignatureFromManaged (sig); - bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0; - - fields.Add (new ExportFieldInfo { - FieldName = fieldName, - MethodName = methodName, - JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig), - IsStatic = isStatic, - }); - } + var value = ca.DecodeValue (index.customAttributeTypeProvider); + if (value.FixedArguments.Length == 0) { + return null; } - return fields; + var fieldName = (string?)value.FixedArguments [0].Value; + return fieldName != null && fieldName.Length > 0 ? fieldName : null; } /// From c13361cf222c68ce5118fb19668b90609dcccdfa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Feb 2026 22:55:09 +0100 Subject: [PATCH 40/43] Add integration tests verifying Export members in RegisterNatives Verify that [Export] methods, constructors, static exports, and [ExportField] backing methods all appear in the RegisterNatives IL body with correct JNI names and signatures. This catches bugs where wrappers are generated but not registered, which would cause runtime UnsatisfiedLinkError. --- .../TypeMapAssemblyGeneratorTests.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 1e3cb2fcdc2..5d95eed8c45 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -44,6 +44,53 @@ static string GenerateAssembly (IReadOnlyList peers, string? assem return (pe, pe.GetMetadataReader ()); } + /// + /// Reads the RegisterNatives method's IL body and extracts all (jniMethodName, jniSignature) pairs + /// from the ldstr instructions. RegisterNatives emits repeating sequences of: + /// ldarg.1; ldstr jniMethodName; ldstr jniSignature; ldftn wrapper; call RegisterMethod + /// + static List<(string jniMethodName, string jniSignature)> ReadRegisterNativesEntries ( + PEReader pe, MetadataReader reader, TypeDefinition proxy) + { + var registerNatives = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .First (m => reader.GetString (m.Name) == "RegisterNatives"); + + var body = pe.GetMethodBody (registerNatives.RelativeVirtualAddress); + var il = body.GetILBytes ()!; + var result = new List<(string, string)> (); + var strings = new List (); + + for (int i = 0; i < il.Length;) { + byte op = il [i++]; + if (op == 0x72) { // ldstr + int token = il [i] | (il [i + 1] << 8) | (il [i + 2] << 16) | (il [i + 3] << 24); + i += 4; + var handle = MetadataTokens.UserStringHandle (token & 0x00FFFFFF); + strings.Add (reader.GetUserString (handle)); + } else if (op == 0x28) { // call — marks end of a registration sequence + i += 4; + if (strings.Count >= 2) { + result.Add ((strings [strings.Count - 2], strings [strings.Count - 1])); + } + strings.Clear (); + } else if (op == 0xFE) { // two-byte opcode prefix (ldftn = 0xFE 0x06) + i++; // skip second opcode byte + if (i < il.Length && il [i - 1] == 0x06) { // ldftn has 4-byte token + i += 4; + } + } else if (op >= 0x02 && op <= 0x05) { // ldarg.0-3 — no operand + // skip + } else if (op == 0x0E) { // ldarg.s — 1-byte operand + i++; + } else if (op == 0x00 || op == 0x01 || op == 0x2A) { // nop, break, ret — no operand + // skip + } + } + + return result; + } + public class BasicAssemblyStructure { @@ -489,6 +536,119 @@ public void Generate_StaticExportMethod_HasMethodBodyWithCorrectPattern () } + public class ExportNativeRegistration + { + + [Fact] + public void Generate_ExportMethods_RegisteredInRegisterNatives () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive"); + var path = GenerateAssembly (new [] { peer }, "ExportRegistration"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy"); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + var jniNames = entries.Select (e => e.jniMethodName).ToList (); + + Assert.Contains ("n_methodNamesNotMangled", jniNames); + Assert.Contains ("n_CompletelyDifferentName", jniNames); + Assert.Contains ("n_methodThatThrows", jniNames); + Assert.Contains ("n_methodThatThrowsEmptyArray", jniNames); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportConstructors_RegisteredInRegisterNatives () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportsConstructors"); + var path = GenerateAssembly (new [] { peer }, "ExportCtorRegistration"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportsConstructors_Proxy"); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + var jniNames = entries.Select (e => e.jniMethodName).ToList (); + + // Two [Export] constructors → nctor_0, nctor_1 + Assert.Contains ("nctor_0", jniNames); + Assert.Contains ("nctor_1", jniNames); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_StaticExportAndExportField_RegisteredInRegisterNatives () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields"); + var path = GenerateAssembly (new [] { peer }, "StaticExportRegistration"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy"); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + var jniNames = entries.Select (e => e.jniMethodName).ToList (); + + // Static [Export] method + Assert.Contains ("n_staticMethodNotMangled", jniNames); + // Instance [Export] method + Assert.Contains ("n_instanceMethod", jniNames); + // [ExportField] backing methods + Assert.Contains ("n_GetInstance", jniNames); + Assert.Contains ("n_GetValue", jniNames); + } + } finally { + CleanUp (path); + } + } + + [Fact] + public void Generate_ExportRegistration_HasCorrectJniSignatures () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive"); + var path = GenerateAssembly (new [] { peer }, "ExportSignatures"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy"); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + + // CompletelyDifferentName(String, int) → String = (Ljava/lang/String;I)Ljava/lang/String; + var nameOverride = entries.First (e => e.jniMethodName == "n_CompletelyDifferentName"); + Assert.Equal ("(Ljava/lang/String;I)Ljava/lang/String;", nameOverride.jniSignature); + + // void methodNamesNotMangled() = ()V + var simple = entries.First (e => e.jniMethodName == "n_methodNamesNotMangled"); + Assert.Equal ("()V", simple.jniSignature); + } + } finally { + CleanUp (path); + } + } + + } + public class Ignoresaccesschecksto { From 99bcbe2923d86f589f6fc9d2a23f7fb5b651ea3f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 13 Feb 2026 09:12:34 +0100 Subject: [PATCH 41/43] Fix JNI boolean mapping: use byte (unsigned) to match legacy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JNI's jboolean is an unsigned 8-bit type. The legacy MarshalMethodsAssemblyRewriter maps System.Boolean → System.Byte. We were using SByte (signed), which is incorrect. --- .../Generator/JniSignatureHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 66aa6ff12e9..df672795274 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -9,8 +9,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; enum JniParamKind { Void, // V - Boolean, // Z → sbyte - Byte, // B → sbyte + Boolean, // Z → byte (JNI's jboolean is unsigned 8-bit) + Byte, // B → sbyte (JNI's jbyte is signed 8-bit) Char, // C → char Short, // S → short Int, // I → int @@ -115,7 +115,7 @@ static void SkipSingleType (string sig, ref int i) public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { switch (kind) { - case JniParamKind.Boolean: encoder.SByte (); break; + case JniParamKind.Boolean: encoder.Byte (); break; case JniParamKind.Byte: encoder.SByte (); break; case JniParamKind.Char: encoder.Char (); break; case JniParamKind.Short: encoder.Int16 (); break; From 45862e5c63ea0d254fc4da3b67befdfe882c6538 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 13 Feb 2026 10:27:09 +0100 Subject: [PATCH 42/43] Full constructor marshal body generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All constructors (both [Register] and [Export]) now generate full marshal bodies with BeginMarshalMethod + try/catch/finally. There are no pre-existing n_* callback methods for constructors — they only exist for [Register] methods where the connector points to a real static method. Constructor callback pattern (nctor_N_uco): GetUninitializedObject(typeof(T)) -> SetHandle(native__this, DoNotTransfer) -> .ctor(params) CreateInstance for inherited activation ctor calls BaseType::.ctor(IntPtr, JniHandleOwnership) on the first base class that declares it, rather than SetHandle directly. Removed ActivateInstance, UcoConstructorData, and the forwarding approach for constructors. Scanner now populates ManagedType on constructor parameters regardless of Connector value. --- .../Generator/Model/TypeMapAssemblyData.cs | 23 +-- .../Generator/ModelBuilder.cs | 47 +++---- .../Generator/TypeMapAssemblyEmitter.cs | 133 ++++++++---------- .../Scanner/JavaPeerScanner.cs | 10 +- .../TypeMapAssemblyGeneratorTests.cs | 17 +++ .../Generator/TypeMapModelBuilderTests.cs | 107 ++++++++++---- 6 files changed, 177 insertions(+), 160 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index fbb21ff9267..bc9ca36efd3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -89,12 +89,9 @@ sealed class JavaPeerProxyData /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). public bool IsAcw { get; set; } - /// UCO method wrappers for marshal methods (non-constructor) with [Register]. + /// UCO method wrappers for [Register] methods and constructors. public List UcoMethods { get; } = new (); - /// UCO constructor wrappers for [Register] constructors. - public List UcoConstructors { get; } = new (); - /// Export marshal method wrappers — full marshal body for [Export] methods and constructors. public List ExportMarshalMethods { get; } = new (); @@ -133,24 +130,6 @@ sealed class UcoMethodData public string JniSignature { get; set; } = ""; } -/// -/// An [UnmanagedCallersOnly] static wrapper for a constructor callback. -/// Signature must match the full JNI native method signature (jnienv + self + ctor params) -/// so the ABI is correct when JNI dispatches the call. -/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). -/// -sealed class UcoConstructorData -{ - /// Name of the generated wrapper, e.g., "nctor_0_uco". - public string WrapperName { get; set; } = ""; - - /// Target type to pass to ActivateInstance. - public TypeRefData TargetType { get; set; } = new (); - - /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration. - public string JniSignature { get; set; } = "()V"; -} - /// /// An [UnmanagedCallersOnly] static wrapper for an [Export] method or constructor. /// Unlike which just forwards to an existing n_* callback, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 8ed331b73d3..6c26c5655ca 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -89,8 +89,17 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri foreach (var export in proxy.ExportMarshalMethods) { AddIfCrossAssembly (referencedAssemblies, export.DeclaringType.AssemblyName, assemblyName); } - if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { - AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); + // SetHandle is protected on Java.Lang.Object (Mono.Android) — needed for + // [Export] constructor marshal methods (nctor_N_uco) that call SetHandle directly. + // Inherited activation ctors call .ctor(IntPtr, JniHandleOwnership) which is public, + // but the base ctor's declaring assembly still needs IgnoresAccessChecksTo. + bool usesSetHandle = proxy.ExportMarshalMethods.Any (e => e.IsConstructor); + bool usesInheritedCtor = proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType; + if (usesSetHandle) { + AddIfCrossAssembly (referencedAssemblies, "Mono.Android", assemblyName); + } + if (usesInheritedCtor) { + AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor!.DeclaringType.AssemblyName, assemblyName); } } model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); @@ -282,21 +291,11 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) string wrapperName = $"nctor_{ctor.ConstructorIndex}_uco"; string nativeCallbackName = $"nctor_{ctor.ConstructorIndex}"; - if (mm.Connector == null) { - // [Export] constructor — generate full marshal body - var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, nativeCallbackName, isConstructor: true); - proxy.ExportMarshalMethods.Add (exportData); - } else { - // [Register] constructor — ActivateInstance pattern - proxy.UcoConstructors.Add (new UcoConstructorData { - WrapperName = wrapperName, - JniSignature = ctor.JniSignature, - TargetType = new TypeRefData { - ManagedTypeName = peer.ManagedTypeName, - AssemblyName = peer.AssemblyName, - }, - }); - } + // ALL constructors need full marshal body generation — there are no + // pre-existing n_* callbacks for constructors (unlike [Register] methods). + // The [Register] connector for constructors is always "" (empty string). + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, nativeCallbackName, isConstructor: true); + proxy.ExportMarshalMethods.Add (exportData); } } @@ -310,20 +309,6 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) }); } - foreach (var uco in proxy.UcoConstructors) { - string jniName = uco.WrapperName; - int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); - if (ucoSuffix >= 0) { - jniName = jniName.Substring (0, ucoSuffix); - } - - proxy.NativeRegistrations.Add (new NativeRegistrationData { - JniMethodName = jniName, - JniSignature = uco.JniSignature, - WrapperMethodName = uco.WrapperName, - }); - } - foreach (var export in proxy.ExportMarshalMethods) { proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = export.NativeCallbackName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 6d944d96eff..8bafad6ba87 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -52,7 +52,6 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; - MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; @@ -65,6 +64,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvGetStringRef; MemberReferenceHandle _jniEnvNewStringRef; MemberReferenceHandle _jniEnvToLocalJniHandleRef; + MemberReferenceHandle _setHandleRef; /// /// Creates a new emitter. @@ -216,14 +216,6 @@ void EmitMemberReferences (MetadataBuilder metadata) rt => rt.Void (), p => p.AddParameter ().Type ().String ())); - _activateInstanceRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "ActivateInstance", - sig => sig.MethodSignature ().Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); - _registerMethodRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "RegisterMethod", sig => sig.MethodSignature ().Parameters (4, rt => rt.Void (), @@ -293,6 +285,15 @@ void EmitMemberReferences (MetadataBuilder metadata) rt => rt.Type ().IntPtr (), p => p.AddParameter ().Type ().Type (_iJavaObjectRef, false))); + // Java.Lang.Object.SetHandle(IntPtr, JniHandleOwnership) : void — protected instance method + _setHandleRef = AddMemberRef (metadata, _javaLangObjectRef, "SetHandle", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + EmitTypeMapAttributeCtorRef (metadata); EmitTypeMapAssociationAttributeCtorRef (metadata); } @@ -401,17 +402,12 @@ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerPro MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); } - // UCO wrappers + // UCO wrappers (methods and constructors with [Register] connectors) foreach (var uco in proxy.UcoMethods) { var handle = EmitUcoMethod (metadata, ilBuilder, uco); wrapperHandles [uco.WrapperName] = handle; } - foreach (var uco in proxy.UcoConstructors) { - var handle = EmitUcoConstructor (metadata, ilBuilder, uco); - wrapperHandles [uco.WrapperName] = handle; - } - // Export marshal method wrappers (full marshal body) foreach (var export in proxy.ExportMarshalMethods) { var handle = EmitExportMarshalMethod (metadata, ilBuilder, export); @@ -473,8 +469,10 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe encoder.OpCode (ILOpCode.Ret); }); } else { - // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) - var baseActivationCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, activationCtor.DeclaringType)); + // Inherited ctor: GetUninitializedObject(typeof(T)) then call BaseType::.ctor(IntPtr, JniHandleOwnership) + // The base ctor does SetHandle + any other initialization the base class needs. + var baseTypeRef = ResolveTypeRef (metadata, activationCtor.DeclaringType); + var baseCtorRef = AddActivationCtorRef (metadata, baseTypeRef); EmitCreateInstanceBody (metadata, ilBuilder, encoder => { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (targetTypeRef); @@ -486,7 +484,7 @@ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPe encoder.OpCode (ILOpCode.Dup); encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); - encoder.Call (baseActivationCtorRef); + encoder.Call (baseCtorRef); encoder.OpCode (ILOpCode.Ret); }); @@ -569,59 +567,26 @@ MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBu return handle; } - MethodDefinitionHandle EmitUcoConstructor (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoConstructorData uco) - { - var userTypeRef = ResolveTypeRef (metadata, uco.TargetType); - - // UCO constructor wrappers must match the JNI native method signature exactly. - // The Java JCW declares e.g. "private native void nctor_0(Context p0)" and calls - // it with arguments. JNI dispatches with (JNIEnv*, jobject, ), - // so the wrapper signature must include all parameters to match the ABI. - // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because ActivateInstance creates the managed peer using the - // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. - var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); - int paramCount = 2 + jniParams.Count; - - var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (paramCount, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); // jnienv - p.AddParameter ().Type ().IntPtr (); // self - for (int j = 0; j < jniParams.Count; j++) - JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); - }), - encoder => { - encoder.LoadArgument (1); // self - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - encoder.OpCode (ILOpCode.Ret); - }); - - AddUnmanagedCallersOnlyAttribute (metadata, handle); - return handle; - } - // ---- Export marshal method wrappers ---- /// /// Emits a full marshal method body for an [Export] method or constructor. - /// Pattern: - /// static RetType n_Method(IntPtr jnienv, IntPtr native__this, ) { + /// Pattern for methods: + /// static RetType n_Method(IntPtr jnienv, IntPtr native__this, <JNI params...>) { /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return default; /// try { /// var __this = Object.GetObject<T>(jnienv, native__this, DoNotTransfer); /// // unmarshal params, call managed method, marshal return - /// } catch (Exception __e) { - /// __r.OnUserUnhandledException(ref __envp, __e); - /// return default; - /// } finally { - /// JniEnvironment.EndMarshalMethod(ref __envp); - /// } + /// } catch / finally ... + /// } + /// Pattern for constructors: + /// static void nctor_N_uco(IntPtr jnienv, IntPtr native__this, <ctor params...>) { + /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return; + /// try { + /// var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + /// __this.SetHandle(native__this, DoNotTransfer); // registers peer with runtime + /// __this..ctor(params...); // user constructor + /// } catch / finally ... /// } /// MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, ExportMarshalMethodData export) @@ -641,29 +606,33 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); + // Resolve managed type references (needed early for locals signature in constructor case) + var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType); + // Build the locals signature: // local 0: JniTransition __envp // local 1: JniRuntime __r // local 2: Exception __e // local 3: __ret (only for non-void methods) + // local 3 (ctor): T __this (for constructors — holds the uninitialized object) int localCount = isVoid ? 3 : 4; + if (export.IsConstructor) localCount = 4; // __envp, __r, __e, __this var localsBlob = new BlobBuilder (32); var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (localCount); localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0 localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1 localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2 - if (!isVoid) { - JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3 + if (export.IsConstructor) { + localsEncoder.AddVariable ().Type ().Type (declaringTypeRef, false); // local 3: T __this + } else if (!isVoid) { + JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3: __ret } var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob)); - // Resolve managed type references - var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType); - // Build GetObject method spec — generic instantiation of Object.GetObject - // Not needed for static methods (no 'this' object to unmarshal) + // Not needed for static methods or constructors EntityHandle getObjectRef = default; - if (!export.IsStatic) { + if (!export.IsStatic && !export.IsConstructor) { getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef); } @@ -705,17 +674,27 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.MarkLabel (tryStartLabel); if (export.IsConstructor) { - // For constructors: ActivateInstance first, then get the managed object and call ctor - // ActivateInstance(native__this, typeof(T)) - encoder.LoadArgument (1); // native__this + // Constructor: create uninitialized object, call activation ctor, then user ctor + // var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (declaringTypeRef); encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - } + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (declaringTypeRef); + encoder.OpCode (ILOpCode.Stloc_3); // store in local 3: __this + + // __this.SetHandle(native__this, JniHandleOwnership.DoNotTransfer) + // — registers the peer with the runtime and sets up the JNI handle association + encoder.OpCode (ILOpCode.Ldloc_3); // __this + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 + encoder.Call (_setHandleRef); - if (!export.IsStatic) { - // Instance methods/constructors: get managed object from JNI handle + // Load __this for the user .ctor call below + encoder.OpCode (ILOpCode.Ldloc_3); + } else if (!export.IsStatic) { + // Instance method: get managed object from JNI handle encoder.LoadArgument (0); // jnienv encoder.LoadArgument (1); // native__this encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index cf5c478109d..8ae673ec67d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -316,10 +316,12 @@ static void AddMarshalMethod (List methods, RegisterInfo regi var parameters = ParseJniParameters (jniSignature); bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0; - // For [Export] methods, populate ManagedType from the actual method signature - // (needed for the generated marshal method body) + // For [Export] methods and constructors, populate ManagedType from the actual + // method signature (needed for the generated marshal method body). + // Constructors always need this because there are no pre-existing n_* callbacks. + bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; string? managedReturnType = null; - if (registerInfo.Connector == null) { + if (registerInfo.Connector == null || isConstructor) { var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); for (int i = 0; i < parameters.Count && i < sig.ParameterTypes.Length; i++) { parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i]); @@ -336,7 +338,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi NativeCallbackName = string.Concat ("n_", methodName), JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature), Parameters = parameters, - IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + IsConstructor = isConstructor, IsStatic = isStatic, ThrownNames = registerInfo.ThrownNames, SuperArgumentsString = registerInfo.SuperArgumentsString, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 5d95eed8c45..8089072fffa 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -943,6 +943,23 @@ public void Generate_SimpleActivity_UsesGetUninitializedObject () .ToList (); Assert.DoesNotContain ("CreateManagedPeer", memberNames); Assert.Contains ("GetUninitializedObject", memberNames); + + // The .ctor MemberRef must target the base type that declares the activation ctor + var baseTypeName = simpleActivity.ActivationCtor.DeclaringTypeName; + var baseSimpleName = baseTypeName.Contains ('.') ? baseTypeName.Substring (baseTypeName.LastIndexOf ('.') + 1) : baseTypeName; + var ctorMemberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + // One of the .ctor refs must be on the base type + bool hasBaseCtorRef = ctorMemberRefs.Any (m => { + if (m.Parent.Kind == HandleKind.TypeReference) { + var tr = reader.GetTypeReference ((TypeReferenceHandle)m.Parent); + return reader.GetString (tr.Name) == baseSimpleName; + } + return false; + }); + Assert.True (hasBaseCtorRef, $"Should have .ctor MemberRef on base type {baseSimpleName}"); } } finally { CleanUp (path); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index a2e691cb35e..3c1212fc4fa 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -286,6 +286,51 @@ public void Build_PeerWithActivationCtor_CreatesProxy () Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); } + [Fact] + public void Build_LeafActivationCtor_IsOnLeafTypeTrue () + { + var peer = MakePeerWithActivation ("my/app/Foo", "MyApp.Foo", "App"); + // Leaf: the type itself declares the activation ctor + peer.ActivationCtor!.DeclaringTypeName = "MyApp.Foo"; + peer.ActivationCtor!.DeclaringAssemblyName = "App"; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.NotNull (proxy.ActivationCtor); + Assert.True (proxy.ActivationCtor!.IsOnLeafType); + Assert.Equal ("MyApp.Foo", proxy.ActivationCtor.DeclaringType.ManagedTypeName); + } + + [Fact] + public void Build_InheritedActivationCtor_IsOnLeafTypeFalse () + { + var peer = MakePeerWithActivation ("my/app/Bar", "MyApp.Bar", "App"); + // Inherited: a base type declares the activation ctor + peer.ActivationCtor!.DeclaringTypeName = "MyApp.BaseBar"; + peer.ActivationCtor!.DeclaringAssemblyName = "App"; + + var model = BuildModel (new [] { peer }); + var proxy = model.ProxyTypes [0]; + + Assert.NotNull (proxy.ActivationCtor); + Assert.False (proxy.ActivationCtor!.IsOnLeafType); + Assert.Equal ("MyApp.BaseBar", proxy.ActivationCtor.DeclaringType.ManagedTypeName); + Assert.Equal ("App", proxy.ActivationCtor.DeclaringType.AssemblyName); + } + + [Fact] + public void Build_InheritedActivationCtor_CrossAssembly_AddsIgnoresAccessChecksTo () + { + var peer = MakePeerWithActivation ("my/app/Baz", "MyApp.Baz", "App"); + peer.ActivationCtor!.DeclaringTypeName = "Base.Activity"; + peer.ActivationCtor!.DeclaringAssemblyName = "Mono.Android"; + + var model = BuildModel (new [] { peer }, "TypeMapAsm"); + + Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo); + } + [Fact] public void Build_PeerWithInvoker_CreatesProxy () { @@ -429,12 +474,19 @@ public void Build_AcwWithMarshalMethods_CreatesUcoMethods () var proxy = model.ProxyTypes [0]; Assert.Equal (2, proxy.UcoMethods.Count); + + // Method UCOs (only non-constructor methods) Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName); Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName); Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature); Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName); Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName); + + // Constructor goes into ExportMarshalMethods (full marshal body) + var ctorExport = proxy.ExportMarshalMethods.FirstOrDefault (e => e.IsConstructor); + Assert.NotNull (ctorExport); + Assert.Equal ("nctor_0_uco", ctorExport.WrapperName); } [Fact] @@ -484,31 +536,34 @@ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () var model = BuildModel (new [] { peer }); var proxy = model.ProxyTypes [0]; - // Only 1 UCO method (constructors are skipped from UcoMethods) + // Constructors go to ExportMarshalMethods, not UcoMethods Assert.Single (proxy.UcoMethods); Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName); + Assert.NotEmpty (proxy.ExportMarshalMethods.Where (e => e.IsConstructor)); } } - public class UcoConstructors + public class ConstructorMarshalMethods { [Fact] - public void Build_AcwWithConstructors_CreatesUcoConstructors () + public void Build_AcwWithConstructors_CreatesExportMarshalMethod () { var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); var model = BuildModel (new [] { peer }); var proxy = model.ProxyTypes [0]; - Assert.Single (proxy.UcoConstructors); - Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); - Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName); + // All constructors generate full marshal bodies in ExportMarshalMethods + var ctorExport = proxy.ExportMarshalMethods.FirstOrDefault (e => e.IsConstructor); + Assert.NotNull (ctorExport); + Assert.Equal ("nctor_0_uco", ctorExport.WrapperName); + Assert.Equal ("nctor_0", ctorExport.NativeCallbackName); } [Fact] - public void Build_PeerWithoutActivationCtor_NoUcoConstructors () + public void Build_PeerWithoutActivationCtor_NoConstructorMarshalMethods () { // Peer with marshal methods but no activation ctor var peer = new JavaPeerInfo { @@ -529,7 +584,8 @@ public void Build_PeerWithoutActivationCtor_NoUcoConstructors () var model = BuildModel (new [] { peer }); var proxy = model.ProxyTypes [0]; - Assert.Empty (proxy.UcoConstructors); + // No activation ctor → no constructor marshal methods + Assert.DoesNotContain (proxy.ExportMarshalMethods, e => e.IsConstructor); } } @@ -818,7 +874,6 @@ public void Fixture_JavaLangObject_HasActivation_CreatesProxy () // MCW with DoNotGenerateAcw → not ACW Assert.False (proxy.IsAcw); Assert.Empty (proxy.UcoMethods); - Assert.Empty (proxy.UcoConstructors); Assert.Empty (proxy.NativeRegistrations); } @@ -1066,15 +1121,15 @@ public void Fixture_CustomView_HasTwoConstructorWrappers () Assert.NotNull (proxy); if (proxy!.IsAcw) { - Assert.Equal (2, proxy.UcoConstructors.Count); - Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName); - Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName); - Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName); - Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName); + // Constructors go into ExportMarshalMethods (full marshal body) + var ctorExports = proxy.ExportMarshalMethods.Where (e => e.IsConstructor).ToList (); + Assert.Equal (2, ctorExports.Count); + Assert.Equal ("nctor_0_uco", ctorExports [0].WrapperName); + Assert.Equal ("nctor_1_uco", ctorExports [1].WrapperName); // Constructor JNI signatures should be propagated - Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature); - Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature); + Assert.Equal ("()V", ctorExports [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorExports [1].JniSignature); // Constructor registrations must use the actual JNI signatures var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList (); @@ -1362,8 +1417,8 @@ public void Fixture_ExportsConstructors_ExportConstructorsInExportMarshalMethods var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsConstructors_Proxy"); Assert.NotNull (proxy); - // [Export] constructors go into ExportMarshalMethods, not UcoConstructors - Assert.Empty (proxy!.UcoConstructors); + // [Export] constructors go into ExportMarshalMethods, not UcoMethods + Assert.DoesNotContain (proxy!.UcoMethods, u => u.WrapperName.StartsWith ("nctor_")); Assert.Equal (exportCtors.Count, proxy.ExportMarshalMethods.Count (e => e.IsConstructor)); } @@ -1375,8 +1430,8 @@ public void Fixture_ExportsThrowsConstructors_ExportConstructorsInExportMarshalM var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsThrowsConstructors_Proxy"); Assert.NotNull (proxy); - // All constructors are [Export] → in ExportMarshalMethods, not UcoConstructors - Assert.Empty (proxy!.UcoConstructors); + // All constructors are [Export] → in ExportMarshalMethods, not UcoMethods + Assert.DoesNotContain (proxy!.UcoMethods, u => u.WrapperName.StartsWith ("nctor_")); Assert.NotEmpty (proxy.ExportMarshalMethods); Assert.All (proxy.ExportMarshalMethods, e => Assert.True (e.IsConstructor)); } @@ -1754,15 +1809,15 @@ public void FullPipeline_CustomView_UcoConstructorMatchesJniSignature () Assert.NotEmpty (ucoCtors); - // Match each UCO constructor to its model data to verify param count + // Match each constructor to its model data to verify param count foreach (var uco in ucoCtors) { var name = reader.GetString (uco.Name); - var modelUco = model.ProxyTypes - .SelectMany (p => p.UcoConstructors) - .First (u => u.WrapperName == name); + var modelExport = model.ProxyTypes + .SelectMany (p => p.ExportMarshalMethods) + .First (e => e.WrapperName == name); - // UCO constructor signature must include jnienv + self + JNI params - int expectedJniParams = JniSignatureHelper.ParseParameterTypes (modelUco.JniSignature).Count; + // Constructor signature must include jnienv + self + JNI params + int expectedJniParams = JniSignatureHelper.ParseParameterTypes (modelExport.JniSignature).Count; int expectedTotal = 2 + expectedJniParams; var sig = reader.GetBlobReader (uco.Signature); From 2b1860ba1714b848a5b78f16db64e556ac24362d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 13 Feb 2026 11:13:32 +0100 Subject: [PATCH 43/43] Implement remaining [Export] marshaling gaps - Support arrays in export marshal wrappers using JNIEnv.GetArray/NewArray/CopyArray including copy-back cleanup for array parameters - Support enum signatures and marshaling as JNI int while calling managed enum methods - Map Java.Lang.ICharSequence to Ljava/lang/CharSequence; in export signature generation - Resolve JNI object descriptors from [Register] metadata for managed object types (e.g. View[] -> [Landroid/view/View; instead of Object[]) - Improve ManagedTypeToAssemblyQualifiedName to resolve non-BCL types and array element types - Generate proxies for export-only ACW types (no activation ctor / no [Register] methods) Add fixture coverage and tests for export-only proxies plus array/enum/charsequence signatures and registration. All 312 tests pass. --- .../Generator/ModelBuilder.cs | 2 +- .../Generator/TypeMapAssemblyEmitter.cs | 374 +++++++++++++++--- .../Scanner/JavaPeerScanner.cs | 207 +++++++--- .../TypeMapAssemblyGeneratorTests.cs | 78 +++- .../Generator/TypeMapModelBuilderTests.cs | 53 ++- .../Scanner/JavaPeerScannerTests.cs | 28 ++ .../TestFixtures/TestTypes.cs | 38 ++ 7 files changed, 670 insertions(+), 110 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 6c26c5655ca..2264f7ec756 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -117,8 +117,8 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, var peer = peersForName [i]; string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; - bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw; JavaPeerProxyData? proxy = null; if (hasProxy) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8bafad6ba87..c6f1b8b0748 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -45,6 +45,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniRuntimeRef; TypeReferenceHandle _javaLangObjectRef; TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _charSequenceRef; TypeReferenceHandle _systemExceptionRef; TypeReferenceHandle _iJavaObjectRef; @@ -62,8 +63,13 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _endMarshalMethodRef; MemberReferenceHandle _onUserUnhandledExceptionRef; MemberReferenceHandle _jniEnvGetStringRef; + MemberReferenceHandle _jniEnvGetCharSequenceRef; MemberReferenceHandle _jniEnvNewStringRef; MemberReferenceHandle _jniEnvToLocalJniHandleRef; + MemberReferenceHandle _charSequenceToLocalJniHandleStringRef; + MemberReferenceHandle _jniEnvGetArrayOpenRef; + MemberReferenceHandle _jniEnvNewArrayOpenRef; + MemberReferenceHandle _jniEnvCopyArrayOpenRef; MemberReferenceHandle _setHandleRef; /// @@ -190,6 +196,8 @@ void EmitTypeReferences (MetadataBuilder metadata) metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); _jniEnvRef = metadata.AddTypeReference (_monoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); + _charSequenceRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("CharSequence")); _systemExceptionRef = metadata.AddTypeReference (_systemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); _iJavaObjectRef = metadata.AddTypeReference (_javaInteropRef, @@ -273,18 +281,63 @@ void EmitMemberReferences (MetadataBuilder metadata) p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); + // JNIEnv.GetCharSequence(IntPtr, JniHandleOwnership) : string + _jniEnvGetCharSequenceRef = AddMemberRef (metadata, _jniEnvRef, "GetCharSequence", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + // JNIEnv.NewString(string) : IntPtr _jniEnvNewStringRef = AddMemberRef (metadata, _jniEnvRef, "NewString", sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().IntPtr (), p => p.AddParameter ().Type ().String ())); + // CharSequence.ToLocalJniHandle(string) : IntPtr + _charSequenceToLocalJniHandleStringRef = AddMemberRef (metadata, _charSequenceRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().String ())); + // JNIEnv.ToLocalJniHandle(IJavaObject) : IntPtr _jniEnvToLocalJniHandleRef = AddMemberRef (metadata, _jniEnvRef, "ToLocalJniHandle", sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().IntPtr (), p => p.AddParameter ().Type ().Type (_iJavaObjectRef, false))); + // JNIEnv.GetArray(IntPtr) : T[] + _jniEnvGetArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "GetArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (1, + rt => rt.Type ().SZArray ().GenericMethodTypeParameter (0), + p => p.AddParameter ().Type ().IntPtr ()); + }); + + // JNIEnv.NewArray(T[]) : IntPtr + _jniEnvNewArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "NewArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0)); + }); + + // JNIEnv.CopyArray(T[], IntPtr) : void + _jniEnvCopyArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "CopyArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0); + p.AddParameter ().Type ().IntPtr (); + }); + }); + // Java.Lang.Object.SetHandle(IntPtr, JniHandleOwnership) : void — protected instance method _setHandleRef = AddMemberRef (metadata, _javaLangObjectRef, "SetHandle", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, @@ -592,6 +645,8 @@ MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBu MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, ExportMarshalMethodData export) { var jniParams = JniSignatureHelper.ParseParameterTypes (export.JniSignature); + var jniParamTypes = JniSignatureHelper.ParseParameterTypeStrings (export.JniSignature); + var jniReturnType = export.IsConstructor ? "V" : JniSignatureHelper.ParseReturnTypeString (export.JniSignature); var returnKind = export.IsConstructor ? JniParamKind.Void : JniSignatureHelper.ParseReturnType (export.JniSignature); bool isVoid = returnKind == JniParamKind.Void; int jniParamCount = 2 + jniParams.Count; // jnienv + self + method params @@ -615,8 +670,10 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu // local 2: Exception __e // local 3: __ret (only for non-void methods) // local 3 (ctor): T __this (for constructors — holds the uninitialized object) - int localCount = isVoid ? 3 : 4; - if (export.IsConstructor) localCount = 4; // __envp, __r, __e, __this + int fixedLocalCount = export.IsConstructor || !isVoid ? 4 : 3; + int parameterLocalStart = fixedLocalCount; + int[] parameterLocals = new int [export.ManagedParameters.Count]; + int localCount = fixedLocalCount + export.ManagedParameters.Count; var localsBlob = new BlobBuilder (32); var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (localCount); localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0 @@ -627,6 +684,11 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu } else if (!isVoid) { JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3: __ret } + for (int i = 0; i < export.ManagedParameters.Count; i++) { + parameterLocals [i] = parameterLocalStart + i; + EncodeManagedTypeForExportCall (localsEncoder.AddVariable ().Type (), metadata, + export.ManagedParameters [i].ManagedTypeName, export.ManagedParameters [i].AssemblyName, jniParams [i], export.ManagedParameters [i].JniType); + } var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob)); // Build GetObject method spec — generic instantiation of Object.GetObject @@ -690,9 +752,6 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.LoadArgument (1); // native__this encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 encoder.Call (_setHandleRef); - - // Load __this for the user .ctor call below - encoder.OpCode (ILOpCode.Ldloc_3); } else if (!export.IsStatic) { // Instance method: get managed object from JNI handle encoder.LoadArgument (0); // jnienv @@ -701,9 +760,18 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu encoder.Call (getObjectRef); } - // Unmarshal each parameter + // Unmarshal each parameter into locals + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], jniParamTypes [i], i + 2); + StoreLocal (encoder, parameterLocals [i]); + } + + // Load target + managed parameters for the managed call + if (export.IsConstructor) { + encoder.OpCode (ILOpCode.Ldloc_3); + } for (int i = 0; i < export.ManagedParameters.Count; i++) { - EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], i + 2); + LoadLocal (encoder, parameterLocals [i]); } // Call managed method: static → call, instance ctor → call, instance method → callvirt @@ -716,10 +784,17 @@ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBu // Marshal return value and store in local 3 if (!isVoid) { - EmitReturnMarshal (encoder, returnKind, export.ManagedReturnType); + EmitReturnMarshal (encoder, metadata, returnKind, jniReturnType, export.ManagedReturnType); encoder.OpCode (ILOpCode.Stloc_3); } + // Copy back array parameter changes to JNI arrays + for (int i = 0; i < export.ManagedParameters.Count; i++) { + if (IsManagedArrayType (export.ManagedParameters [i].ManagedTypeName)) { + EmitArrayParameterCopyBack (encoder, metadata, export.ManagedParameters [i], parameterLocals [i], i + 2); + } + } + // leave to after the handler encoder.Branch (ILOpCode.Leave_s, returnLabel); encoder.MarkLabel (tryEndLabel); @@ -830,7 +905,7 @@ MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMars if (isVoid) { rt.Void (); } else { - EncodeExportReturnType (rt, metadata, export.ManagedReturnType, returnKind); + EncodeExportReturnType (rt, metadata, export.ManagedReturnType, returnKind, JniSignatureHelper.ParseReturnTypeString (export.JniSignature)); } }, p => { @@ -842,62 +917,148 @@ MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMars void EncodeExportParamType (ParametersEncoder p, MetadataBuilder metadata, ExportParamData param) { var jniKind = JniSignatureHelper.ParseSingleTypeFromDescriptor (param.JniType); - if (jniKind != JniParamKind.Object) { + if (!string.IsNullOrEmpty (param.ManagedTypeName)) { + EncodeManagedTypeForExportCall (p.AddParameter ().Type (), metadata, param.ManagedTypeName, param.AssemblyName, jniKind, param.JniType); + } else if (jniKind != JniParamKind.Object) { JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniKind); - } else if (param.ManagedTypeName == "System.String") { - p.AddParameter ().Type ().String (); } else { - var typeRef = ResolveTypeRef (metadata, new TypeRefData { - ManagedTypeName = param.ManagedTypeName, - AssemblyName = param.AssemblyName, - }); - p.AddParameter ().Type ().Type (typeRef, false); + p.AddParameter ().Type ().IntPtr (); } } - void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, string? managedReturnType, JniParamKind returnKind) + void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, string? managedReturnType, JniParamKind returnKind, string jniReturnType) { - if (returnKind != JniParamKind.Object) { - JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); - } else if (managedReturnType == "System.String") { - rt.Type ().String (); - } else if (managedReturnType != null) { - // Resolve the managed return type for the method ref signature - string typeName = managedReturnType; + if (!string.IsNullOrEmpty (managedReturnType)) { + string typeName = managedReturnType!; string assemblyName = ""; - int commaIndex = managedReturnType.IndexOf (", ", StringComparison.Ordinal); + int commaIndex = typeName.IndexOf (", ", StringComparison.Ordinal); if (commaIndex >= 0) { - assemblyName = managedReturnType.Substring (commaIndex + 2); - typeName = managedReturnType.Substring (0, commaIndex); - } - if (assemblyName.Length > 0) { - var typeRef = ResolveTypeRef (metadata, new TypeRefData { - ManagedTypeName = typeName, - AssemblyName = assemblyName, - }); - rt.Type ().Type (typeRef, false); - } else { - // Fallback: no assembly info available, use IntPtr - rt.Type ().IntPtr (); + assemblyName = typeName.Substring (commaIndex + 2); + typeName = typeName.Substring (0, commaIndex); } + EncodeManagedTypeForExportCall (rt.Type (), metadata, typeName, assemblyName, returnKind, jniReturnType); + } else if (returnKind != JniParamKind.Object) { + JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); } else { rt.Type ().IntPtr (); } } - void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, JniParamKind jniKind, int argIndex) + void EncodeManagedTypeForExportCall (SignatureTypeEncoder encoder, MetadataBuilder metadata, + string managedTypeName, string assemblyName, JniParamKind jniKind, string jniType) + { + if (TryEncodeManagedPrimitiveType (encoder, managedTypeName)) { + return; + } + + if (managedTypeName == "System.String") { + encoder.String (); + return; + } + + if (IsManagedArrayType (managedTypeName)) { + EncodeManagedArrayType (encoder, metadata, managedTypeName, assemblyName, jniType); + return; + } + + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + encoder.Type (typeRef, IsEnumManagedType (managedTypeName, jniKind)); + } + + void EncodeManagedArrayType (SignatureTypeEncoder encoder, MetadataBuilder metadata, string managedArrayTypeName, string assemblyName, string jniType) { + string elementType = managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2); + var arrayEncoder = encoder.SZArray (); + if (TryEncodeManagedPrimitiveType (arrayEncoder, elementType)) { + return; + } + if (elementType == "System.String") { + arrayEncoder.String (); + return; + } + var elementRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = elementType, + AssemblyName = assemblyName, + }); + var elementJniKind = jniType.StartsWith ("[", StringComparison.Ordinal) + ? JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.Substring (1)) + : JniParamKind.Object; + arrayEncoder.Type (elementRef, IsEnumManagedType (elementType, elementJniKind)); + } + + static bool TryEncodeManagedPrimitiveType (SignatureTypeEncoder encoder, string managedTypeName) + { + switch (managedTypeName) { + case "System.Boolean": encoder.Boolean (); return true; + case "System.SByte": encoder.SByte (); return true; + case "System.Byte": encoder.Byte (); return true; + case "System.Char": encoder.Char (); return true; + case "System.Int16": encoder.Int16 (); return true; + case "System.UInt16": encoder.UInt16 (); return true; + case "System.Int32": encoder.Int32 (); return true; + case "System.UInt32": encoder.UInt32 (); return true; + case "System.Int64": encoder.Int64 (); return true; + case "System.UInt64": encoder.UInt64 (); return true; + case "System.Single": encoder.Single (); return true; + case "System.Double": encoder.Double (); return true; + case "System.IntPtr": encoder.IntPtr (); return true; + case "System.UIntPtr": encoder.UIntPtr (); return true; + default: return false; + } + } + + static bool IsManagedArrayType (string managedTypeName) + => managedTypeName.EndsWith ("[]", StringComparison.Ordinal); + + static bool IsEnumManagedType (string managedTypeName, JniParamKind jniKind) + { + if (jniKind == JniParamKind.Object || IsManagedArrayType (managedTypeName)) { + return false; + } + return !string.Equals (managedTypeName, "System.Boolean", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.SByte", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Byte", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Char", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int16", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt16", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int32", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt32", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int64", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt64", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Single", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Double", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.IntPtr", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UIntPtr", StringComparison.Ordinal); + } + + void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, JniParamKind jniKind, string jniType, int argIndex) + { + if (IsManagedArrayType (param.ManagedTypeName)) { + // Arrays: JNIEnv.GetArray(handle) + var getArraySpec = BuildArrayMethodSpec (metadata, _jniEnvGetArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType); + encoder.LoadArgument (argIndex); + encoder.Call (getArraySpec); + return; + } + if (jniKind != JniParamKind.Object) { - // Primitives: just load the argument directly encoder.LoadArgument (argIndex); + if (jniKind == JniParamKind.Boolean && param.ManagedTypeName == "System.Boolean") { + // JNI jboolean is byte; managed bool expects 0/1 semantics. + encoder.OpCode (ILOpCode.Ldc_i4_0); + encoder.OpCode (ILOpCode.Cgt_un); + } return; } if (param.ManagedTypeName == "System.String") { - // String: JNIEnv.GetString(handle, DoNotTransfer) + // String: GetString or GetCharSequence depending on JNI descriptor. encoder.LoadArgument (argIndex); encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer - encoder.Call (_jniEnvGetStringRef); + encoder.Call (jniType == "Ljava/lang/CharSequence;" ? _jniEnvGetCharSequenceRef : _jniEnvGetStringRef); return; } @@ -929,16 +1090,31 @@ void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadat encoder.Call (methodSpec); } - void EmitReturnMarshal (InstructionEncoder encoder, JniParamKind returnKind, string? managedReturnType) + void EmitReturnMarshal (InstructionEncoder encoder, MetadataBuilder metadata, JniParamKind returnKind, string jniReturnType, string? managedReturnType) { + string? managedTypeName = null; + string managedAssemblyName = ""; + if (!string.IsNullOrEmpty (managedReturnType)) { + (managedTypeName, managedAssemblyName) = SplitManagedTypeNameAndAssembly (managedReturnType!); + } + + if (!string.IsNullOrEmpty (managedTypeName) && IsManagedArrayType (managedTypeName!)) { + // Managed array -> JNI array + var newArraySpec = BuildArrayMethodSpec (metadata, _jniEnvNewArrayOpenRef, managedTypeName!, managedAssemblyName, jniReturnType); + encoder.Call (newArraySpec); + return; + } + if (returnKind != JniParamKind.Object) { - // Primitives: return directly (value is already on the stack) + // Enum return values are marshaled as int (legacy behavior). + if (!string.IsNullOrEmpty (managedTypeName) && IsEnumManagedType (managedTypeName!, returnKind)) { + encoder.OpCode (ILOpCode.Conv_i4); + } return; } - if (managedReturnType == "System.String") { - // String: JNIEnv.NewString(result) - encoder.Call (_jniEnvNewStringRef); + if (managedTypeName == "System.String") { + encoder.Call (jniReturnType == "Ljava/lang/CharSequence;" ? _charSequenceToLocalJniHandleStringRef : _jniEnvNewStringRef); return; } @@ -946,6 +1122,110 @@ void EmitReturnMarshal (InstructionEncoder encoder, JniParamKind returnKind, str encoder.Call (_jniEnvToLocalJniHandleRef); } + void EmitArrayParameterCopyBack (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, int localIndex, int argIndex) + { + var skipLabel = encoder.DefineLabel (); + LoadLocal (encoder, localIndex); + encoder.Branch (ILOpCode.Brfalse_s, skipLabel); + LoadLocal (encoder, localIndex); + encoder.LoadArgument (argIndex); + var copyArraySpec = BuildArrayMethodSpec (metadata, _jniEnvCopyArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType); + encoder.Call (copyArraySpec); + encoder.MarkLabel (skipLabel); + } + + EntityHandle BuildArrayMethodSpec (MetadataBuilder metadata, MemberReferenceHandle openMethodRef, string managedArrayTypeName, string assemblyName, string jniType) + { + string elementTypeName = managedArrayTypeName.EndsWith ("[]", StringComparison.Ordinal) + ? managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2) + : managedArrayTypeName; + + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (method) + instBlob.WriteCompressedInteger (1); // one type argument + WriteGenericTypeArgument (instBlob, metadata, elementTypeName, assemblyName, + IsEnumManagedType (elementTypeName, JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.StartsWith ("[", StringComparison.Ordinal) ? jniType.Substring (1) : jniType))); + return metadata.AddMethodSpecification (openMethodRef, metadata.GetOrAddBlob (instBlob)); + } + + void WriteGenericTypeArgument (BlobBuilder blob, MetadataBuilder metadata, string managedTypeName, string assemblyName, bool isValueType) + { + if (TryGetPrimitiveElementTypeCode (managedTypeName, out byte primitiveCode)) { + blob.WriteByte (primitiveCode); + return; + } + + if (managedTypeName == "System.String") { + blob.WriteByte (0x0E); // ELEMENT_TYPE_STRING + return; + } + + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + blob.WriteByte (isValueType ? (byte) 0x11 : (byte) 0x12); // VALUETYPE | CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef)); + } + + static bool TryGetPrimitiveElementTypeCode (string managedTypeName, out byte typeCode) + { + switch (managedTypeName) { + case "System.Boolean": typeCode = 0x02; return true; + case "System.Char": typeCode = 0x03; return true; + case "System.SByte": typeCode = 0x04; return true; + case "System.Byte": typeCode = 0x05; return true; + case "System.Int16": typeCode = 0x06; return true; + case "System.UInt16": typeCode = 0x07; return true; + case "System.Int32": typeCode = 0x08; return true; + case "System.UInt32": typeCode = 0x09; return true; + case "System.Int64": typeCode = 0x0A; return true; + case "System.UInt64": typeCode = 0x0B; return true; + case "System.Single": typeCode = 0x0C; return true; + case "System.Double": typeCode = 0x0D; return true; + default: + typeCode = 0; + return false; + } + } + + static (string managedTypeName, string assemblyName) SplitManagedTypeNameAndAssembly (string managedType) + { + int commaIndex = managedType.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex < 0) { + return (managedType, ""); + } + return (managedType.Substring (0, commaIndex), managedType.Substring (commaIndex + 2)); + } + + static void LoadLocal (InstructionEncoder encoder, int localIndex) + { + switch (localIndex) { + case 0: encoder.OpCode (ILOpCode.Ldloc_0); return; + case 1: encoder.OpCode (ILOpCode.Ldloc_1); return; + case 2: encoder.OpCode (ILOpCode.Ldloc_2); return; + case 3: encoder.OpCode (ILOpCode.Ldloc_3); return; + default: + encoder.OpCode (ILOpCode.Ldloc_s); + encoder.CodeBuilder.WriteByte ((byte) localIndex); + return; + } + } + + static void StoreLocal (InstructionEncoder encoder, int localIndex) + { + switch (localIndex) { + case 0: encoder.OpCode (ILOpCode.Stloc_0); return; + case 1: encoder.OpCode (ILOpCode.Stloc_1); return; + case 2: encoder.OpCode (ILOpCode.Stloc_2); return; + case 3: encoder.OpCode (ILOpCode.Stloc_3); return; + default: + encoder.OpCode (ILOpCode.Stloc_s); + encoder.CodeBuilder.WriteByte ((byte) localIndex); + return; + } + } + static void EmitDefaultReturnValue (InstructionEncoder encoder, JniParamKind kind) { switch (kind) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 8ae673ec67d..28d9ccbf2ef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -304,7 +304,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results return (methods, exportFields); } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) + void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) { // Skip methods that are just the JNI name (type-level [Register]) if (registerInfo.Signature == null && registerInfo.Connector == null) { @@ -324,10 +324,10 @@ static void AddMarshalMethod (List methods, RegisterInfo regi if (registerInfo.Connector == null || isConstructor) { var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); for (int i = 0; i < parameters.Count && i < sig.ParameterTypes.Length; i++) { - parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i]); + parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i], index); } if (sig.ReturnType != "System.Void") { - managedReturnType = ManagedTypeToAssemblyQualifiedName (sig.ReturnType); + managedReturnType = ManagedTypeToAssemblyQualifiedName (sig.ReturnType, index); } } methods.Add (new MarshalMethodInfo { @@ -404,7 +404,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem return null; } - static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = ca.DecodeValue (index.customAttributeTypeProvider); @@ -434,7 +434,7 @@ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition m // Build JNI signature from method signature var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var jniSig = BuildJniSignatureFromManaged (sig); + var jniSig = BuildJniSignatureFromManaged (sig, index); return new RegisterInfo (exportName, jniSig, null, false, thrownNames: thrownNames, superArgumentsString: superArguments); @@ -444,11 +444,11 @@ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition m /// Creates a RegisterInfo for an [ExportField] method. /// The method is registered like [Export] (Connector = null) so it gets a full marshal body. /// - static RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) { var methodName = index.Reader.GetString (methodDef.Name); var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var jniSig = BuildJniSignatureFromManaged (sig); + var jniSig = BuildJniSignatureFromManaged (sig, index); return new RegisterInfo (methodName, jniSig, null, false); } @@ -492,19 +492,19 @@ static RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, return null; } - static string BuildJniSignatureFromManaged (MethodSignature sig) + string BuildJniSignatureFromManaged (MethodSignature sig, AssemblyIndex index) { var sb = new System.Text.StringBuilder (); sb.Append ('('); foreach (var param in sig.ParameterTypes) { - sb.Append (ManagedTypeToJniDescriptor (param)); + sb.Append (ManagedTypeToJniDescriptor (param, index)); } sb.Append (')'); - sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType, index)); return sb.ToString (); } - static string ManagedTypeToJniDescriptor (string managedType) + string ManagedTypeToJniDescriptor (string managedType, AssemblyIndex index) { switch (managedType) { case "System.Void": return "V"; @@ -521,9 +521,17 @@ static string ManagedTypeToJniDescriptor (string managedType) case "System.Single": return "F"; case "System.Double": return "D"; case "System.String": return "Ljava/lang/String;"; + case "Java.Lang.ICharSequence": return "Ljava/lang/CharSequence;"; default: if (managedType.EndsWith ("[]")) { - return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); + return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2), index); + } + if (IsEnumManagedType (managedType, index)) { + return "I"; + } + var jniName = ResolveManagedTypeJniName (managedType, index); + if (!string.IsNullOrEmpty (jniName)) { + return "L" + jniName + ";"; } return "Ljava/lang/Object;"; } @@ -533,8 +541,12 @@ static string ManagedTypeToJniDescriptor (string managedType) /// Maps a managed type name (from SignatureTypeProvider) to an assembly-qualified name /// like "System.Int32, System.Private.CoreLib" used in TypeManager.Activate calls. /// - static string ManagedTypeToAssemblyQualifiedName (string managedType) + string ManagedTypeToAssemblyQualifiedName (string managedType, AssemblyIndex index) { + if (managedType.IndexOf (", ", StringComparison.Ordinal) >= 0) { + return managedType; + } + // BCL types all live in System.Private.CoreLib switch (managedType) { case "System.Void": @@ -556,11 +568,131 @@ static string ManagedTypeToAssemblyQualifiedName (string managedType) case "System.UIntPtr": return managedType + ", System.Private.CoreLib"; default: - // For non-BCL types, we don't know the assembly at this point. - // This is a best-effort mapping; full assembly resolution for - // arbitrary types is a follow-up. - return managedType; + // Best-effort assembly resolution across loaded assemblies. + var assemblyName = ResolveManagedTypeAssemblyName (managedType, index); + return assemblyName != null ? managedType + ", " + assemblyName : managedType; + } + } + + string? ResolveManagedTypeAssemblyName (string managedType, AssemblyIndex index) + { + string typeName = StripManagedTypeDecorations (managedType); + if (typeName.Length == 0 || typeName [0] == '!') { + return null; // generic method/type parameter + } + + if (IsBclTypeName (typeName)) { + return "System.Private.CoreLib"; + } + + if (TryResolveManagedTypeDefinition (typeName, index, out _, out var resolvedIndex)) { + return resolvedIndex.AssemblyName; + } + + return index.AssemblyName; + } + + bool IsEnumManagedType (string managedType, AssemblyIndex index) + { + string typeName = StripManagedTypeDecorations (managedType); + if (typeName.Length == 0 || typeName [0] == '!') { + return false; + } + + if (!TryResolveManagedTypeDefinition (typeName, index, out var handle, out var resolvedIndex)) { + return false; + } + + var typeDef = resolvedIndex.Reader.GetTypeDefinition (handle); + if (typeDef.BaseType.Kind == HandleKind.TypeReference) { + var (baseTypeName, _) = ResolveTypeReference ((TypeReferenceHandle) typeDef.BaseType, resolvedIndex); + return baseTypeName == "System.Enum"; + } + if (typeDef.BaseType.Kind == HandleKind.TypeDefinition) { + var baseTypeDef = resolvedIndex.Reader.GetTypeDefinition ((TypeDefinitionHandle) typeDef.BaseType); + return AssemblyIndex.GetFullName (baseTypeDef, resolvedIndex.Reader) == "System.Enum"; } + return false; + } + + string? ResolveManagedTypeJniName (string managedType, AssemblyIndex index) + { + string typeName = StripManagedTypeDecorations (managedType); + if (typeName.Length == 0 || typeName [0] == '!') { + return null; + } + + if (!TryResolveManagedTypeDefinition (typeName, index, out var handle, out var resolvedIndex)) { + return null; + } + + if (resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo) && + !string.IsNullOrEmpty (regInfo.JniName)) { + return regInfo.JniName; + } + + return null; + } + + bool TryResolveManagedTypeDefinition (string managedTypeName, AssemblyIndex index, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex) + { + if (TryResolveType (managedTypeName, index.AssemblyName, out handle, out resolvedIndex)) { + return true; + } + + foreach (var candidate in assemblyCache.Values) { + if (candidate.TypesByFullName.TryGetValue (managedTypeName, out handle)) { + resolvedIndex = candidate; + return true; + } + } + + handle = default; + resolvedIndex = null!; + return false; + } + + static bool IsBclTypeName (string managedTypeName) + { + switch (managedTypeName) { + case "System.Void": + case "System.Boolean": + 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.String": + case "System.Object": + case "System.IntPtr": + case "System.UIntPtr": + return true; + default: + return false; + } + } + + static string StripManagedTypeDecorations (string managedType) + { + string result = managedType; + while (result.EndsWith ("[]", StringComparison.Ordinal) || + result.EndsWith ("&", StringComparison.Ordinal) || + result.EndsWith ("*", StringComparison.Ordinal)) { + result = result.Substring (0, result.Length - (result.EndsWith ("[]", StringComparison.Ordinal) ? 2 : 1)); + } + + int genericStart = result.IndexOf ('<'); + if (genericStart >= 0) { + result = result.Substring (0, genericStart); + } + + return result; } ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) @@ -872,47 +1004,6 @@ static List ParseJniParameters (string jniSignature) return result; } - static List BuildJavaConstructors (List marshalMethods) - { - var ctors = new List (); - int ctorIndex = 0; - foreach (var mm in marshalMethods) { - if (!mm.IsConstructor) { - continue; - } - ctors.Add (new JavaConstructorInfo { - JniSignature = mm.JniSignature, - ConstructorIndex = ctorIndex, - Parameters = mm.Parameters, - SuperArgumentsString = mm.SuperArgumentsString, - }); - ctorIndex++; - } - return ctors; - } - - static string ExtractNamespace (string fullName) - { - int lastDot = fullName.LastIndexOf ('.'); - return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; - } - - static string ExtractShortName (string fullName) - { - int lastDot = fullName.LastIndexOf ('.'); - return lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; - } - - static List ParseJniParameters (string jniSignature) - { - var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature); - var result = new List (typeStrings.Count); - foreach (var t in typeStrings) { - result.Add (new JniParameterInfo { JniType = t }); - } - return result; - } - static List BuildJavaConstructors (List marshalMethods) { var ctors = new List (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 8089072fffa..52e42e9db45 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -383,6 +383,29 @@ public void Generate_ExportMethod_HasWrapperMethods () } } + [Fact] + public void Generate_ExportMarshalComplex_UsesArrayMarshalHelpers () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMarshalComplex"); + var path = GenerateAssembly (new [] { peer }, "ExportMarshalComplexHelpers"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); + Assert.Contains ("GetArray", memberNames); + Assert.Contains ("NewArray", memberNames); + Assert.Contains ("CopyArray", memberNames); + Assert.Contains ("GetCharSequence", memberNames); + } + } finally { + CleanUp (path); + } + } + [Fact] public void Generate_ExportConstructor_HasWrapperMethods () { @@ -410,6 +433,34 @@ public void Generate_ExportConstructor_HasWrapperMethods () } } + [Fact] + public void Generate_ExportOnlyType_HasProxyAndRegistration () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.ManagedTypeName == "MyApp.UnregisteredExporter"); + var path = GenerateAssembly (new [] { peer }, "ExportOnlyType"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_UnregisteredExporter_Proxy"); + + var methods = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + Assert.Contains ("n_doExportedWork_uco_0", methods); + Assert.Contains ("RegisterNatives", methods); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + Assert.Contains (entries, e => e.jniMethodName == "n_DoExportedWork" && e.jniSignature == "()V"); + } + } finally { + CleanUp (path); + } + } + [Fact] public void Generate_ExportMethod_HasMethodBody () { @@ -619,6 +670,31 @@ public void Generate_StaticExportAndExportField_RegisteredInRegisterNatives () } } + [Fact] + public void Generate_ExportMarshalComplex_RegisteredInRegisterNatives () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.JavaName == "my/app/ExportMarshalComplex"); + var path = GenerateAssembly (new [] { peer }, "ExportMarshalComplexRegistration"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_ExportMarshalComplex_Proxy"); + + var entries = ReadRegisterNativesEntries (pe, reader, proxy); + Assert.Contains (entries, e => e.jniMethodName == "n_MutateInts" && e.jniSignature == "([I)V"); + Assert.Contains (entries, e => e.jniMethodName == "n_RoundTripEnum" && e.jniSignature == "(I)I"); + Assert.Contains (entries, e => e.jniMethodName == "n_EchoCharSequence" && e.jniSignature == "(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;"); + Assert.Contains (entries, e => e.jniMethodName == "n_EchoViews" && e.jniSignature == "([Landroid/view/View;)[Landroid/view/View;"); + Assert.Contains (entries, e => e.jniMethodName == "n_EchoStrings" && e.jniSignature == "([Ljava/lang/String;)[Ljava/lang/String;"); + } + } finally { + CleanUp (path); + } + } + [Fact] public void Generate_ExportRegistration_HasCorrectJniSignatures () { @@ -1071,4 +1147,4 @@ static void CleanUp (string path) try { Directory.Delete (dir, true); } catch { } } } -} \ No newline at end of file +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 3c1212fc4fa..4231f6af4ee 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -695,10 +695,14 @@ public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods () var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList (); Assert.NotEmpty (acwProxies); - // ACW proxies with [Register] marshal methods should have registrations. - // [Export]-only types don't generate UCO wrappers yet (TODO). + // ACW proxies should have native registrations for [Register] and [Export] members. var proxiesWithRegistrations = acwProxies.Where (p => p.NativeRegistrations.Count > 0).ToList (); Assert.NotEmpty (proxiesWithRegistrations); + + // [Export]-only unregistered type should still get a proxy + registration. + var exportOnlyProxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_UnregisteredExporter_Proxy"); + Assert.NotNull (exportOnlyProxy); + Assert.Contains (exportOnlyProxy!.NativeRegistrations, r => r.JniMethodName == "n_DoExportedWork"); } } @@ -1486,6 +1490,49 @@ public void Fixture_ExportMarshalMethod_HasCorrectManagedParameters () Assert.Equal ("System.String, System.Private.CoreLib", exportMethod.ManagedReturnType); } + [Fact] + public void Fixture_ExportMarshalComplex_HasArrayEnumAndCharSequenceMetadata () + { + var peer = FindFixtureByJavaName ("my/app/ExportMarshalComplex"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMarshalComplex_Proxy"); + Assert.NotNull (proxy); + + var roundTripEnum = proxy!.ExportMarshalMethods.First (e => e.ManagedMethodName == "RoundTripEnum"); + Assert.Equal ("(I)I", roundTripEnum.JniSignature); + Assert.Equal ("MyApp.ExportSampleEnum", roundTripEnum.ManagedParameters [0].ManagedTypeName); + Assert.Equal ("TestFixtures", roundTripEnum.ManagedParameters [0].AssemblyName); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.ManagedReturnType); + + var echoCharSequence = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoCharSequence"); + Assert.Equal ("(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;", echoCharSequence.JniSignature); + Assert.Equal ("Java.Lang.ICharSequence", echoCharSequence.ManagedParameters [0].ManagedTypeName); + + var mutateInts = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "MutateInts"); + Assert.Equal ("([I)V", mutateInts.JniSignature); + Assert.Equal ("System.Int32[]", mutateInts.ManagedParameters [0].ManagedTypeName); + + var echoViews = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoViews"); + Assert.Equal ("([Landroid/view/View;)[Landroid/view/View;", echoViews.JniSignature); + Assert.Equal ("Android.Views.View[]", echoViews.ManagedParameters [0].ManagedTypeName); + + var echoStrings = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoStrings"); + Assert.Equal ("([Ljava/lang/String;)[Ljava/lang/String;", echoStrings.JniSignature); + Assert.Equal ("System.String[]", echoStrings.ManagedParameters [0].ManagedTypeName); + } + + [Fact] + public void Fixture_UnregisteredExporter_ExportOnlyTypeGetsProxy () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.ManagedTypeName == "MyApp.UnregisteredExporter"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_UnregisteredExporter_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_DoExportedWork"); + } + } public class FixtureStaticExportAndExportField @@ -2067,4 +2114,4 @@ static void CleanUpDir (string path) if (dir != null && Directory.Exists (dir)) try { Directory.Delete (dir, true); } catch { } } -} \ No newline at end of file +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index df468190a5c..bb368215353 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -469,6 +469,34 @@ public void Scan_ExportMembersComprehensive_NameOverrideAndThrows () Assert.True (emptyThrows.ThrownNames == null || emptyThrows.ThrownNames.Count == 0); } + [Fact] + public void Scan_ExportMarshalComplex_HasArrayEnumAndCharSequenceSignatures () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMarshalComplex"); + + var mutateInts = peer.MarshalMethods.First (m => m.JniName == "mutateInts"); + Assert.Equal ("([I)V", mutateInts.JniSignature); + Assert.Equal ("System.Int32[], System.Private.CoreLib", mutateInts.Parameters [0].ManagedType); + + var roundTripEnum = peer.MarshalMethods.First (m => m.JniName == "roundTripEnum"); + Assert.Equal ("(I)I", roundTripEnum.JniSignature); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.Parameters [0].ManagedType); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.ManagedReturnType); + + var echoCharSequence = peer.MarshalMethods.First (m => m.JniName == "echoCharSequence"); + Assert.Equal ("(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;", echoCharSequence.JniSignature); + Assert.Equal ("Java.Lang.ICharSequence, TestFixtures", echoCharSequence.Parameters [0].ManagedType); + + var echoViews = peer.MarshalMethods.First (m => m.JniName == "echoViews"); + Assert.Equal ("([Landroid/view/View;)[Landroid/view/View;", echoViews.JniSignature); + Assert.Equal ("Android.Views.View[], TestFixtures", echoViews.Parameters [0].ManagedType); + + var echoStrings = peer.MarshalMethods.First (m => m.JniName == "echoStrings"); + Assert.Equal ("([Ljava/lang/String;)[Ljava/lang/String;", echoStrings.JniSignature); + Assert.Equal ("System.String[], System.Private.CoreLib", echoStrings.Parameters [0].ManagedType); + } + [Fact] public void Scan_ExportCtorWithSuperArgs_HasSuperArgumentsString () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index cedf6831f10..42962239d27 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -20,6 +20,11 @@ protected Object (IntPtr handle, JniHandleOwnership transfer) { } } + + [Register ("java/lang/CharSequence", DoNotGenerateAcw = true)] + public interface ICharSequence + { + } } namespace Java.Lang @@ -682,6 +687,12 @@ public class GlobalUnregisteredType : Java.Lang.Object // ================================================================ namespace MyApp { + public enum ExportSampleEnum + { + None, + One, + } + /// /// Type with [Export] constructors (no [Register] on ctors). /// Legacy JCW: TypeManager.Activate pattern, not nctor_N. @@ -741,6 +752,33 @@ public void DoWork (int count) { } public string ComputeName (string prefix, int index) { return ""; } } + /// + /// Complex [Export] marshal scenarios: arrays, enums, and CharSequence. + /// + [Register ("my/app/ExportMarshalComplex")] + public class ExportMarshalComplex : Java.Lang.Object + { + protected ExportMarshalComplex (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export ("mutateInts")] + public void MutateInts (int[] values) { } + + [Java.Interop.Export ("roundTripEnum")] + public ExportSampleEnum RoundTripEnum (ExportSampleEnum value) { return value; } + + [Java.Interop.Export ("echoCharSequence")] + public Java.Lang.ICharSequence EchoCharSequence (Java.Lang.ICharSequence value) { return value; } + + [Java.Interop.Export ("echoViews")] + public Android.Views.View[] EchoViews (Android.Views.View[] values) { return values; } + + [Java.Interop.Export ("echoStrings")] + public string[] EchoStrings (string[] values) { return values; } + } + /// /// Comprehensive [Export] member scenarios ported from legacy ExportsMembers. /// Tests: name override, throws, empty throws, static methods.