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 ()
{