diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs new file mode 100644 index 00000000000..088a7d8f845 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// A read-only, seekable over the chunks a already +/// holds, so a serialised PE image can be hashed and copied to disk without being duplicated into +/// a second contiguous buffer. +/// +/// +/// Only the chunk arrays are retained — the itself and the metadata +/// graph that produced it stay collectible — so the live byte count matches what a +/// copy would have held, without the transient second copy. +/// +sealed class BlobBuilderStream : Stream +{ + readonly ArraySegment [] segments; + readonly long [] segmentStarts; + readonly long length; + long position; + int cursor; + + public BlobBuilderStream (BlobBuilder builder) + { + _ = builder ?? throw new ArgumentNullException (nameof (builder)); + + var collected = new List> (); + foreach (var blob in builder.GetBlobs ()) { + var bytes = blob.GetBytes (); + if (bytes.Count == 0 || bytes.Array is null) { + continue; + } + collected.Add (bytes); + } + + segments = collected.ToArray (); + segmentStarts = new long [segments.Length + 1]; + long total = 0; + for (int i = 0; i < segments.Length; i++) { + segmentStarts [i] = total; + total += segments [i].Count; + } + segmentStarts [segments.Length] = total; + length = total; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => length; + + public override long Position { + get => position; + set { + if (value < 0) { + throw new ArgumentOutOfRangeException (nameof (value)); + } + position = value; + } + } + + public override void Flush () + { + } + + public override int Read (byte [] buffer, int offset, int count) + { + if (buffer is null) { + throw new ArgumentNullException (nameof (buffer)); + } + if (offset < 0) { + throw new ArgumentOutOfRangeException (nameof (offset)); + } + if (count < 0) { + throw new ArgumentOutOfRangeException (nameof (count)); + } + if (buffer.Length - offset < count) { + throw new ArgumentException ("The buffer is too small for the requested range."); + } + + int copied = 0; + while (count > 0 && position < length) { + int index = FindSegment (position); + var segment = segments [index]; + int within = (int) (position - segmentStarts [index]); + int available = segment.Count - within; + int toCopy = Math.Min (available, count); + if (segment.Array is null) { + break; + } + Buffer.BlockCopy (segment.Array, segment.Offset + within, buffer, offset, toCopy); + position += toCopy; + offset += toCopy; + count -= toCopy; + copied += toCopy; + } + return copied; + } + + public override long Seek (long offset, SeekOrigin origin) + { + long target = origin switch { + SeekOrigin.Begin => offset, + SeekOrigin.Current => position + offset, + SeekOrigin.End => length + offset, + _ => throw new ArgumentOutOfRangeException (nameof (origin)), + }; + if (target < 0) { + throw new IOException ("Cannot seek before the beginning of the stream."); + } + position = target; + return position; + } + + public override void SetLength (long value) => throw new NotSupportedException (); + + public override void Write (byte [] buffer, int offset, int count) => throw new NotSupportedException (); + + int FindSegment (long offset) + { + // Reads are overwhelmingly sequential, so try the last used chunk first. + if (cursor < segments.Length && offset >= segmentStarts [cursor] && offset < segmentStarts [cursor + 1]) { + return cursor; + } + + int low = 0; + int high = segments.Length - 1; + while (low <= high) { + int middle = low + ((high - low) / 2); + if (offset < segmentStarts [middle]) { + high = middle - 1; + } else if (offset >= segmentStarts [middle + 1]) { + low = middle + 1; + } else { + cursor = middle; + return middle; + } + } + throw new ArgumentOutOfRangeException (nameof (offset)); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs new file mode 100644 index 00000000000..ff5621db3e7 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs @@ -0,0 +1,183 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Streams model fields into one or two SHA-256 hashes without materialising the whole +/// serialised model in memory. +/// +/// +/// +/// The byte stream produced for each sink is identical to what +/// would have written into a : +/// strings are UTF-8 encoded with a 7-bit encoded byte-length prefix, is one +/// byte, and is four bytes little-endian. Keeping the encoding identical means +/// the resulting fingerprints — and therefore the deterministic MVIDs derived from them — are +/// unchanged. +/// +/// +/// Two sinks are supported so the content fingerprint (which seeds the MVID) and the +/// incremental-build fingerprint can be produced from a single walk over the model. Fields shared +/// by both fingerprints are UTF-8 encoded once and appended to both sinks; fields belonging to +/// only one fingerprint are appended to that sink alone. +/// +/// +sealed class FingerprintWriter : IDisposable +{ + /// Selects which fingerprint(s) a write applies to. + [Flags] + public enum Sink + { + Content = 1, + Incremental = 2, + Both = Content | Incremental, + } + + // Large enough to absorb the small field writes that dominate the model walk without + // paying per-field hash update costs, small enough to stay off the large object heap. + const int BufferSize = 8 * 1024; + + readonly IncrementalHash contentHash; + readonly IncrementalHash? incrementalHash; + readonly byte [] contentBuffer; + readonly byte []? incrementalBuffer; + byte [] scratch = new byte [512]; + int contentPosition; + int incrementalPosition; + + public FingerprintWriter (bool includeIncremental) + { + contentHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + contentBuffer = new byte [BufferSize]; + if (includeIncremental) { + incrementalHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + incrementalBuffer = new byte [BufferSize]; + } + } + + public void WriteString (Sink sink, string value) + { + int byteCount = Encoding.UTF8.GetByteCount (value); + EnsureScratch (byteCount + 5); + int offset = Write7BitEncodedInt (scratch, 0, byteCount); + Encoding.UTF8.GetBytes (value, 0, value.Length, scratch, offset); + Write (sink, scratch, 0, offset + byteCount); + } + + public void WriteOptionalString (Sink sink, string? value) + { + WriteBoolean (sink, value is not null); + if (value is not null) { + WriteString (sink, value); + } + } + + public void WriteBoolean (Sink sink, bool value) => WriteByte (sink, value ? (byte) 1 : (byte) 0); + + public void WriteByte (Sink sink, byte value) + { + scratch [0] = value; + Write (sink, scratch, 0, 1); + } + + public void WriteInt32 (Sink sink, int value) + { + scratch [0] = (byte) value; + scratch [1] = (byte) (value >> 8); + scratch [2] = (byte) (value >> 16); + scratch [3] = (byte) (value >> 24); + Write (sink, scratch, 0, 4); + } + + public void WriteRaw (Sink sink, byte [] value) => Write (sink, value, 0, value.Length); + + public byte [] GetContentFingerprint () + { + FlushContent (); + return contentHash.GetHashAndReset (); + } + + public byte [] GetIncrementalFingerprint () + { + if (incrementalHash is null) { + throw new InvalidOperationException ("The incremental fingerprint was not requested."); + } + FlushIncremental (); + return incrementalHash.GetHashAndReset (); + } + + public void Dispose () + { + contentHash.Dispose (); + incrementalHash?.Dispose (); + } + + void EnsureScratch (int required) + { + if (scratch.Length < required) { + scratch = new byte [Math.Max (required, scratch.Length * 2)]; + } + } + + void Write (Sink sink, byte [] data, int offset, int count) + { + if ((sink & Sink.Content) != 0) { + if (count > contentBuffer.Length - contentPosition) { + FlushContent (); + } + if (count > contentBuffer.Length) { + contentHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, contentBuffer, contentPosition, count); + contentPosition += count; + } + } + if ((sink & Sink.Incremental) == 0) { + return; + } + if (incrementalHash is null || incrementalBuffer is null) { + // Silently dropping the write would produce a fingerprint that looks valid but + // covers less than it claims to, so fail fast on the caller's mistake instead. + throw new InvalidOperationException ( + $"Cannot write to {nameof (Sink.Incremental)} because the incremental fingerprint was not requested."); + } + if (count > incrementalBuffer.Length - incrementalPosition) { + FlushIncremental (); + } + if (count > incrementalBuffer.Length) { + incrementalHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); + incrementalPosition += count; + } + } + + void FlushContent () + { + if (contentPosition > 0) { + contentHash.AppendData (contentBuffer, 0, contentPosition); + contentPosition = 0; + } + } + + void FlushIncremental () + { + if (incrementalPosition > 0 && incrementalHash is not null && incrementalBuffer is not null) { + incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); + incrementalPosition = 0; + } + } + + static int Write7BitEncodedInt (byte [] destination, int offset, int value) + { + uint remaining = (uint) value; + while (remaining > 0x7Fu) { + destination [offset++] = (byte) (remaining | ~0x7Fu); + remaining >>= 7; + } + destination [offset++] = (byte) remaining; + return offset; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 54633913dfe..1cc2c2b7e4a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,11 +1,22 @@ using System; using System.Collections.Generic; -using System.IO; using System.Security.Cryptography; using System.Text; +using Sink = Microsoft.Android.Sdk.TrimmableTypeMap.FingerprintWriter.Sink; namespace Microsoft.Android.Sdk.TrimmableTypeMap; +/// +/// Fingerprints computed from a model in a single walk. +/// +/// +/// Content fingerprint that seeds the deterministic MVID of the emitted assembly. +/// +/// +/// Incremental-build fingerprint, or when it was not requested. +/// +readonly record struct ModelFingerprints (byte [] Content, byte []? Incremental); + static class MetadataHelper { static readonly Guid GeneratorModuleVersionId = typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; @@ -28,116 +39,119 @@ public static Guid DeterministicMvid (string moduleName, ReadOnlySpan cont } /// - /// Computes a content fingerprint for the given . + /// Computes the content fingerprint — and optionally the incremental-build fingerprint — for + /// in a single walk over the model. /// - public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) + /// + /// The content fingerprint covers only the model data that changes the emitted assembly's + /// contents. The incremental fingerprint is an incremental-build contract, so it additionally + /// covers the generator binary identity, the emitter configuration, and every model field the + /// emitter consumes. Both are serialised from the same walk: fields shared by the two + /// fingerprints are UTF-8 encoded once and appended to both hashes. + /// + public static ModelFingerprints ComputeFingerprints ( + TypeMapAssemblyData data, + Version systemRuntimeVersion, + bool useSharedTypemapUniverse, + bool includeIncremental) { - using var sha = SHA256.Create (); - using var stream = new MemoryStream (); - using var writer = new BinaryWriter (stream, Encoding.UTF8); + using var writer = new FingerprintWriter (includeIncremental); + var incremental = Sink.Incremental; + var both = includeIncremental ? Sink.Both : Sink.Content; + + if (includeIncremental) { + writer.WriteRaw (incremental, GeneratorModuleVersionId.ToByteArray ()); + writer.WriteString (incremental, systemRuntimeVersion.ToString ()); + writer.WriteBoolean (incremental, useSharedTypemapUniverse); + writer.WriteString (incremental, data.AssemblyName); + writer.WriteString (incremental, data.ModuleName); + writer.WriteInt32 (incremental, data.Entries.Count); + } + foreach (var entry in data.Entries) { - writer.Write (entry.MapKey); - writer.Write (entry.ProxyTypeReference); - writer.Write (entry.TargetTypeReference ?? ""); + writer.WriteString (both, entry.MapKey); + writer.WriteString (both, entry.ProxyTypeReference); + writer.WriteString (Sink.Content, entry.TargetTypeReference ?? ""); + if (includeIncremental) { + writer.WriteOptionalString (incremental, entry.TargetTypeReference); + } + } + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.ProxyTypes.Count); } foreach (var proxy in data.ProxyTypes) { - writer.Write (proxy.TypeName); - writer.WriteTypeRef (proxy.TargetType); - writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); - if (proxy.ActivationCtor is not null) { - writer.WriteTypeRef (proxy.ActivationCtor.DeclaringType); + writer.WriteString (both, proxy.TypeName); + if (includeIncremental) { + writer.WriteString (incremental, proxy.JniName); + writer.WriteString (incremental, proxy.Namespace); } - writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); - writer.Write (proxy.UcoMethods.Count); - foreach (var method in proxy.UcoMethods) { - writer.WriteUcoMethod (method); + WriteTypeRef (writer, both, proxy.TargetType); + if (includeIncremental) { + WriteOptionalTypeRef (writer, incremental, proxy.InvokerType); } - writer.Write (proxy.UcoConstructors.Count); - foreach (var constructor in proxy.UcoConstructors) { - writer.WriteUcoConstructor (constructor); + writer.WriteByte (Sink.Content, (byte) (proxy.ActivationCtor?.Style ?? 0)); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.WriteByte (incremental, (byte) proxy.InvokerActivationCtorStyle.Value); + } + writer.WriteBoolean (incremental, proxy.ActivationCtor is not null); } - writer.Write (proxy.NativeRegistrations.Count); - foreach (var registration in proxy.NativeRegistrations) { - writer.WriteNativeRegistration (registration); + if (proxy.ActivationCtor is not null) { + WriteTypeRef (writer, both, proxy.ActivationCtor.DeclaringType); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.ActivationCtor.IsOnLeafType); + writer.WriteByte (incremental, (byte) proxy.ActivationCtor.Style); + } } - } - foreach (var assoc in data.Associations) { - writer.Write (assoc.SourceTypeReference); - writer.Write (assoc.AliasProxyTypeReference); - } - writer.Flush (); - return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); - } - - /// - /// Computes a fingerprint of every input that affects a generated per-assembly typemap. - /// Unlike , this is an incremental-build contract, - /// so it includes the generator binary identity and all model fields consumed by the emitter. - /// - public static byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData data, Version systemRuntimeVersion, bool useSharedTypemapUniverse) - { - using var sha = SHA256.Create (); - using var stream = new MemoryStream (); - using var writer = new BinaryWriter (stream, Encoding.UTF8); - writer.Write (GeneratorModuleVersionId.ToByteArray ()); - writer.Write (systemRuntimeVersion.ToString ()); - writer.Write (useSharedTypemapUniverse); - writer.Write (data.AssemblyName); - writer.Write (data.ModuleName); - writer.Write (data.Entries.Count); - foreach (var entry in data.Entries) { - writer.Write (entry.MapKey); - writer.Write (entry.ProxyTypeReference); - writer.WriteOptionalString (entry.TargetTypeReference); - } - writer.Write (data.ProxyTypes.Count); - foreach (var proxy in data.ProxyTypes) { - writer.Write (proxy.TypeName); - writer.Write (proxy.JniName); - writer.Write (proxy.Namespace); - writer.WriteTypeRef (proxy.TargetType); - writer.WriteOptionalTypeRef (proxy.InvokerType); - writer.Write (proxy.InvokerActivationCtorStyle.HasValue); - if (proxy.InvokerActivationCtorStyle.HasValue) { - writer.Write ((byte) proxy.InvokerActivationCtorStyle.Value); + writer.WriteByte (Sink.Content, (byte) (proxy.InvokerActivationCtorStyle ?? 0)); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.IsGenericDefinition); + writer.WriteBoolean (incremental, proxy.CannotRegisterInStaticConstructor); + writer.WriteBoolean (incremental, proxy.IsAcw); } - writer.WriteOptionalActivationCtor (proxy.ActivationCtor); - writer.Write (proxy.IsGenericDefinition); - writer.Write (proxy.CannotRegisterInStaticConstructor); - writer.Write (proxy.IsAcw); - writer.Write (proxy.UcoMethods.Count); + writer.WriteInt32 (both, proxy.UcoMethods.Count); foreach (var method in proxy.UcoMethods) { - writer.WriteUcoMethod (method); + WriteUcoMethod (writer, both, method); } - writer.Write (proxy.UcoConstructors.Count); + writer.WriteInt32 (both, proxy.UcoConstructors.Count); foreach (var constructor in proxy.UcoConstructors) { - writer.WriteUcoConstructor (constructor); + WriteUcoConstructor (writer, both, constructor); } - writer.Write (proxy.NativeRegistrations.Count); + writer.WriteInt32 (both, proxy.NativeRegistrations.Count); foreach (var registration in proxy.NativeRegistrations) { - writer.WriteNativeRegistration (registration); + WriteNativeRegistration (writer, both, registration); } } - writer.Write (data.Associations.Count); + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.Associations.Count); + } foreach (var assoc in data.Associations) { - writer.Write (assoc.SourceTypeReference); - writer.Write (assoc.AliasProxyTypeReference); + writer.WriteString (both, assoc.SourceTypeReference); + writer.WriteString (both, assoc.AliasProxyTypeReference); } - writer.Write (data.AliasHolders.Count); - foreach (var holder in data.AliasHolders) { - writer.Write (holder.TypeName); - writer.Write (holder.Namespace); - writer.Write (holder.AliasKeys.Count); - foreach (var aliasKey in holder.AliasKeys) { - writer.Write (aliasKey); + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.AliasHolders.Count); + foreach (var holder in data.AliasHolders) { + writer.WriteString (incremental, holder.TypeName); + writer.WriteString (incremental, holder.Namespace); + writer.WriteInt32 (incremental, holder.AliasKeys.Count); + foreach (var aliasKey in holder.AliasKeys) { + writer.WriteString (incremental, aliasKey); + } + } + writer.WriteInt32 (incremental, data.IgnoresAccessChecksTo.Count); + foreach (var assemblyName in data.IgnoresAccessChecksTo) { + writer.WriteString (incremental, assemblyName); } } - writer.Write (data.IgnoresAccessChecksTo.Count); - foreach (var assemblyName in data.IgnoresAccessChecksTo) { - writer.Write (assemblyName); - } - writer.Flush (); - return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + + return new ModelFingerprints ( + writer.GetContentFingerprint (), + includeIncremental ? writer.GetIncrementalFingerprint () : null); } /// @@ -148,121 +162,100 @@ public static byte [] ComputeRootIncrementalFingerprint ( Version systemRuntimeVersion, bool useSharedTypemapUniverse) { - using var sha = SHA256.Create (); - using var stream = new MemoryStream (); - using var writer = new BinaryWriter (stream, Encoding.UTF8); - writer.Write (GeneratorModuleVersionId.ToByteArray ()); - writer.Write (systemRuntimeVersion.ToString ()); - writer.Write (useSharedTypemapUniverse); - writer.Write (perAssemblyTypeMapNames.Count); + using var writer = new FingerprintWriter (includeIncremental: false); + writer.WriteRaw (Sink.Content, GeneratorModuleVersionId.ToByteArray ()); + writer.WriteString (Sink.Content, systemRuntimeVersion.ToString ()); + writer.WriteBoolean (Sink.Content, useSharedTypemapUniverse); + writer.WriteInt32 (Sink.Content, perAssemblyTypeMapNames.Count); foreach (var assemblyName in perAssemblyTypeMapNames) { - writer.Write (assemblyName); + writer.WriteString (Sink.Content, assemblyName); } - writer.Flush (); - return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + return writer.GetContentFingerprint (); } - static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) + static void WriteTypeRef (FingerprintWriter writer, Sink sink, TypeRefData type) { - writer.Write (type.ManagedTypeName); - writer.Write (type.AssemblyName); - writer.Write (type.IsValueType ? (byte) 1 : (byte) 0); - writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); - writer.Write (type.GenericArguments.Count); + writer.WriteString (sink, type.ManagedTypeName); + writer.WriteString (sink, type.AssemblyName); + writer.WriteByte (sink, type.IsValueType ? (byte) 1 : (byte) 0); + writer.WriteByte (sink, type.IsEnum ? (byte) 1 : (byte) 0); + writer.WriteInt32 (sink, type.GenericArguments.Count); foreach (var argument in type.GenericArguments) { - writer.WriteTypeRef (argument); + WriteTypeRef (writer, sink, argument); } } - static void WriteOptionalTypeRef (this BinaryWriter writer, TypeRefData? type) + static void WriteOptionalTypeRef (FingerprintWriter writer, Sink sink, TypeRefData? type) { - writer.Write (type is not null); + writer.WriteBoolean (sink, type is not null); if (type is not null) { - writer.WriteTypeRef (type); - } - } - - static void WriteOptionalString (this BinaryWriter writer, string? value) - { - writer.Write (value is not null); - if (value is not null) { - writer.Write (value); - } - } - - static void WriteOptionalActivationCtor (this BinaryWriter writer, ActivationCtorData? constructor) - { - writer.Write (constructor is not null); - if (constructor is not null) { - writer.WriteTypeRef (constructor.DeclaringType); - writer.Write (constructor.IsOnLeafType); - writer.Write ((byte) constructor.Style); + WriteTypeRef (writer, sink, type); } } - static void WriteUcoMethod (this BinaryWriter writer, UcoMethodData method) + static void WriteUcoMethod (FingerprintWriter writer, Sink sink, UcoMethodData method) { - writer.Write (method.WrapperName); - writer.Write (method.CallbackMethodName); - writer.WriteTypeRef (method.CallbackType); - writer.Write (method.JniSignature); - writer.WriteOptionalStrings (method.CallbackParameterTypeNames); - writer.WriteOptionalString (method.CallbackReturnTypeName); - writer.WriteExportMethodDispatch (method.ExportMethodDispatch); + writer.WriteString (sink, method.WrapperName); + writer.WriteString (sink, method.CallbackMethodName); + WriteTypeRef (writer, sink, method.CallbackType); + writer.WriteString (sink, method.JniSignature); + WriteOptionalStrings (writer, sink, method.CallbackParameterTypeNames); + writer.WriteOptionalString (sink, method.CallbackReturnTypeName); + WriteExportMethodDispatch (writer, sink, method.ExportMethodDispatch); } - static void WriteOptionalStrings (this BinaryWriter writer, IReadOnlyList? values) + static void WriteOptionalStrings (FingerprintWriter writer, Sink sink, IReadOnlyList? values) { - writer.Write (values is not null); + writer.WriteBoolean (sink, values is not null); if (values is null) { return; } - writer.Write (values.Count); + writer.WriteInt32 (sink, values.Count); foreach (var value in values) { - writer.Write (value); + writer.WriteString (sink, value); } } - static void WriteExportMethodDispatch (this BinaryWriter writer, ExportMethodDispatchData? dispatch) + static void WriteExportMethodDispatch (FingerprintWriter writer, Sink sink, ExportMethodDispatchData? dispatch) { - writer.Write (dispatch is not null); + writer.WriteBoolean (sink, dispatch is not null); if (dispatch is null) { return; } - writer.Write (dispatch.ManagedMethodName); - writer.Write (dispatch.ParameterTypes.Count); + writer.WriteString (sink, dispatch.ManagedMethodName); + writer.WriteInt32 (sink, dispatch.ParameterTypes.Count); foreach (var parameterType in dispatch.ParameterTypes) { - writer.WriteTypeRef (parameterType); + WriteTypeRef (writer, sink, parameterType); } - writer.Write (dispatch.ParameterKinds.Count); + writer.WriteInt32 (sink, dispatch.ParameterKinds.Count); foreach (var parameterKind in dispatch.ParameterKinds) { - writer.Write ((int) parameterKind); + writer.WriteInt32 (sink, (int) parameterKind); } - writer.WriteTypeRef (dispatch.ReturnType); - writer.Write ((int) dispatch.ReturnKind); - writer.Write (dispatch.IsStatic); + WriteTypeRef (writer, sink, dispatch.ReturnType); + writer.WriteInt32 (sink, (int) dispatch.ReturnKind); + writer.WriteBoolean (sink, dispatch.IsStatic); } - static void WriteUcoConstructor (this BinaryWriter writer, UcoConstructorData constructor) + static void WriteUcoConstructor (FingerprintWriter writer, Sink sink, UcoConstructorData constructor) { - writer.Write (constructor.WrapperName); - writer.WriteTypeRef (constructor.TargetType); - writer.Write (constructor.JniSignature); - writer.Write (constructor.HasMatchingManagedCtor); - writer.Write (constructor.ManagedParameterTypes.Count); + writer.WriteString (sink, constructor.WrapperName); + WriteTypeRef (writer, sink, constructor.TargetType); + writer.WriteString (sink, constructor.JniSignature); + writer.WriteBoolean (sink, constructor.HasMatchingManagedCtor); + writer.WriteInt32 (sink, constructor.ManagedParameterTypes.Count); foreach (var parameterType in constructor.ManagedParameterTypes) { - writer.WriteTypeRef (parameterType); + WriteTypeRef (writer, sink, parameterType); } } - static void WriteNativeRegistration (this BinaryWriter writer, NativeRegistrationData registration) + static void WriteNativeRegistration (FingerprintWriter writer, Sink sink, NativeRegistrationData registration) { - writer.Write (registration.JniMethodName); - writer.Write (registration.JniSignature); - writer.Write (registration.WrapperMethodName); - writer.Write (registration.WrapperTarget.TypeNamespace); - writer.Write (registration.WrapperTarget.TypeName); - writer.Write (registration.WrapperTarget.MethodName); + writer.WriteString (sink, registration.JniMethodName); + writer.WriteString (sink, registration.JniSignature); + writer.WriteString (sink, registration.WrapperMethodName); + writer.WriteString (sink, registration.WrapperTarget.TypeNamespace); + writer.WriteString (sink, registration.WrapperTarget.TypeName); + writer.WriteString (sink, registration.WrapperTarget.MethodName); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 6434c515e27..1149154817a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -104,6 +104,22 @@ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan. /// public void WritePE (Stream stream) + { + var peBlob = SerializePE (); + if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) { + memoryStream.Capacity = peBlob.Count; + } + peBlob.WriteContentTo (stream); + } + + /// + /// Serialises the metadata + IL into a PE DLL and returns a read-only stream over the + /// serialised bytes. Unlike the image is not copied into a + /// second contiguous buffer. + /// + public Stream CreatePEStream () => new BlobBuilderStream (SerializePE ()); + + BlobBuilder SerializePE () { var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), @@ -114,10 +130,7 @@ public void WritePE (Stream stream) deterministicIdProvider: DeterministicContentId); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); - if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) { - memoryStream.Capacity = peBlob.Count; - } - peBlob.WriteContentTo (stream); + return peBlob; } static BlobContentId DeterministicContentId (IEnumerable content) @@ -159,10 +172,16 @@ public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName) /// Adds a member reference using the reusable signature blob builder. /// public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig) + => AddMemberRef (parent, name, GetOrAddSignature (encodeSig)); + + public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, BlobHandle signature) + => Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), signature); + + public BlobHandle GetOrAddSignature (Action encodeSig) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); - return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob)); + return Metadata.GetOrAddBlob (_sigBlob); } /// @@ -382,6 +401,15 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false); + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL) + => EmitBody (name, attrs, signature, emitIL, encodeLocals: null, useBranches: false); + + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL, + Action? encodeLocals) + => EmitBody (name, attrs, signature, emitIL, encodeLocals, useBranches: false); + /// /// Emits a method body and definition with optional local variable declarations. /// @@ -404,11 +432,15 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, Action? encodeLocals, bool useBranches) { - _sigBlob.Clear (); - encodeSig (new BlobEncoder (_sigBlob)); // Capture the sig blob handle before emitIL, because emitIL callbacks // may call AddMemberRef which clears and repopulates _sigBlob. - var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); + return EmitBody (name, attrs, GetOrAddSignature (encodeSig), emitIL, encodeLocals, useBranches); + } + + MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL, + Action? encodeLocals, bool useBranches) + { StandaloneSignatureHandle localSigHandle = default; if (encodeLocals != null) { @@ -433,7 +465,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), - sigBlobHandle, + signature, bodyOffset, MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 56a160a7914..d441c0e4026 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -137,6 +137,9 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + BlobHandle _activationCtorSignature; + BlobHandle _createInstanceSignature; + EntityHandle _anchorTypeHandle; ExportMethodDispatchEmitter? _exportMethodDispatchEmitter; @@ -165,6 +168,14 @@ public TypeMapAssemblyEmitter (Version systemRuntimeVersion) /// share a single typemap universe. When false, emits a per-assembly __TypeMapAnchor. /// public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse = false) + => Emit (model, stream, useSharedTypemapUniverse, contentFingerprint: null); + + /// + /// Pre-computed content fingerprint seeding the deterministic MVID. When + /// it is computed here; callers that already walked the model should pass it in to avoid a + /// second walk. + /// + internal void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse, byte []? contentFingerprint) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -173,13 +184,31 @@ public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypema throw new ArgumentNullException (nameof (stream)); } - EmitCore (model, useSharedTypemapUniverse); + EmitCore (model, useSharedTypemapUniverse, contentFingerprint); _pe.WritePE (stream); } - void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + /// + /// Emits a PE assembly from the given model and returns a read-only stream over the serialised + /// image, avoiding the copy into a second buffer that + /// performs. + /// + internal Stream EmitToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint) { - _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + + EmitCore (model, useSharedTypemapUniverse, contentFingerprint); + return _pe.CreatePEStream (); + } + + void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint) + { + contentFingerprint ??= MetadataHelper + .ComputeFingerprints (model, _systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental: false) + .Content; + _pe.EmitPreamble (model.AssemblyName, model.ModuleName, contentFingerprint); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); @@ -950,12 +979,7 @@ void EmitCreateInstanceBody (Action emitIL) { _pe.EmitBody ("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); - }), + GetCreateInstanceSignature (), emitIL); } @@ -963,25 +987,42 @@ void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action< { _pe.EmitBody ("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); - }), + GetCreateInstanceSignature (), emitIL, encodeLocals); } MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) { - return _pe.AddMemberRef (declaringTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - })); + return _pe.AddMemberRef (declaringTypeRef, ".ctor", GetActivationCtorSignature ()); + } + + BlobHandle GetActivationCtorSignature () + { + if (_activationCtorSignature.IsNil) { + _activationCtorSignature = _pe.GetOrAddSignature ( + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + } + return _activationCtorSignature; + } + + BlobHandle GetCreateInstanceSignature () + { + if (_createInstanceSignature.IsNil) { + _createInstanceSignature = _pe.GetOrAddSignature ( + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + } + return _createInstanceSignature; } MemberReferenceHandle AddManagedCtorRef (EntityHandle declaringTypeRef, IReadOnlyList parameterTypes) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 9e5ae23dd25..00db5979451 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -38,15 +38,31 @@ internal TypeMapAssemblyData CreateModel (IReadOnlyList peers, str return ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); } - internal byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + /// + /// Computes the content fingerprint — and, when is + /// , the incremental-build fingerprint — in a single walk over the model. + /// The content fingerprint should be passed back to + /// so the model is not walked twice. + /// + internal ModelFingerprints ComputeFingerprints (TypeMapAssemblyData model, bool useSharedTypemapUniverse, bool includeIncremental) + { + return MetadataHelper.ComputeFingerprints (model, _systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental); + } + + internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse, byte []? contentFingerprint = null) { - return MetadataHelper.ComputeIncrementalFingerprint (model, _systemRuntimeVersion, useSharedTypemapUniverse); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + emitter.Emit (model, stream, useSharedTypemapUniverse, contentFingerprint); } - internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse) + /// + /// Generates the PE assembly and returns a read-only stream over the serialised image without + /// copying it into a second buffer. + /// + internal Stream GenerateToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint = null) { var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, stream, useSharedTypemapUniverse); + return emitter.EmitToStream (model, useSharedTypemapUniverse, contentFingerprint); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 60c98619437..c77f11dbf4c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -325,15 +325,17 @@ internal List GenerateTypeMapAssemblies ( string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var model = generator.CreateModel (peers, typeMapAssemblyName); + // Both fingerprints come out of a single walk over the model: the incremental one + // gates emission, the content one seeds the emitted assembly's deterministic MVID. + var fingerprints = generator.ComputeFingerprints (model, useSharedTypemapUniverse, + includeIncremental: shouldGenerateTypeMapAssembly is not null); if (shouldGenerateTypeMapAssembly is not null) { - var fingerprint = generator.ComputeIncrementalFingerprint (model, useSharedTypemapUniverse); + var fingerprint = fingerprints.Incremental ?? throw new InvalidOperationException ("Incremental fingerprint was requested but not produced."); if (!shouldGenerateTypeMapAssembly (typeMapAssemblyName, fingerprint)) { continue; } } - var stream = new MemoryStream (); - generator.Generate (model, stream, useSharedTypemapUniverse); - stream.Position = 0; + var stream = generator.GenerateToStream (model, useSharedTypemapUniverse, fingerprints.Content); generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index d2a3c1e7122..e0d8e0aa4f5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -20,7 +20,21 @@ public record TrimmableTypeMapResult ( ApplicationRegistrationTypes ?? []; } -public record GeneratedAssembly (string Name, MemoryStream Content); +/// +/// A generated typemap assembly. is a read-only, seekable stream +/// positioned at the start of the serialised PE image; callers own it and should dispose it. +/// +/// +/// is deliberately typed as rather than +/// : the emitter hands back a view over the buffers the PE serialiser +/// already produced, so the image is never copied into a second contiguous buffer. Consumers +/// should treat it as a forward-reading stream — hash it, rewind, and copy it — rather than +/// reaching for members such as ToArray or GetBuffer. +/// This assembly is build-time SDK infrastructure rather than a library third parties compile +/// against, and the in-tree consumer (GenerateTrimmableTypeMap) only ever streams the +/// content to disk, so the narrowed member surface is an intentional trade for the removed copy. +/// +public record GeneratedAssembly (string Name, Stream Content); public record GeneratedJavaSource (string RelativePath, string Content); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs new file mode 100644 index 00000000000..d9b60994bcf --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -0,0 +1,184 @@ +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.Sdk.TrimmableTypeMap.Tests; + +/// +/// Generated typemap assemblies are surfaced as a read-only stream over the chunks the PE +/// serialiser already produced instead of being copied into a . +/// These tests pin the stream semantics the build task relies on: seek to the start, hash the +/// whole image, then copy it to disk. +/// +public class BlobBuilderStreamTests : FixtureTestBase +{ + static BlobBuilder MultiChunkBuilder (byte [] content, int chunkSize) + { + var builder = new BlobBuilder (chunkSize); + for (int offset = 0; offset < content.Length; offset += chunkSize) { + int count = Math.Min (chunkSize, content.Length - offset); + builder.WriteBytes (content, offset, count); + if (offset + count < content.Length) { + // Force a new chunk so the stream has to walk several segments. + builder.LinkSuffix (MultiChunkBuilderTail (content, offset + count, chunkSize)); + return builder; + } + } + return builder; + } + + static BlobBuilder MultiChunkBuilderTail (byte [] content, int start, int chunkSize) + { + var tail = new BlobBuilder (chunkSize); + tail.WriteBytes (content, start, content.Length - start); + return tail; + } + + static byte [] Sequence (int length) + { + var bytes = new byte [length]; + for (int i = 0; i < length; i++) { + bytes [i] = (byte) (i % 251); + } + return bytes; + } + + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (63)] + [InlineData (64)] + [InlineData (1000)] + public void ReadsBackTheExactContent (int length) + { + var content = Sequence (length); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 16)); + + Assert.Equal (length, stream.Length); + Assert.Equal (0, stream.Position); + Assert.True (stream.CanRead); + Assert.True (stream.CanSeek); + Assert.False (stream.CanWrite); + + using var copy = new MemoryStream (); + stream.CopyTo (copy); + Assert.Equal (content, copy.ToArray ()); + Assert.Equal (length, stream.Position); + Assert.Equal (0, stream.Read (new byte [1], 0, 1)); + } + + [Fact] + public void SupportsRepeatedSequentialAndRandomAccess () + { + var content = Sequence (5000); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 37)); + + // The build task hashes the stream and then rewinds it to copy it to disk. + using var first = new MemoryStream (); + stream.CopyTo (first); + stream.Position = 0; + using var second = new MemoryStream (); + stream.CopyTo (second); + Assert.Equal (first.ToArray (), second.ToArray ()); + + Assert.Equal (4000, stream.Seek (4000, SeekOrigin.Begin)); + var buffer = new byte [10]; + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (4000).Take (10), buffer); + + Assert.Equal (10, stream.Seek (-4000, SeekOrigin.Current)); + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (10).Take (10), buffer); + + Assert.Equal (4990, stream.Seek (-10, SeekOrigin.End)); + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (4990).Take (10), buffer); + + // Backwards seeks must invalidate the sequential-read cursor. + stream.Position = 0; + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Take (10), buffer); + } + + [Fact] + public void PartialReadsSpanChunkBoundaries () + { + var content = Sequence (300); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 7)); + using var copy = new MemoryStream (); + + var buffer = new byte [11]; + int read; + while ((read = stream.Read (buffer, 0, buffer.Length)) > 0) { + copy.Write (buffer, 0, read); + } + Assert.Equal (content, copy.ToArray ()); + } + + [Fact] + public void RejectsWrites () + { + using var stream = new BlobBuilderStream (MultiChunkBuilder (Sequence (16), 8)); + Assert.Throws (() => stream.Write (new byte [1], 0, 1)); + Assert.Throws (() => stream.SetLength (0)); + } + + [Fact] + public void ValidatesReadArguments () + { + using var stream = new BlobBuilderStream (MultiChunkBuilder (Sequence (32), 8)); + var buffer = new byte [16]; + + Assert.Throws (() => stream.Read (buffer, -1, 1)); + Assert.Throws (() => stream.Read (buffer, 0, -1)); + // Range overflows the buffer: ArgumentException, matching Stream.Read conventions. + Assert.Throws (() => stream.Read (buffer, 8, 16)); + } + + [Fact] + public void GeneratedAssemblyStreamIsAReadableImage () + { + var generator = new TrimmableTypeMapGenerator (new CollectingLogger ()); + using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var reader = peReader.GetMetadataReader (); + var result = generator.Execute ( + [new AssemblyInput (reader.GetString (reader.GetAssemblyDefinition ().Name), TestFixtureAssemblyPath, peReader)], + new Version (11, 0), + new HashSet ()); + + Assert.NotEmpty (result.GeneratedAssemblies); + foreach (var assembly in result.GeneratedAssemblies) { + Assert.Equal (0, assembly.Content.Position); + Assert.True (assembly.Content.Length > 0); + using var generatedReader = new PEReader (assembly.Content); + var metadata = generatedReader.GetMetadataReader (); + Assert.Equal (assembly.Name, metadata.GetString (metadata.GetAssemblyDefinition ().Name)); + } + } + + sealed class CollectingLogger : ITrimmableTypeMapLogger + { + public void LogNoJavaPeerTypesFound () { } + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } + public void LogDeferredRegistrationTypesInfo (int typeCount) { } + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } + public void LogGeneratedJcwFilesInfo (int sourceCount) { } + public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } + public void LogLibraryManifestMergeWarning (string message) { } + public void LogInvalidManifestPlaceholderWarning (string placeholders) { } + public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, + string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) { } + public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } + public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } + public void LogCustomJavaObjectError (string managedTypeName) { } + public void LogCustomJavaObjectWarning (string managedTypeName) { } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs new file mode 100644 index 00000000000..79d63bcd8e1 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs @@ -0,0 +1,412 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Xunit; +using Sink = Microsoft.Android.Sdk.TrimmableTypeMap.FingerprintWriter.Sink; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +/// +/// The fingerprints are computed by streaming model fields straight into SHA-256 instead of +/// buffering a serialisation in a . These +/// tests pin that rewrite to the previous byte stream: the content fingerprint seeds the emitted +/// assembly's deterministic MVID and the incremental fingerprint is an incremental-build contract, +/// so neither may drift. +/// +public class FingerprintWriterTests : FixtureTestBase +{ + [Theory] + [InlineData ("")] + [InlineData ("a")] + [InlineData ("Java.Lang.Object")] + [InlineData ("caf\u00e9 \u00fcber \u65e5\u672c\u8a9e")] + public void WriteString_MatchesBinaryWriter (string value) + { + AssertSameBytes (writer => writer.Write (value), writer => writer.WriteString (Sink.Content, value)); + } + + [Fact] + public void WriteString_LongerThanInternalBuffers_MatchesBinaryWriter () + { + // Exercises both the scratch-buffer growth and the direct-append path for values that + // cannot fit into the sink buffer. + foreach (int length in new [] { 100, 511, 512, 513, 8191, 8192, 8193, 40000 }) { + var value = new string ('x', length); + AssertSameBytes (writer => writer.Write (value), writer => writer.WriteString (Sink.Content, value)); + } + } + + [Fact] + public void WritePrimitives_MatchBinaryWriter () + { + AssertSameBytes (writer => writer.Write (true), writer => writer.WriteBoolean (Sink.Content, true)); + AssertSameBytes (writer => writer.Write (false), writer => writer.WriteBoolean (Sink.Content, false)); + AssertSameBytes (writer => writer.Write ((byte) 0xAB), writer => writer.WriteByte (Sink.Content, 0xAB)); + AssertSameBytes (writer => writer.Write (int.MinValue), writer => writer.WriteInt32 (Sink.Content, int.MinValue)); + AssertSameBytes (writer => writer.Write (0x12345678), writer => writer.WriteInt32 (Sink.Content, 0x12345678)); + AssertSameBytes (writer => writer.Write (false), writer => writer.WriteOptionalString (Sink.Content, null)); + AssertSameBytes ( + writer => { writer.Write (true); writer.Write ("value"); }, + writer => writer.WriteOptionalString (Sink.Content, "value")); + AssertSameBytes ( + writer => writer.Write (GeneratorModuleVersionId.ToByteArray ()), + writer => writer.WriteRaw (Sink.Content, GeneratorModuleVersionId.ToByteArray ())); + } + + [Fact] + public void SinksAreIndependent () + { + using var writer = new FingerprintWriter (includeIncremental: true); + writer.WriteString (Sink.Both, "shared"); + writer.WriteString (Sink.Incremental, "incremental-only"); + writer.WriteInt32 (Sink.Content, 7); + + Assert.Equal (Sha256 (BinaryWriterBytes (w => { w.Write ("shared"); w.Write (7); })), writer.GetContentFingerprint ()); + Assert.Equal (Sha256 (BinaryWriterBytes (w => { w.Write ("shared"); w.Write ("incremental-only"); })), writer.GetIncrementalFingerprint ()); + } + + [Fact] + public void GetIncrementalFingerprint_WhenNotRequested_Throws () + { + using var writer = new FingerprintWriter (includeIncremental: false); + Assert.Throws (() => writer.GetIncrementalFingerprint ()); + } + + [Fact] + public void WritingToIncrementalSink_WhenNotRequested_Throws () + { + // Silently dropping the write would yield a fingerprint that looks valid but covers + // less than it claims to, so the writer must fail fast instead. + foreach (var sink in new [] { Sink.Incremental, Sink.Both }) { + using var writer = new FingerprintWriter (includeIncremental: false); + Assert.Throws (() => writer.WriteString (sink, "value")); + Assert.Throws (() => writer.WriteBoolean (sink, true)); + Assert.Throws (() => writer.WriteInt32 (sink, 1)); + Assert.Throws (() => writer.WriteRaw (sink, [1, 2, 3])); + } + } + + [Fact] + public void IncrementalSink_FlushesBufferedDataSpanningMultipleFlushes () + { + // Exercises the incremental sink's buffer-flush path across the 8 KB boundary, both for + // values that fit in the buffer and for one larger than it. + using var writer = new FingerprintWriter (includeIncremental: true); + var chunk = new string ('y', 3000); + var oversized = new string ('z', 20000); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, oversized); + writer.WriteString (Sink.Incremental, chunk); + + var expected = Sha256 (BinaryWriterBytes (w => { + w.Write (chunk); + w.Write (chunk); + w.Write (chunk); + w.Write (oversized); + w.Write (chunk); + })); + Assert.Equal (expected, writer.GetIncrementalFingerprint ()); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void ComputeFingerprints_MatchesLegacyBufferedSerialization (bool useSharedTypemapUniverse) + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + // The fixtures must exercise every part of the walk that the two fingerprints share. + Assert.NotEmpty (model.Entries); + Assert.NotEmpty (model.ProxyTypes); + + var fingerprints = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental: true); + + Assert.Equal (LegacyContentFingerprint (model), fingerprints.Content); + Assert.Equal (LegacyIncrementalFingerprint (model, systemRuntimeVersion, useSharedTypemapUniverse), fingerprints.Incremental); + } + + [Fact] + public void ComputeFingerprints_WithoutIncremental_ProducesSameContentFingerprint () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + var withIncremental = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: true); + var contentOnly = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: false); + + Assert.Equal (withIncremental.Content, contentOnly.Content); + Assert.Null (contentOnly.Incremental); + Assert.NotNull (withIncremental.Incremental); + } + + [Fact] + public void ComputeFingerprints_ContentIgnoresEmitterConfiguration () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + var shared = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: true); + var perAssembly = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: false, includeIncremental: true); + + // The content fingerprint only covers the model, the incremental one also covers config. + Assert.Equal (shared.Content, perAssembly.Content); + Assert.NotEqual (shared.Incremental, perAssembly.Incremental); + } + + [Fact] + public void ComputeRootIncrementalFingerprint_MatchesLegacyBufferedSerialization () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + string [] names = ["_A.TypeMap", "_B.TypeMap"]; + + var expected = Sha256 (BinaryWriterBytes (writer => { + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (true); + writer.Write (names.Length); + foreach (var name in names) { + writer.Write (name); + } + })); + + Assert.Equal (expected, MetadataHelper.ComputeRootIncrementalFingerprint (names, systemRuntimeVersion, useSharedTypemapUniverse: true)); + } + + static Guid GeneratorModuleVersionId => typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; + + static void AssertSameBytes (Action expected, Action actual) + { + Assert.Equal (Sha256 (BinaryWriterBytes (expected)), WriterBytes (actual)); + } + + static byte [] WriterBytes (Action write) + { + using var writer = new FingerprintWriter (includeIncremental: false); + write (writer); + return writer.GetContentFingerprint (); + } + + static byte [] BinaryWriterBytes (Action write) + { + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + write (writer); + writer.Flush (); + return stream.ToArray (); + } + + static byte [] Sha256 (byte [] bytes) + { + using var sha = SHA256.Create (); + return sha.ComputeHash (bytes); + } + + // The implementations below are the buffered BinaryWriter serialisation used before the + // streaming rewrite. They are intentionally verbatim copies so the tests fail if the new + // walk changes the byte stream in any way. + + static byte [] LegacyContentFingerprint (TypeMapAssemblyData data) => Sha256 (BinaryWriterBytes (writer => { + foreach (var entry in data.Entries) { + writer.Write (entry.MapKey); + writer.Write (entry.ProxyTypeReference); + writer.Write (entry.TargetTypeReference ?? ""); + } + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + LegacyWriteTypeRef (writer, proxy.TargetType); + writer.Write ((byte) (proxy.ActivationCtor?.Style ?? 0)); + if (proxy.ActivationCtor is not null) { + LegacyWriteTypeRef (writer, proxy.ActivationCtor.DeclaringType); + } + writer.Write ((byte) (proxy.InvokerActivationCtorStyle ?? 0)); + writer.Write (proxy.UcoMethods.Count); + foreach (var method in proxy.UcoMethods) { + LegacyWriteUcoMethod (writer, method); + } + writer.Write (proxy.UcoConstructors.Count); + foreach (var constructor in proxy.UcoConstructors) { + LegacyWriteUcoConstructor (writer, constructor); + } + writer.Write (proxy.NativeRegistrations.Count); + foreach (var registration in proxy.NativeRegistrations) { + LegacyWriteNativeRegistration (writer, registration); + } + } + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + })); + + static byte [] LegacyIncrementalFingerprint (TypeMapAssemblyData data, Version systemRuntimeVersion, bool useSharedTypemapUniverse) => + Sha256 (BinaryWriterBytes (writer => { + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (useSharedTypemapUniverse); + writer.Write (data.AssemblyName); + writer.Write (data.ModuleName); + writer.Write (data.Entries.Count); + foreach (var entry in data.Entries) { + writer.Write (entry.MapKey); + writer.Write (entry.ProxyTypeReference); + LegacyWriteOptionalString (writer, entry.TargetTypeReference); + } + writer.Write (data.ProxyTypes.Count); + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.JniName); + writer.Write (proxy.Namespace); + LegacyWriteTypeRef (writer, proxy.TargetType); + LegacyWriteOptionalTypeRef (writer, proxy.InvokerType); + writer.Write (proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.Write ((byte) proxy.InvokerActivationCtorStyle.Value); + } + LegacyWriteOptionalActivationCtor (writer, proxy.ActivationCtor); + writer.Write (proxy.IsGenericDefinition); + writer.Write (proxy.CannotRegisterInStaticConstructor); + writer.Write (proxy.IsAcw); + writer.Write (proxy.UcoMethods.Count); + foreach (var method in proxy.UcoMethods) { + LegacyWriteUcoMethod (writer, method); + } + writer.Write (proxy.UcoConstructors.Count); + foreach (var constructor in proxy.UcoConstructors) { + LegacyWriteUcoConstructor (writer, constructor); + } + writer.Write (proxy.NativeRegistrations.Count); + foreach (var registration in proxy.NativeRegistrations) { + LegacyWriteNativeRegistration (writer, registration); + } + } + writer.Write (data.Associations.Count); + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + writer.Write (data.AliasHolders.Count); + foreach (var holder in data.AliasHolders) { + writer.Write (holder.TypeName); + writer.Write (holder.Namespace); + writer.Write (holder.AliasKeys.Count); + foreach (var aliasKey in holder.AliasKeys) { + writer.Write (aliasKey); + } + } + writer.Write (data.IgnoresAccessChecksTo.Count); + foreach (var assemblyName in data.IgnoresAccessChecksTo) { + writer.Write (assemblyName); + } + })); + + static void LegacyWriteTypeRef (BinaryWriter writer, TypeRefData type) + { + writer.Write (type.ManagedTypeName); + writer.Write (type.AssemblyName); + writer.Write (type.IsValueType ? (byte) 1 : (byte) 0); + writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); + writer.Write (type.GenericArguments.Count); + foreach (var argument in type.GenericArguments) { + LegacyWriteTypeRef (writer, argument); + } + } + + static void LegacyWriteOptionalTypeRef (BinaryWriter writer, TypeRefData? type) + { + writer.Write (type is not null); + if (type is not null) { + LegacyWriteTypeRef (writer, type); + } + } + + static void LegacyWriteOptionalString (BinaryWriter writer, string? value) + { + writer.Write (value is not null); + if (value is not null) { + writer.Write (value); + } + } + + static void LegacyWriteOptionalActivationCtor (BinaryWriter writer, ActivationCtorData? constructor) + { + writer.Write (constructor is not null); + if (constructor is not null) { + LegacyWriteTypeRef (writer, constructor.DeclaringType); + writer.Write (constructor.IsOnLeafType); + writer.Write ((byte) constructor.Style); + } + } + + static void LegacyWriteUcoMethod (BinaryWriter writer, UcoMethodData method) + { + writer.Write (method.WrapperName); + writer.Write (method.CallbackMethodName); + LegacyWriteTypeRef (writer, method.CallbackType); + writer.Write (method.JniSignature); + LegacyWriteOptionalStrings (writer, method.CallbackParameterTypeNames); + LegacyWriteOptionalString (writer, method.CallbackReturnTypeName); + LegacyWriteExportMethodDispatch (writer, method.ExportMethodDispatch); + } + + static void LegacyWriteOptionalStrings (BinaryWriter writer, IReadOnlyList? values) + { + writer.Write (values is not null); + if (values is null) { + return; + } + writer.Write (values.Count); + foreach (var value in values) { + writer.Write (value); + } + } + + static void LegacyWriteExportMethodDispatch (BinaryWriter writer, ExportMethodDispatchData? dispatch) + { + writer.Write (dispatch is not null); + if (dispatch is null) { + return; + } + writer.Write (dispatch.ManagedMethodName); + writer.Write (dispatch.ParameterTypes.Count); + foreach (var parameterType in dispatch.ParameterTypes) { + LegacyWriteTypeRef (writer, parameterType); + } + writer.Write (dispatch.ParameterKinds.Count); + foreach (var parameterKind in dispatch.ParameterKinds) { + writer.Write ((int) parameterKind); + } + LegacyWriteTypeRef (writer, dispatch.ReturnType); + writer.Write ((int) dispatch.ReturnKind); + writer.Write (dispatch.IsStatic); + } + + static void LegacyWriteUcoConstructor (BinaryWriter writer, UcoConstructorData constructor) + { + writer.Write (constructor.WrapperName); + LegacyWriteTypeRef (writer, constructor.TargetType); + writer.Write (constructor.JniSignature); + writer.Write (constructor.HasMatchingManagedCtor); + writer.Write (constructor.ManagedParameterTypes.Count); + foreach (var parameterType in constructor.ManagedParameterTypes) { + LegacyWriteTypeRef (writer, parameterType); + } + } + + static void LegacyWriteNativeRegistration (BinaryWriter writer, NativeRegistrationData registration) + { + writer.Write (registration.JniMethodName); + writer.Write (registration.JniSignature); + writer.Write (registration.WrapperMethodName); + writer.Write (registration.WrapperTarget.TypeNamespace); + writer.Write (registration.WrapperTarget.TypeName); + writer.Write (registration.WrapperTarget.MethodName); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index cc6a0ec36b9..42e985cb438 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -336,7 +336,7 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () Assert.Equal (full.GeneratedAssemblies.Count, optimized.GeneratedAssemblies.Count); for (int i = 0; i < full.GeneratedAssemblies.Count; i++) { Assert.Equal (full.GeneratedAssemblies [i].Name, optimized.GeneratedAssemblies [i].Name); - Assert.Equal (full.GeneratedAssemblies [i].Content.ToArray (), optimized.GeneratedAssemblies [i].Content.ToArray ()); + Assert.Equal (ReadAllBytes (full.GeneratedAssemblies [i].Content), ReadAllBytes (optimized.GeneratedAssemblies [i].Content)); } } @@ -387,7 +387,7 @@ public void Execute_IncrementalCallbackPreservesGeneratedBytes () Assert.Equal (full.GeneratedAssemblies.Count, incremental.GeneratedAssemblies.Count); for (int i = 0; i < full.GeneratedAssemblies.Count; i++) { Assert.Equal (full.GeneratedAssemblies [i].Name, incremental.GeneratedAssemblies [i].Name); - Assert.Equal (full.GeneratedAssemblies [i].Content.ToArray (), incremental.GeneratedAssemblies [i].Content.ToArray ()); + Assert.Equal (ReadAllBytes (full.GeneratedAssemblies [i].Content), ReadAllBytes (incremental.GeneratedAssemblies [i].Content)); } DisposeGeneratedAssemblies (full.GeneratedAssemblies); DisposeGeneratedAssemblies (incremental.GeneratedAssemblies); @@ -492,6 +492,14 @@ static JavaPeerInfo CreatePeer (string assemblyName, string managedTypeName, str }; } + static byte [] ReadAllBytes (Stream stream) + { + stream.Position = 0; + using var buffer = new MemoryStream (); + stream.CopyTo (buffer); + return buffer.ToArray (); + } + static void DisposeGeneratedAssemblies (IEnumerable assemblies) { foreach (var assembly in assemblies) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 48e55b3edc5..f205fc5236a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -393,6 +393,49 @@ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () Assert.Equal ("System.Int32", paramType); } + [Fact] + public void EmitBody_PreencodedSignature_PreservesMethodSignature () + { + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("PreencodedSigTest", "PreencodedSigTest.dll"); + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + pe.Metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Class, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("MyType"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + var signature = new BlobBuilder (); + signature.WriteByte ((byte) SignatureAttributes.Instance); + signature.WriteCompressedInteger (1); + signature.WriteByte ((byte) SignatureTypeCode.String); + signature.WriteByte ((byte) SignatureTypeCode.Int32); + + pe.EmitBody ( + "PreencodedMethod", + MethodAttributes.Public, + pe.Metadata.GetOrAddBlob (signature), + encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.Return (returnsValue: true); + }); + using var stream = new MemoryStream (); + pe.WritePE (stream); + stream.Position = 0; + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + var method = reader.TypeDefinitions + .SelectMany (handle => reader.GetTypeDefinition (handle).GetMethods ()) + .Select (handle => reader.GetMethodDefinition (handle)) + .Single (method => reader.GetString (method.Name) == "PreencodedMethod"); + var decoded = method.DecodeSignature (SignatureTypeProvider.Instance, null); + + Assert.Equal ("System.String", decoded.ReturnType); + Assert.Equal ("System.Int32", Assert.Single (decoded.ParameterTypes)); + } + [Fact] public void Generate_JiStyleInvoker_FirstParamIsByRef () {