From 6b6631f0c73c1edc24b200fcff564af703a43dfb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 12:37:51 +0100 Subject: [PATCH] Emit direct RegisterNatives with UTF-8 RVA data Replace per-method TrimmableTypeMap.RegisterMethod() calls with a single JNI RegisterNatives call using stackalloc'd JniNativeMethod structs and compile-time UTF-8 byte data stored in RVA static fields. Changes: - PEAssemblyBuilder: Add AddRvaField() for static fields backed by raw byte data in the PE mapped field data section. Pass mappedFieldData to ManagedPEBuilder. - TypeMapAssemblyEmitter: Add type/member refs for JniNativeMethod, JniEnvironment.Types.RegisterNatives, JniType.PeerReference, and ReadOnlySpan. Rewrite EmitRegisterNatives to pack all method names/signatures into one UTF-8 RVA blob per proxy type, stackalloc JniNativeMethod[N], and call RegisterNatives once. - TrimmableTypeMap: Remove RegisterMethod (no longer called by generated code). Result: zero delegate allocations, zero string allocations, zero array allocations, one JNI call per class during native registration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Generator/PEAssemblyBuilder.cs | 102 ++++++++- .../Generator/TypeMapAssemblyEmitter.cs | 199 ++++++++++++++++-- .../TrimmableTypeMap.cs | 24 --- .../TypeMapAssemblyGeneratorTests.cs | 80 +++++++ 5 files changed, 365 insertions(+), 42 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index fb0952fad89..c14ba04c7fa 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a +Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index b862cc2b29f..560fe363ae5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -28,6 +28,20 @@ sealed class PEAssemblyBuilder readonly BlobBuilder _codeBlob = new BlobBuilder (256); readonly BlobBuilder _attrBlob = new BlobBuilder (64); + // Holds raw byte data for fields with FieldAttributes.HasFieldRVA (e.g., UTF-8 string literals). + // Passed to ManagedPEBuilder as the mappedFieldData section. + readonly BlobBuilder _mappedFieldData = new BlobBuilder (); + + // Cache of sized value types for RVA fields, keyed by byte length. + // Avoids creating duplicate __utf8_N types when multiple fields share the same size. + readonly Dictionary _sizedTypeCache = new (); + + // Deduplication cache for UTF-8 string RVA fields. Strings like "()V" that repeat across + // many proxy types are stored once and shared via the same FieldDefinitionHandle. + readonly Dictionary _utf8FieldCache = new (StringComparer.Ordinal); + TypeDefinitionHandle _privateImplDetailsType; + int _utf8FieldCounter; + readonly Version _systemRuntimeVersion; public MetadataBuilder Metadata { get; } = new MetadataBuilder (); @@ -102,7 +116,8 @@ public void WritePE (Stream stream) var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), new MetadataRootBuilder (Metadata), - ILBuilder); + ILBuilder, + mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); peBlob.WriteContentTo (stream); @@ -167,6 +182,91 @@ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string manage return Metadata.AddTypeReference (scope, Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name)); } + /// + /// Returns a deduplicated RVA field containing the null-terminated UTF-8 encoding of + /// . Strings like "()V" that appear across many proxy + /// types are stored once and share the same . + /// The field lives on a shared <PrivateImplementationDetails> type. + /// + public FieldDefinitionHandle GetOrAddUtf8Field (string value) + { + if (_utf8FieldCache.TryGetValue (value, out var existing)) { + return existing; + } + + EnsurePrivateImplDetailsType (); + + // Encode to null-terminated UTF-8 (all JNI names/signatures are ASCII). + int byteCount = System.Text.Encoding.UTF8.GetByteCount (value); + var bytes = new byte [byteCount + 1]; + System.Text.Encoding.UTF8.GetBytes (value, 0, value.Length, bytes, 0); + // bytes[byteCount] is already 0 (null terminator) + + var sizedType = GetOrCreateSizedType (bytes.Length); + + _sigBlob.Clear (); + new BlobEncoder (_sigBlob).FieldSignature ().Type (sizedType, true); + + int rva = _mappedFieldData.Count; + _mappedFieldData.WriteBytes (bytes); + + var fieldHandle = Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA | FieldAttributes.InitOnly, + Metadata.GetOrAddString ($"__utf8_{_utf8FieldCounter++}"), + Metadata.GetOrAddBlob (_sigBlob)); + + Metadata.AddFieldRelativeVirtualAddress (fieldHandle, rva); + + _utf8FieldCache [value] = fieldHandle; + return fieldHandle; + } + + void EnsurePrivateImplDetailsType () + { + if (!_privateImplDetailsType.IsNil) { + return; + } + + int typeFieldStart = Metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; + + _privateImplDetailsType = Metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.BeforeFieldInit, + default, + Metadata.GetOrAddString (""), + Metadata.AddTypeReference (SystemRuntimeRef, + Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("Object")), + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + } + + TypeDefinitionHandle GetOrCreateSizedType (int size) + { + if (_sizedTypeCache.TryGetValue (size, out var existing)) { + return existing; + } + + EnsurePrivateImplDetailsType (); + + int typeFieldStart = Metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var handle = Metadata.AddTypeDefinition ( + TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + default, + Metadata.GetOrAddString ($"__utf8_{size}"), + Metadata.AddTypeReference (SystemRuntimeRef, + Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("ValueType")), + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + Metadata.AddTypeLayout (handle, packingSize: 1, size: (uint) size); + Metadata.AddNestedType (handle, _privateImplDetailsType); + + _sizedTypeCache [size] = handle; + return handle; + } + /// /// Emits a method body and definition in one call. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 976bebf88a7..629976571d7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -48,8 +48,10 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) /// { -/// TrimmableTypeMap.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); -/// TrimmableTypeMap.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); +/// JniNativeMethod* methods = stackalloc JniNativeMethod[2]; +/// methods[0] = new JniNativeMethod(&__utf8_0, &__utf8_1, &n_OnCreate_uco_0); +/// methods[1] = new JniNativeMethod(&__utf8_2, &__utf8_3, &nctor_0_uco); +/// JniEnvironment.Types.RegisterNatives(jniType.PeerReference, new ReadOnlySpan<JniNativeMethod>(methods, 2)); /// } /// } /// @@ -85,13 +87,23 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _activateInstanceRef; - MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; + // RegisterNatives with JniNativeMethod + TypeReferenceHandle _jniNativeMethodRef; + TypeReferenceHandle _jniEnvironmentRef; + TypeReferenceHandle _jniEnvironmentTypesRef; + TypeReferenceHandle _readOnlySpanOpenRef; + TypeSpecificationHandle _readOnlySpanOfJniNativeMethodSpec; + MemberReferenceHandle _jniNativeMethodCtorRef; + MemberReferenceHandle _jniTypePeerReferenceRef; + MemberReferenceHandle _jniEnvTypesRegisterNativesRef; + MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + /// /// Creates a new emitter. /// @@ -193,6 +205,18 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + + _jniNativeMethodRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniNativeMethod")); + _jniEnvironmentRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniEnvironment")); + _jniEnvironmentTypesRef = metadata.AddTypeReference (_jniEnvironmentRef, + default, metadata.GetOrAddString ("Types")); + + // ReadOnlySpan — TypeSpec for generic instantiation + _readOnlySpanOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("ReadOnlySpan`1")); + _readOnlySpanOfJniNativeMethodSpec = MakeGenericTypeSpec_ValueType (_readOnlySpanOpenRef, _jniNativeMethodRef); } void EmitMemberReferences () @@ -236,16 +260,41 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - _registerMethodRef = _pe.AddMemberRef (_trimmableTypeMapRef, "RegisterMethod", - sig => sig.MethodSignature ().Parameters (4, + // JniNativeMethod..ctor(byte*, byte*, IntPtr) + _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, rt => rt.Void (), p => { - p.AddParameter ().Type ().Type (_jniTypeRef, false); - p.AddParameter ().Type ().String (); - p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Pointer ().Byte (); + p.AddParameter ().Type ().Pointer ().Byte (); p.AddParameter ().Type ().IntPtr (); })); + // JniType.get_PeerReference() -> JniObjectReference + _jniTypePeerReferenceRef = _pe.AddMemberRef (_jniTypeRef, "get_PeerReference", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().Type (_jniObjectReferenceRef, true), + p => { })); + + // JniEnvironment.Types.RegisterNatives(JniObjectReference, ReadOnlySpan) + _jniEnvTypesRegisterNativesRef = _pe.AddMemberRef (_jniEnvironmentTypesRef, "RegisterNatives", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_jniObjectReferenceRef, true); + // ReadOnlySpan — must encode as GENERICINST manually + EncodeReadOnlySpanOfJniNativeMethod (p.AddParameter ().Type ()); + })); + + // ReadOnlySpan..ctor(void*, int) + _readOnlySpanOfJniNativeMethodCtorRef = _pe.AddMemberRef (_readOnlySpanOfJniNativeMethodSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().VoidPointer (); + p.AddParameter ().Type ().Int32 (); + })); + var ucoAttrTypeRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, _pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), _pe.Metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute")); @@ -703,6 +752,35 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco) void EmitRegisterNatives (List registrations, Dictionary wrapperHandles) { + // Filter to only registrations that have corresponding wrapper methods + var validRegs = new List<(NativeRegistrationData Reg, MethodDefinitionHandle Wrapper)> (); + foreach (var reg in registrations) { + if (wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { + validRegs.Add ((reg, wrapperHandle)); + } + } + + if (validRegs.Count == 0) { + _pe.EmitBody ("RegisterNatives", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.NewSlot | MethodAttributes.Final, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniTypeRef, false)), + encoder => encoder.OpCode (ILOpCode.Ret)); + return; + } + + // Get or create deduplicated RVA fields for each unique name/signature string. + var nameFields = new FieldDefinitionHandle [validRegs.Count]; + var sigFields = new FieldDefinitionHandle [validRegs.Count]; + for (int i = 0; i < validRegs.Count; i++) { + nameFields [i] = _pe.GetOrAddUtf8Field (validRegs [i].Reg.JniMethodName); + sigFields [i] = _pe.GetOrAddUtf8Field (validRegs [i].Reg.JniSignature); + } + + int methodCount = validRegs.Count; + _pe.EmitBody ("RegisterNatives", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final, @@ -710,18 +788,76 @@ void EmitRegisterNatives (List registrations, 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; + // stackalloc JniNativeMethod[N] + encoder.LoadConstantI4 (methodCount); + encoder.OpCode (ILOpCode.Sizeof); + encoder.Token (_jniNativeMethodRef); + encoder.OpCode (ILOpCode.Mul); + encoder.OpCode (ILOpCode.Localloc); + encoder.StoreLocal (0); + + for (int i = 0; i < methodCount; i++) { + // &methods[i] + encoder.LoadLocal (0); + if (i > 0) { + encoder.LoadConstantI4 (i); + encoder.OpCode (ILOpCode.Sizeof); + encoder.Token (_jniNativeMethodRef); + encoder.OpCode (ILOpCode.Mul); + encoder.OpCode (ILOpCode.Add); } - encoder.LoadArgument (1); - encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniMethodName)); - encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniSignature)); + + // byte* name — ldsflda of deduplicated field + encoder.OpCode (ILOpCode.Ldsflda); + encoder.Token (nameFields [i]); + + // byte* signature + encoder.OpCode (ILOpCode.Ldsflda); + encoder.Token (sigFields [i]); + + // IntPtr functionPointer encoder.OpCode (ILOpCode.Ldftn); - encoder.Token (wrapperHandle); - encoder.Call (_registerMethodRef); + encoder.Token (validRegs [i].Wrapper); + + encoder.Call (_jniNativeMethodCtorRef); } + + // JniObjectReference peerRef = jniType.PeerReference + encoder.LoadArgumentAddress (1); + encoder.Call (_jniTypePeerReferenceRef); + encoder.StoreLocal (1); + + // new ReadOnlySpan(methods, count) + encoder.LoadLocalAddress (2); + encoder.LoadLocal (0); + encoder.LoadConstantI4 (methodCount); + encoder.Call (_readOnlySpanOfJniNativeMethodCtorRef); + + // JniEnvironment.Types.RegisterNatives(peerRef, span) + encoder.LoadLocal (1); + encoder.LoadLocal (2); + encoder.Call (_jniEnvTypesRegisterNativesRef); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: localSig => { + localSig.WriteByte (0x07); // IMAGE_CEE_CS_CALLCONV_LOCAL_SIG + localSig.WriteCompressedInteger (3); + + // local 0: native int (stackalloc pointer) + localSig.WriteByte (0x18); // ELEMENT_TYPE_I + + // local 1: JniObjectReference + localSig.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + localSig.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + + // local 2: ReadOnlySpan + localSig.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + localSig.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + localSig.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_readOnlySpanOpenRef)); + localSig.WriteCompressedInteger (1); + localSig.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + localSig.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniNativeMethodRef)); }); } @@ -754,4 +890,35 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } + + /// + /// Builds a TypeSpec for a closed generic type with a single value-type argument. + /// E.g., ReadOnlySpan<JniNativeMethod>. + /// + TypeSpecificationHandle MakeGenericTypeSpec_ValueType (EntityHandle openType, EntityHandle valueTypeArg) + { + var sigBlob = new BlobBuilder (32); + sigBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + sigBlob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE (ReadOnlySpan is a struct) + sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType)); + sigBlob.WriteCompressedInteger (1); // generic arity = 1 + sigBlob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (valueTypeArg)); + return _pe.Metadata.AddTypeSpecification (_pe.Metadata.GetOrAddBlob (sigBlob)); + } + + /// + /// Encodes ReadOnlySpan<JniNativeMethod> directly into a signature type encoder. + /// Required because doesn't accept TypeSpec handles. + /// + void EncodeReadOnlySpanOfJniNativeMethod (SignatureTypeEncoder encoder) + { + var builder = encoder.Builder; + builder.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + builder.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + builder.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_readOnlySpanOpenRef)); + builder.WriteCompressedInteger (1); // arity = 1 + builder.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + builder.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniNativeMethodRef)); + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index e598cc38820..7bb3a405dcf 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; using Android.Runtime; using Java.Interop; @@ -162,29 +161,6 @@ internal static void ActivateInstance (IntPtr self, Type targetType) } } - // TODO (https://github.com/dotnet/android/issues/10794): The generator currently emits per-method RegisterMethod() calls. - // This should be changed to emit a single JNI RegisterNatives call with - // all methods at once, eliminating this helper. Follow-up generator change. - - /// - /// Registers a single JNI native method. Called from generated - /// implementations. - /// - public static void RegisterMethod (JniType nativeClass, string name, string signature, IntPtr functionPointer) - { - // The java-interop JniNativeMethodRegistration API requires a Delegate, but we have - // a raw function pointer from an [UnmanagedCallersOnly] method. JNI only uses the - // function pointer extracted via Marshal.GetFunctionPointerForDelegate(), so the - // delegate type doesn't matter — Action is used as a lightweight wrapper. - // TODO (https://github.com/dotnet/java-interop/pull/1391): Use JniNativeMethod overload to avoid delegate allocation. - var registration = new JniNativeMethodRegistration (name, signature, - Marshal.GetDelegateForFunctionPointer (functionPointer)); - JniEnvironment.Types.RegisterNatives ( - nativeClass.PeerReference, - new [] { registration }, - 1); - } - static readonly RegisterNativesHandler s_onRegisterNatives = OnRegisterNatives; delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index cb917e0fdde..d892b0161da 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -503,4 +503,84 @@ public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList () { Assert.Empty (JniSignatureHelper.ParseParameterTypes ("(")); } + + [Fact] + public void Generate_AcwProxy_UsesJniNativeMethodDirectly () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity"); + + using var stream = GenerateAssembly (new [] { acwPeer }, "DirectRegisterTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var memberNames = GetMemberRefNames (reader); + var typeNames = GetTypeRefNames (reader); + + // Should reference JniNativeMethod and RegisterNatives directly + Assert.Contains ("JniNativeMethod", typeNames); + Assert.Contains ("Types", typeNames); // JniEnvironment.Types nested type + Assert.Contains ("RegisterNatives", memberNames); + Assert.Contains ("get_PeerReference", memberNames); + + // Should NOT reference the old RegisterMethod helper + Assert.DoesNotContain ("RegisterMethod", memberNames); + } + + [Fact] + public void Generate_AcwProxy_HasPrivateImplementationDetails () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity"); + + using var stream = GenerateAssembly (new [] { acwPeer }, "PrivImplTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var typeDefNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + + Assert.Contains ("", typeDefNames); + } + + [Fact] + public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () + { + var peers = ScanFixtures (); + // Get all ACW peers — they likely share signatures like "()V" + var acwPeers = peers.Where (p => !p.DoNotGenerateAcw && p.MarshalMethods.Count > 0).ToList (); + Assert.True (acwPeers.Count >= 2, "Need at least 2 ACW peers to test deduplication"); + + using var stream = GenerateAssembly (acwPeers, "DedupTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Count fields with HasFieldRVA — these are our UTF-8 RVA fields. + // With deduplication, common strings like "()V" should appear only once. + var rvaFields = reader.FieldDefinitions + .Select (h => reader.GetFieldDefinition (h)) + .Where (f => (f.Attributes & FieldAttributes.HasFieldRVA) != 0) + .ToList (); + + // Collect all JNI method names and signatures from the ACW peers + var allStrings = acwPeers + .SelectMany (p => p.MarshalMethods) + .SelectMany (m => new [] { m.JniName, m.JniSignature }) + .ToList (); + var uniqueStrings = allStrings.Distinct ().Count (); + + // With dedup, RVA field count should equal unique string count, not total string count. + // Also include constructor registrations (nctor_*), so use <= for a safe assertion. + Assert.True (rvaFields.Count <= uniqueStrings + acwPeers.Count * 2, + $"Expected at most {uniqueStrings + acwPeers.Count * 2} RVA fields (unique strings + ctor names/sigs), " + + $"but found {rvaFields.Count}. Deduplication may not be working."); + + // The key assertion: fewer RVA fields than total strings means dedup is working + if (allStrings.Count > uniqueStrings) { + Assert.True (rvaFields.Count < allStrings.Count, + $"Expected fewer RVA fields ({rvaFields.Count}) than total strings ({allStrings.Count}) due to deduplication"); + } + } }