Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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>&lt;PrivateImplementationDetails&gt;</c> type.
/// </summary>
Comment on lines +189 to +190
Copy link

Copilot AI Mar 21, 2026

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>, 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).

Suggested change
/// The field lives on a shared <c>&lt;PrivateImplementationDetails&gt;</c> type.
/// </summary>
/// The field is declared on an internal sized helper type (typically nested under
/// <c>&lt;PrivateImplementationDetails&gt;</c>) and is considered an implementation detail.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

_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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", &amp;n_OnCreate_uco_0);
/// TrimmableTypeMap.RegisterMethod(jniType, "nctor_0", "()V", &amp;nctor_0_uco);
/// JniNativeMethod* methods = stackalloc JniNativeMethod[2];
/// methods[0] = new JniNativeMethod(&amp;__utf8_0, &amp;__utf8_1, &amp;n_OnCreate_uco_0);
/// methods[1] = new JniNativeMethod(&amp;__utf8_2, &amp;__utf8_3, &amp;nctor_0_uco);
/// JniEnvironment.Types.RegisterNatives(jniType.PeerReference, new ReadOnlySpan&lt;JniNativeMethod&gt;(methods, 2));
/// }
/// }
///
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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 ()
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -703,25 +752,112 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco)
void EmitRegisterNatives (List<NativeRegistrationData> registrations,
Dictionary<string, MethodDefinitionHandle> wrapperHandles)
{
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{
{
// ownerType is reserved for future use (e.g., to scope UTF-8 fields or member refs).
_ = ownerType;

Copilot uses AI. Check for mistakes.
// 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,
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;
// 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);
}
Comment on lines +791 to 823
Copy link

Copilot AI Mar 21, 2026

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 uses AI. Check for mistakes.

// JniObjectReference peerRef = jniType.PeerReference
encoder.LoadArgumentAddress (1);
Copy link

Copilot AI Mar 21, 2026

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).

Suggested change
encoder.LoadArgumentAddress (1);
encoder.LoadArgument (1);

Copilot uses AI. Check for mistakes.
encoder.Call (_jniTypePeerReferenceRef);
encoder.StoreLocal (1);

// new ReadOnlySpan<JniNativeMethod>(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<JniNativeMethod>
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));
});
}

Expand Down Expand Up @@ -754,4 +890,35 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc)
});
_pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob);
}

/// <summary>
/// Builds a <c>TypeSpec</c> for a closed generic type with a single value-type argument.
/// E.g., <c>ReadOnlySpan&lt;JniNativeMethod&gt;</c>.
/// </summary>
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));
}

/// <summary>
/// Encodes <c>ReadOnlySpan&lt;JniNativeMethod&gt;</c> directly into a signature type encoder.
/// Required because <see cref="SignatureTypeEncoder.Type"/> doesn't accept TypeSpec handles.
/// </summary>
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));
}
}
Loading