[TrimmableTypeMap] Emit direct RegisterNatives with UTF-8 RVA data#10988
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the generated ACW registration codepath to use a single JniEnvironment.Types.RegisterNatives call with stackalloc’d JniNativeMethod entries and deduplicated UTF-8 data stored in PE RVA field data, removing the older per-method TrimmableTypeMap.RegisterMethod() helper.
Changes:
- Emit
RegisterNativesonce per type usingJniNativeMethod+ReadOnlySpan<JniNativeMethod>and method pointers. - Add
PEAssemblyBuilder.GetOrAddUtf8Field()with mapped field data + caching to deduplicate UTF-8 literals. - Remove
TrimmableTypeMap.RegisterMethod()and add generator tests validating new emission behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs | Adds tests ensuring direct RegisterNatives usage, presence of <PrivateImplementationDetails>, and UTF-8 field dedup behavior. |
| src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs | Removes the obsolete per-method RegisterMethod helper. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs | Reworks emitted IL to build JniNativeMethod arrays via stackalloc and call RegisterNatives once. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs | Adds mapped field data support and deduped UTF-8 RVA field creation. |
| external/Java.Interop | Updates submodule to include JniNativeMethod and raw RegisterNatives overload. |
| // 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); | ||
| } |
There was a problem hiding this comment.
JniNativeMethod initialization IL is very likely invalid due to stack type mismatches: (1) ldsflda pushes a managed byref to the RVA field’s value-type, not a byte*; passing it to a byte* parameter usually requires an explicit conversion (e.g., conv.u) or otherwise producing an unmanaged pointer. (2) Calling a value-type instance .ctor with call expects a managed byref (&JniNativeMethod) as the this argument; here methods[i] is computed from localloc as a native integer pointer, which generally won’t satisfy the required & type. A more robust pattern is to construct the struct value with newobj and then store it into methods[i] with stobj (or otherwise follow the IL pattern produced by the C# compiler for stackalloc + element assignment). This should prevent BadImageFormatException / invalid IL at runtime.
| } | ||
|
|
||
| // JniObjectReference peerRef = jniType.PeerReference | ||
| encoder.LoadArgumentAddress (1); |
There was a problem hiding this comment.
RegisterNatives(JniType jniType) passes jniType by value/reference depending on whether JniType is a reference type (it commonly is). Using LoadArgumentAddress(1) will push an argument-slot address (&), which is not a valid receiver for an instance property getter on a reference type and can produce invalid IL at runtime. Use LoadArgument(1) (and consider callvirt if the target is a reference type and you want the normal null-check semantics).
| encoder.LoadArgumentAddress (1); | |
| encoder.LoadArgument (1); |
| var readOnlySpanRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeRef, | ||
| _pe.Metadata.GetOrAddString ("System"), _pe.Metadata.GetOrAddString ("ReadOnlySpan`1")); | ||
| localSig.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (readOnlySpanRef)); |
There was a problem hiding this comment.
ReadOnlySpan type refs are being re-added in multiple places (here, plus EmitTypeReferences() and EncodeReadOnlySpanOfJniNativeMethod()), which can create duplicate TypeRef rows and makes handle usage harder to reason about. Prefer caching a single TypeReferenceHandle (e.g., _readOnlySpanRef) and reusing it consistently in the member refs, local signature encoding, and parameter signature encoding.
|
|
||
| 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); |
There was a problem hiding this comment.
Field RVA data alignment can matter for PE/CLR readers. Writing raw bytes back-to-back and using the current _mappedFieldData.Count as the next RVA risks generating misaligned RVAs depending on platform/reader expectations. Consider aligning _mappedFieldData to at least 4 bytes (or the required alignment for mapped field data) before recording rva and writing each payload, padding with zeros as needed.
| /// The field lives on a shared <c><PrivateImplementationDetails></c> type. | ||
| /// </summary> |
There was a problem hiding this comment.
The comment claims the RVA field lives on <PrivateImplementationDetails>, but GetOrAddUtf8Field() calls GetOrCreateSizedType() (which adds a new nested TypeDefinition) immediately before AddFieldDefinition(). In ECMA-335 metadata, fields are associated to the most recently added TypeDefinition by table ordering, so these fields will typically end up belonging to the last created __utf8_{size} nested type, not <PrivateImplementationDetails>. Either update the documentation to match the actual metadata layout, or refactor emission so <PrivateImplementationDetails> remains the declaring type for these fields (e.g., avoid creating additional TypeDefinition rows between creating <PrivateImplementationDetails> and adding its fields, potentially by precomputing/creating sized types earlier or making sized types non-nested/top-level).
| /// The field lives on a shared <c><PrivateImplementationDetails></c> type. | |
| /// </summary> | |
| /// The field is declared on an internal sized helper type (typically nested under | |
| /// <c><PrivateImplementationDetails></c>) and is considered an implementation detail. |
| void EmitRegisterNatives (List<NativeRegistrationData> registrations, | ||
| void EmitRegisterNatives (TypeDefinitionHandle ownerType, List<NativeRegistrationData> registrations, | ||
| Dictionary<string, MethodDefinitionHandle> wrapperHandles) | ||
| { |
There was a problem hiding this comment.
ownerType is introduced but not used inside this method in the shown changes. If it’s not needed, remove it to avoid confusion; if it’s intended for future use (e.g., to scope UTF-8 fields or member refs), consider wiring it in now or adding a short comment explaining the planned usage.
| { | |
| { | |
| // ownerType is reserved for future use (e.g., to scope UTF-8 fields or member refs). | |
| _ = ownerType; |
70f9e84 to
3134fef
Compare
simonrozsival
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Verdict:
Found 3 issues (0 error, 2 warning, 1 suggestion):
⚠️ Performance:ReadOnlySpan\1type reference is re-created inencodeLocalsevery timeEmitRegisterNativesis called (TypeMapAssemblyEmitter.cs:857`)⚠️ Documentation: Doc comment example still shows packed-blob offset pattern but implementation uses per-string fields (TypeMapAssemblyEmitter.cs:52)- 💡 Code organization:
ownerTypeparameter is unused after dedup refactor (TypeMapAssemblyEmitter.cs:751)
👍 Excellent change overall. The RVA field approach eliminates all allocations during native method registration — no delegates, no strings, no arrays. String deduplication across proxy types is a nice touch. The <PrivateImplementationDetails> pattern mirrors what the C# compiler does. Good test coverage with 3 new tests.
Review generated by android-reviewer from review guidelines.
| localSig.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST | ||
| localSig.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE | ||
| var readOnlySpanRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeRef, | ||
| _pe.Metadata.GetOrAddString ("System"), _pe.Metadata.GetOrAddString ("ReadOnlySpan`1")); |
There was a problem hiding this comment.
🤖 _pe.Metadata.AddTypeReference for ReadOnlySpan\1is called inside theencodeLocalscallback, which runs every timeEmitRegisterNatives` is invoked (once per ACW proxy type). This creates a duplicate type reference row in the metadata table for each proxy.
Consider moving this to EmitTypeReferences() where readOnlySpanRef is already created for the TypeSpec, and reuse the cached handle here.
Rule: Cache repeated accessor calls / Avoid unnecessary allocations
| /// 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_natives[0], &__utf8_natives[11], &n_OnCreate_uco_0); |
There was a problem hiding this comment.
🤖 __utf8_natives[0], __utf8_natives[11]) but the implementation now uses per-string deduplicated fields with ldsflda. Should be updated to reflect the actual generated code, e.g.:
/// methods[0] = new JniNativeMethod(&__utf8_0, &__utf8_1, &n_OnCreate_uco_0);
/// methods[1] = new JniNativeMethod(&__utf8_2, &__utf8_1, &nctor_0_uco);Rule: Remove stale comments (Postmortem #59)
| } | ||
|
|
||
| void EmitRegisterNatives (List<NativeRegistrationData> registrations, | ||
| void EmitRegisterNatives (TypeDefinitionHandle ownerType, List<NativeRegistrationData> registrations, |
There was a problem hiding this comment.
🤖 💡 Code organization — The ownerType parameter is no longer used after the refactor to deduplicated fields on <PrivateImplementationDetails>. Consider removing it from the signature and the callsite at line 415.
Rule: Remove unused code (Postmortem #58)
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<JniNativeMethod>. 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>
3134fef to
6b6631f
Compare
Summary
Replace per-method
TrimmableTypeMap.RegisterMethod()calls with a single JNIRegisterNativescall usingstackalloc'dJniNativeMethodstructs and compile-time UTF-8 byte data stored in RVA static fields.What changed
PEAssemblyBuilder: AddGetOrAddUtf8Field()for deduplicated static fields backed by raw byte data in the PE mapped field data section. Sized value types and string content are both cached —"()V"appearing in 100 proxy types is stored once.TypeMapAssemblyEmitter: Add type/member refs forJniNativeMethod,JniEnvironment.Types.RegisterNatives,JniType.PeerReference, andReadOnlySpan<JniNativeMethod>. RewriteEmitRegisterNativestoldsfldadeduplicated RVA fields for UTF-8 name/signature pointers,stackalloc JniNativeMethod[N], and callRegisterNativesonce.TrimmableTypeMap: RemoveRegisterMethod(no longer called by generated code).external/Java.Interop: Update submodule toc14ba04(includes Add RegisterNatives overload for raw function pointers java-interop#1391JniNativeMethod+ rawRegisterNativesoverload).Result
Zero delegate allocations, zero string allocations, zero array allocations, one JNI call per class during native registration.
Dependencies