-
Notifications
You must be signed in to change notification settings - Fork 566
[TrimmableTypeMap] Emit direct RegisterNatives with UTF-8 RVA data #10988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/simonrozsival/trimmable-typemap-runtime-pr
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<int, TypeDefinitionHandle> _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<string, FieldDefinitionHandle> _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)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a deduplicated RVA field containing the null-terminated UTF-8 encoding of | ||
| /// <paramref name="value"/>. Strings like <c>"()V"</c> that appear across many proxy | ||
| /// types are stored once and share the same <see cref="FieldDefinitionHandle"/>. | ||
| /// The field lives on a shared <c><PrivateImplementationDetails></c> type. | ||
| /// </summary> | ||
| 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); | ||
|
Comment on lines
+209
to
+218
|
||
|
|
||
| _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 ("<PrivateImplementationDetails>"), | ||
| 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; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits a method body and definition in one call. | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||
|
|
||||||||||
| /// <summary> | ||||||||||
| /// Creates a new emitter. | ||||||||||
| /// </summary> | ||||||||||
|
|
@@ -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<JniNativeMethod> — 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<JniNativeMethod>) | ||||||||||
| _jniEnvTypesRegisterNativesRef = _pe.AddMemberRef (_jniEnvironmentTypesRef, "RegisterNatives", | ||||||||||
| sig => sig.MethodSignature ().Parameters (2, | ||||||||||
| rt => rt.Void (), | ||||||||||
| p => { | ||||||||||
| p.AddParameter ().Type ().Type (_jniObjectReferenceRef, true); | ||||||||||
| // ReadOnlySpan<JniNativeMethod> — must encode as GENERICINST manually | ||||||||||
| EncodeReadOnlySpanOfJniNativeMethod (p.AddParameter ().Type ()); | ||||||||||
| })); | ||||||||||
|
|
||||||||||
| // ReadOnlySpan<JniNativeMethod>..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,25 +752,112 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco) | |||||||||
| void EmitRegisterNatives (List<NativeRegistrationData> registrations, | ||||||||||
| Dictionary<string, MethodDefinitionHandle> wrapperHandles) | ||||||||||
| { | ||||||||||
|
||||||||||
| { | |
| { | |
| // ownerType is reserved for future use (e.g., to scope UTF-8 fields or member refs). | |
| _ = ownerType; |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment claims the RVA field lives on
<PrivateImplementationDetails>, butGetOrAddUtf8Field()callsGetOrCreateSizedType()(which adds a new nestedTypeDefinition) immediately beforeAddFieldDefinition(). In ECMA-335 metadata, fields are associated to the most recently addedTypeDefinitionby 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 additionalTypeDefinitionrows between creating<PrivateImplementationDetails>and adding its fields, potentially by precomputing/creating sized types earlier or making sized types non-nested/top-level).