diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs new file mode 100644 index 00000000000..e561aa0e4b0 --- /dev/null +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs @@ -0,0 +1,112 @@ +extern alias xamarinbuildtasks; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.Android.Sdk.TrimmableTypeMap; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using FieldRvaTable = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.FieldRvaTable; +using JniAssemblyRewriter = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniAssemblyRewriter; +using JniRewriteResult = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniRewriteResult; +using R8Mapping = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.R8Mapping; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +[Parallelizable (ParallelScope.Children)] +public class JniAssemblyRewriterTypeMapTests +{ + [Test] + public void RewritesGeneratedTypeMapWithOwnerSpecificMethodNames () + { + byte [] source = GenerateTypeMapWithSharedMethodName (); + var warnings = new List (); + + JniRewriteResult result = Rewrite (source, Mapping ( + "test.First -> a.b.First:\n" + + " void n_Run() -> a\n" + + "test.Second -> a.b.Second:\n" + + " void n_Run() -> b\n"), warnings); + + CollectionAssert.AreEquivalent (new [] { "a", "b", "()V" }, ReadUtf8Values (result.Image)); + CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); + } + + [Test] + public void RewritesGeneratedTypeMapWithMappedAndUnmappedMethodNames () + { + byte [] source = GenerateTypeMapWithSharedMethodName (); + var warnings = new List (); + + JniRewriteResult result = Rewrite (source, Mapping ( + "test.First -> a.b.First:\n" + + " void n_Run() -> a\n" + + "test.Second -> test.Second:\n"), warnings); + + CollectionAssert.AreEquivalent (new [] { "a", "n_Run", "()V" }, ReadUtf8Values (result.Image)); + CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); + } + + static JniRewriteResult Rewrite (byte [] sourceImage, R8Mapping mapping, IList warnings) + { + var engine = new MockBuildEngine (TestContext.Out, warnings: warnings); + var log = new TaskLoggingHelper (engine, nameof (JniAssemblyRewriterTypeMapTests)); + return JniAssemblyRewriter.Rewrite (sourceImage, mapping, log); + } + + static R8Mapping Mapping (string text) => R8Mapping.Parse (new StringReader (text)); + + static byte [] GenerateTypeMapWithSharedMethodName () + { + var peers = new [] { + CreatePeer ("test/First", "Test.First"), + CreatePeer ("test/Second", "Test.Second"), + }; + using var stream = new MemoryStream (); + new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)).Generate (peers, stream, "OwnerSpecificNames"); + return stream.ToArray (); + } + + static JavaPeerInfo CreatePeer (string javaName, string managedName) + { + int separator = managedName.LastIndexOf ('.'); + return new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = managedName, + ManagedTypeNamespace = managedName.Substring (0, separator), + ManagedTypeShortName = managedName.Substring (separator + 1), + AssemblyName = "TestAsm", + DoNotGenerateAcw = false, + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = "TestAsm", + Style = ActivationCtorStyle.XamarinAndroid, + }, + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "run", + NativeCallbackName = "n_Run", + JniSignature = "()V", + ManagedMethodName = "Run", + }, + ], + }; + } + + static string [] ReadUtf8Values (byte [] image) + { + using var peReader = new PEReader (ImmutableArray.Create (image)); + MetadataReader reader = peReader.GetMetadataReader (); + return FieldRvaTable.Read (peReader, reader).Entries + .Select (entry => entry.Utf8Value) + .OfType () + .ToArray (); + } +} diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj index 8e92f0383b8..0245cc843b8 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj @@ -19,6 +19,9 @@ + + xamarinbuildtasks + 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..5ec4c354a59 --- /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. +/// The source builder must not be cleared, freed, or reused after this stream is created because +/// the stream retains references to its chunk arrays. +/// +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); + var source = segment.Array ?? throw new InvalidOperationException ("Blob segment has no backing array."); + Buffer.BlockCopy (source, 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..f9bd9e541c3 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs @@ -0,0 +1,194 @@ +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.Incremental) == 0) { + if ((sink & Sink.Content) != 0) { + WriteContent (data, offset, count); + } + return; + } + + // Validate before writing either sink so a rejected Sink.Both write cannot leave the + // content fingerprint in a partially updated state. + if (incrementalHash is not IncrementalHash incrementalHashForWrite || + incrementalBuffer is not byte [] incrementalBufferForWrite) { + throw new InvalidOperationException ( + $"Cannot write to {nameof (Sink.Incremental)} because the incremental fingerprint was not requested."); + } + + if ((sink & Sink.Content) != 0) { + WriteContent (data, offset, count); + } + if (count > incrementalBufferForWrite.Length - incrementalPosition) { + FlushIncremental (); + } + if (count > incrementalBufferForWrite.Length) { + incrementalHashForWrite.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, incrementalBufferForWrite, incrementalPosition, count); + incrementalPosition += count; + } + } + + void WriteContent (byte [] data, int offset, int count) + { + 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; + } + } + + 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 d785fb59f6a..a0cb3f8725f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,12 +1,23 @@ using System; using System.Buffers; 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; @@ -38,114 +49,118 @@ 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 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 SHA256.HashData (stream.GetBuffer ().AsSpan (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 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 SHA256.HashData (stream.GetBuffer ().AsSpan (0, checked ((int) stream.Length))); + return new ModelFingerprints ( + writer.GetContentFingerprint (), + includeIncremental ? writer.GetIncrementalFingerprint () : null); } /// @@ -156,124 +171,106 @@ public static byte [] ComputeRootIncrementalFingerprint ( Version systemRuntimeVersion, bool useSharedTypemapUniverse) { - 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); + // This method needs only one hash. The content sink is used as the writer's always-present + // sink; the returned value is still solely the incremental-build fingerprint for the root. + 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 SHA256.HashData (stream.GetBuffer ().AsSpan (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); } - writer.Write (constructor.ParameterKinds.Count); + writer.WriteInt32 (sink, constructor.ParameterKinds.Count); foreach (var parameterKind in constructor.ParameterKinds) { - writer.Write ((int) parameterKind); + writer.WriteInt32 (sink, (int) parameterKind); } } - 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 2a0ba4da1c5..a96047aa98f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -105,6 +105,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), @@ -115,10 +131,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) @@ -160,10 +173,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); } /// @@ -420,6 +439,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. /// @@ -442,12 +470,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) { var localSigBlob = new BlobBuilder (32); @@ -471,7 +502,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/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 683464daa4c..56ebc34b512 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -83,6 +83,23 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha throw new ArgumentNullException (nameof (stream)); } + CreatePEBuilder (perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName, moduleName).WritePE (stream); + } + + /// + /// Generates the root typemap assembly and returns a read-only stream over the serialised image. + /// + internal Stream GenerateToStream (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, string? assemblyName = null, string? moduleName = null) + { + return CreatePEBuilder (perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName, moduleName).CreatePEStream (); + } + + PEAssemblyBuilder CreatePEBuilder (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, string? assemblyName, string? moduleName) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + assemblyName ??= DefaultAssemblyName; moduleName ??= assemblyName + ".dll"; @@ -130,7 +147,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha // Emit TypeMapLoader class with Initialize() method EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName); - pe.WritePE (stream); + return pe; } static void EmitSharedUniverseAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 68980695055..af57da825b3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -139,6 +139,11 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + // These handles belong to this emitter's readonly PE metadata builder and must not be shared + // across emitter instances. + BlobHandle _activationCtorSignature; + BlobHandle _createInstanceSignature; + EntityHandle _anchorTypeHandle; ExportMethodDispatchEmitter? _exportMethodDispatchEmitter; @@ -167,6 +172,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)); @@ -175,13 +188,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)); @@ -992,12 +1023,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); } @@ -1005,25 +1031,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 b0c6ee40bcb..7ae943196d8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection.PortableExecutable; using System.Xml.Linq; @@ -443,15 +442,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); } @@ -462,10 +463,8 @@ internal List GenerateTypeMapAssemblies ( generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); } if (generateRoot) { - var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); - rootStream.Position = 0; + var rootStream = rootGenerator.GenerateToStream (perAssemblyNames, useSharedTypemapUniverse); generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.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/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs index e66435ccc40..7dedecd1006 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs @@ -19,5 +19,6 @@ [assembly: Guid ("42bb9aea-f4d8-4b43-8956-8e1fee857697")] [assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo ("Microsoft.Android.Build.Tasks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("MSBuildDeviceIntegration, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index edae5d32fcf..33fd84ffdcb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -1479,6 +1479,7 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, }; + proj.MainActivity = proj.DefaultMainActivity; proj.SetRuntime (runtime); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); bool trimNativeAotJavaCode = isRelease && runtime == AndroidRuntime.NativeAOT; @@ -1528,7 +1529,7 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease FileAssert.Exists (typemapFingerprints, "IncrementalClean should preserve typemap fingerprints on a no-op build."); Assert.AreEqual (typemapFingerprintContent, File.ReadAllText (typemapFingerprints), "A no-op build should not change typemap fingerprints."); - proj.MainActivity = proj.DefaultMainActivity + Environment.NewLine + "// Force trimmable typemap regeneration."; + proj.MainActivity += Environment.NewLine + "// Force trimmable typemap regeneration."; proj.Touch ("MainActivity.cs"); Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Changed-input build should have succeeded."); builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); @@ -1546,6 +1547,7 @@ public void Build_WithTrimmableTypeMap_MissingJavaListPreservesGeneratedJava () } var proj = new XamarinAndroidApplicationProject (); + proj.MainActivity = proj.DefaultMainActivity; proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); 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..2a8f79d05bd --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -0,0 +1,164 @@ +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 NoOpTrimmableTypeMapLogger ()); + 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.IsType (assembly.Content); + 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)); + } + } + +} 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..d699c7c2404 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs @@ -0,0 +1,417 @@ +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])); + Assert.Equal (Sha256 ([]), writer.GetContentFingerprint ()); + } + } + + [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); + } + writer.Write (constructor.ParameterKinds.Count); + foreach (var parameterKind in constructor.ParameterKinds) { + writer.Write ((int) parameterKind); + } + } + + 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 d8fb8ebfe85..9a1a90346f8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -15,33 +15,33 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); - sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : ITrimmableTypeMapLogger + sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : NoOpTrimmableTypeMapLogger { - public void LogNoJavaPeerTypesFound () => + public override void LogNoJavaPeerTypesFound () => logMessages.Add ("No Java peer types found, skipping typemap generation."); - public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) => + public override void LogJavaPeerScanInfo (int assemblyCount, int peerCount) => logMessages.Add ($"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); - public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) => + public override void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) => logMessages.Add ($"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); - public void LogDeferredRegistrationTypesInfo (int typeCount) => + public override void LogDeferredRegistrationTypesInfo (int typeCount) => logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); - public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => + public override void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => logMessages.Add ($" {assemblyName}: {typeCount} types"); - public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => + public override void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); - public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => + public override void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => logMessages.Add ($"Generated {assemblyCount} typemap assemblies."); - public void LogGeneratedJcwFilesInfo (int sourceCount) => + public override void LogGeneratedJcwFilesInfo (int sourceCount) => logMessages.Add ($"Generated {sourceCount} JCW Java source files."); - public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + public override void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); - public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + public override void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); - public void LogLibraryManifestMergeWarning (string message) => + public override void LogLibraryManifestMergeWarning (string message) => warnings?.Add (message); - public void LogInvalidManifestPlaceholderWarning (string placeholders) => + public override void LogInvalidManifestPlaceholderWarning (string placeholders) => warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'."); - public void LogUnresolvableJavaPeerSkippedWarning ( + public override void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, string unresolvedTypeName, @@ -50,35 +50,35 @@ public void LogUnresolvableJavaPeerSkippedWarning ( warnings?.Add ( $"Skipping Java peer '{managedTypeName}' from '{assemblyName}' because referenced type " + $"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); - public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => + public override void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); - public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => + public override void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => logMessages.Add ($"XA4258: Java name '{javaName}' contains invalid or unsupported Java identifier '{invalidIdentifier}'."); - public void LogDuplicateJavaTypeError (string javaName) => + public override void LogDuplicateJavaTypeError (string javaName) => logMessages.Add ($"XA4215: The Java type `{javaName}` is generated by more than one managed type."); - public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + public override void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => logMessages.Add ($"XA4215: `{javaName}` generated by: {managedTypeName}"); - public void LogExportFieldWithParametersError () => + public override void LogExportFieldWithParametersError () => logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); - public void LogExportOnGenericTypeError () => + public override void LogExportOnGenericTypeError () => logMessages.Add ("XA4206: [Export] cannot be used on a generic type."); - public void LogExportFieldReturnsVoidError () => + public override void LogExportFieldReturnsVoidError () => logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); - public void LogExportFieldOnGenericTypeError () => + public override void LogExportFieldOnGenericTypeError () => logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); - public void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => + public override void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => logMessages.Add ($"XA4263: The exported member '{memberName}' has unsupported signature type '{managedTypeName}'."); - public void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature) => + public override void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature) => logMessages.Add ($"XA4259: Type '{managedTypeName}' has multiple managed constructors that map to JNI signature '{jniSignature}'."); - public void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType) => + public override void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType) => logMessages.Add ($"XA4260: Type '{managedTypeName}' has a constructor parameter type '{parameterType}' that cannot be represented in Java."); - public void LogMissingBaseConstructorError (string managedTypeName, string jniSignature) => + public override void LogMissingBaseConstructorError (string managedTypeName, string jniSignature) => logMessages.Add ($"XA4261: Type '{managedTypeName}' has constructor '{jniSignature}' with no callable Java base constructor."); - public void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString) => + public override void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString) => logMessages.Add ($"XA4262: Type '{managedTypeName}' has invalid SuperArgumentsString '{superArgumentsString}'."); - public void LogCustomJavaObjectError (string managedTypeName) => + public override void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); - public void LogCustomJavaObjectWarning (string managedTypeName) => + public override void LogCustomJavaObjectWarning (string managedTypeName) => warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); } @@ -610,7 +610,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)); } } @@ -698,7 +698,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); @@ -854,6 +854,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 9a44ee63a6b..c06151ad8c9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -444,6 +444,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 () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs new file mode 100644 index 00000000000..7ce45e66686 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs @@ -0,0 +1,38 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +class NoOpTrimmableTypeMapLogger : ITrimmableTypeMapLogger +{ + public virtual void LogNoJavaPeerTypesFound () { } + public virtual void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } + public virtual void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } + public virtual void LogDeferredRegistrationTypesInfo (int typeCount) { } + public virtual void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } + public virtual void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } + public virtual void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } + public virtual void LogGeneratedJcwFilesInfo (int sourceCount) { } + public virtual void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } + public virtual void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } + public virtual void LogLibraryManifestMergeWarning (string message) { } + public virtual void LogInvalidManifestPlaceholderWarning (string placeholders) { } + public virtual void LogUnresolvableJavaPeerSkippedWarning ( + string managedTypeName, + string assemblyName, + string unresolvedTypeName, + string unresolvedAssemblyName, + string unresolvedAssemblyPath) { } + public virtual void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } + public virtual void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } + public virtual void LogDuplicateJavaTypeError (string javaName) { } + public virtual void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) { } + public virtual void LogExportFieldWithParametersError () { } + public virtual void LogExportOnGenericTypeError () { } + public virtual void LogExportFieldOnGenericTypeError () { } + public virtual void LogExportFieldReturnsVoidError () { } + public virtual void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) { } + public virtual void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature) { } + public virtual void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType) { } + public virtual void LogMissingBaseConstructorError (string managedTypeName, string jniSignature) { } + public virtual void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString) { } + public virtual void LogCustomJavaObjectError (string managedTypeName) { } + public virtual void LogCustomJavaObjectWarning (string managedTypeName) { } +}