File: Serialization\SerializerService_Reference.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection.Metadata;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Serialization;
 
using static TemporaryStorageService;
 
internal partial class SerializerService
{
    private const int MetadataFailed = int.MaxValue;
 
    /// <summary>
    /// Allow analyzer tests to exercise the oop codepaths, even though they're referring to in-memory instances of
    /// DiagnosticAnalyzers.  In that case, we'll just share the in-memory instance of the analyzer across the OOP
    /// boundary (which still runs in proc in tests), but we will still exercise all codepaths that use the RemoteClient
    /// as well as exercising all codepaths that send data across the OOP boundary.  Effectively, this allows us to
    /// pretend that a <see cref="AnalyzerImageReference"/> is a <see cref="AnalyzerFileReference"/> during tests.
    /// </summary>
    private static readonly object s_analyzerImageReferenceMapGate = new();
    private static IBidirectionalMap<AnalyzerImageReference, Guid> s_analyzerImageReferenceMap = BidirectionalMap<AnalyzerImageReference, Guid>.Empty;
 
    private static bool TryGetAnalyzerImageReferenceGuid(AnalyzerImageReference imageReference, out Guid guid)
    {
        lock (s_analyzerImageReferenceMapGate)
            return s_analyzerImageReferenceMap.TryGetValue(imageReference, out guid);
    }
 
    private static bool TryGetAnalyzerImageReferenceFromGuid(Guid guid, [NotNullWhen(true)] out AnalyzerImageReference? imageReference)
    {
        lock (s_analyzerImageReferenceMapGate)
            return s_analyzerImageReferenceMap.TryGetKey(guid, out imageReference);
    }
 
    private static Checksum CreateChecksum(MetadataReference reference)
    {
        if (reference is PortableExecutableReference portable)
            return CreatePortableExecutableReferenceChecksum(portable);
 
        throw ExceptionUtilities.UnexpectedValue(reference.GetType());
    }
 
    protected virtual Checksum CreateChecksum(AnalyzerReference reference)
    {
#if NET
        // If we're in the oop side and we're being asked to produce our local checksum (so we can compare it to the
        // host checksum), then we want to just defer to the underlying analyzer reference of our isolated reference.
        // This underlying reference corresponds to the reference that the host has, and we do not want to make any
        // changes as long as they're both in agreement.
        if (reference is IsolatedAnalyzerFileReference { UnderlyingAnalyzerFileReference: var underlyingReference })
            reference = underlyingReference;
#endif
 
        using var stream = SerializableBytes.CreateWritableStream();
 
        using (var writer = new ObjectWriter(stream, leaveOpen: true))
        {
            switch (reference)
            {
                case AnalyzerFileReference fileReference:
                    writer.WriteString(fileReference.FullPath);
                    writer.WriteGuid(IsolatedAnalyzerReferenceSet.TryGetFileReferenceMvid(fileReference.FullPath));
                    break;
 
                case AnalyzerImageReference analyzerImageReference:
                    Contract.ThrowIfFalse(TryGetAnalyzerImageReferenceGuid(analyzerImageReference, out var guid), "AnalyzerImageReferences are only supported during testing");
                    writer.WriteGuid(guid);
                    break;
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(reference);
            }
        }
 
        stream.Position = 0;
        return Checksum.Create(stream);
    }
 
    protected virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer)
    {
        if (reference is PortableExecutableReference portable)
        {
            if (portable is ISupportTemporaryStorage { StorageHandles: { Count: > 0 } handles } &&
                TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(portable, handles, writer))
            {
                return;
            }
 
            WritePortableExecutableReferenceTo(portable, writer);
            return;
        }
 
        throw ExceptionUtilities.UnexpectedValue(reference.GetType());
    }
 
    protected virtual MetadataReference ReadMetadataReferenceFrom(ObjectReader reader)
    {
        var type = reader.ReadString();
        if (type == nameof(PortableExecutableReference))
            return ReadPortableExecutableReferenceFrom(reader);
 
        throw ExceptionUtilities.UnexpectedValue(type);
    }
 
    protected virtual void WriteAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer)
    {
        switch (reference)
        {
            case AnalyzerFileReference fileReference:
                writer.WriteString(nameof(AnalyzerFileReference));
                writer.WriteString(fileReference.FullPath);
 
                // Note: it is intentional that we are not writing the MVID of the analyzer file reference over in (even
                // though we mixed it into the checksum).  We don't actually need the data on the other side as it will
                // be read out from the file itself.  So the flow is as follows when an analyzer-file-reference changes:
                //
                // 1. Change to file happens on disk and is detected by the host, which will reload the reference within it.
                // 2. When producing the checksum for the project, this analyzer file reference will not be found in the
                //    ChecksumCache, causing it to be recomputed (in `Checksum CreateChecksum(AnalyzerReference
                //    reference, CancellationToken cancellationToken)`.
                // 3. The checksum will be computed based on the file path and the MVID of the file.
                // 4. This will now cause a diff between the host and OOP.
                // 5. When OOP syncs with the host, it will create a fresh AnalyzerFileReference pointing to the right
                //    path, and specifying it wants to use the shadow copy loader.  The workspace snapshot will be
                //    updated to use this new reference.  Note: this is guaranteed, as `SolutionCompilationState
                //    WithProjectAnalyzerReferences(...)` uses reference-equality to determine if the analyzer is
                //    different, always picking up the new instances.
                // 6. When we actually need to load analyzers/generators in OOP it will then defer to the
                //    ShadowCopyAnalyzerAssemblyLoader.  This loader will *itself* then use the MVID of the file
                //    reference at the requested path to shadow copy to a new location specific to that mvid, ensuring
                //    that its data can be cleanly loaded in isolation from any prior version.
                break;
 
            case AnalyzerImageReference analyzerImageReference:
                Contract.ThrowIfFalse(TryGetAnalyzerImageReferenceGuid(analyzerImageReference, out var guid), "AnalyzerImageReferences are only supported during testing");
                writer.WriteString(nameof(AnalyzerImageReference));
                writer.WriteGuid(guid);
                break;
 
            default:
                throw ExceptionUtilities.UnexpectedValue(reference);
        }
    }
 
    protected virtual AnalyzerReference ReadAnalyzerReferenceFrom(ObjectReader reader)
    {
        switch (reader.ReadString())
        {
            case nameof(AnalyzerFileReference):
                // Rehydrate the analyzer file reference with the simple shared shadow copy loader.  Note: we won't
                // actually use this instance we create.  Instead, the caller will use create an IsolatedAssemblyReferenceSet
                // from these to ensure that all the types can be safely loaded into their own ALC.
                return new AnalyzerFileReference(reader.ReadRequiredString(), _analyzerLoaderProvider.SharedShadowCopyLoader);
 
            case nameof(AnalyzerImageReference):
                var guid = reader.ReadGuid();
                Contract.ThrowIfFalse(TryGetAnalyzerImageReferenceFromGuid(guid, out var analyzerImageReference));
                return analyzerImageReference;
 
            case var type:
                throw ExceptionUtilities.UnexpectedValue(type);
        }
    }
 
    protected static void WritePortableExecutableReferenceHeaderTo(
        PortableExecutableReference reference, SerializationKinds kind, ObjectWriter writer)
    {
        writer.WriteString(nameof(PortableExecutableReference));
        writer.WriteInt32((int)kind);
 
        WritePortableExecutableReferencePropertiesTo(reference, writer);
    }
 
    private static void WritePortableExecutableReferencePropertiesTo(PortableExecutableReference reference, ObjectWriter writer)
    {
        WriteTo(reference.Properties, writer);
        writer.WriteString(reference.FilePath);
    }
 
    private static Checksum CreatePortableExecutableReferenceChecksum(PortableExecutableReference reference)
    {
        using var stream = SerializableBytes.CreateWritableStream();
 
        using (var writer = new ObjectWriter(stream, leaveOpen: true))
        {
            WritePortableExecutableReferencePropertiesTo(reference, writer);
            WriteMvidsTo(TryGetMetadata(reference), writer);
        }
 
        stream.Position = 0;
        return Checksum.Create(stream);
    }
 
    private static void WriteMvidsTo(Metadata? metadata, ObjectWriter writer)
    {
        if (metadata == null)
        {
            // handle error case where we couldn't load metadata of the reference.
            // this basically won't write anything to writer
            return;
        }
 
        if (metadata is AssemblyMetadata assemblyMetadata)
        {
            if (!TryGetModules(assemblyMetadata, out var modules))
            {
                // Gracefully bail out without writing anything to the writer.
                return;
            }
 
            writer.WriteInt32((int)assemblyMetadata.Kind);
            writer.WriteInt32(modules.Length);
            foreach (var module in modules)
                WriteMvidTo(module, writer);
 
            return;
        }
 
        WriteMvidTo((ModuleMetadata)metadata, writer);
    }
 
    private static bool TryGetModules(AssemblyMetadata assemblyMetadata, out ImmutableArray<ModuleMetadata> modules)
    {
        // Gracefully handle documented exceptions from 'GetModules' invocation.
        try
        {
            modules = assemblyMetadata.GetModules();
            return true;
        }
        catch (Exception ex) when (ex is BadImageFormatException or
                                   IOException or
                                   ObjectDisposedException)
        {
            modules = default;
            return false;
        }
    }
 
    private static void WriteMvidTo(ModuleMetadata metadata, ObjectWriter writer)
    {
        writer.WriteInt32((int)metadata.Kind);
        writer.WriteGuid(GetMetadataGuid(metadata));
    }
 
    private static Guid GetMetadataGuid(ModuleMetadata metadata)
    {
        var metadataReader = metadata.GetMetadataReader();
        var mvidHandle = metadataReader.GetModuleDefinition().Mvid;
        var guid = metadataReader.GetGuid(mvidHandle);
        return guid;
    }
 
    private static void WritePortableExecutableReferenceTo(
        PortableExecutableReference reference, ObjectWriter writer)
    {
        WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.Bits, writer);
        WriteTo(TryGetMetadata(reference), writer);
 
        // TODO: what I should do with documentation provider? it is not exposed outside
    }
 
    private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectReader reader)
    {
        var kind = (SerializationKinds)reader.ReadInt32();
        Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile);
 
        var properties = ReadMetadataReferencePropertiesFrom(reader);
 
        var filePath = reader.ReadString();
 
        if (TryReadMetadataFrom(reader, kind) is not (var metadata, var storageHandles))
        {
            // TODO: deal with xml document provider properly
            //       should we shadow copy xml doc comment?
 
            // image doesn't exist
            return new MissingMetadataReference(properties, filePath, DocumentationProvider.Default);
        }
 
        // for now, we will use IDocumentationProviderService to get DocumentationProvider for metadata
        // references. if the service is not available, then use Default (NoOp) provider.
        // since xml doc comment is not part of solution snapshot, (like xml reference resolver or strong name
        // provider) this provider can also potentially provide content that is different than one in the host. 
        // an alternative approach of this is synching content of xml doc comment to remote host as well
        // so that we can put xml doc comment as part of snapshot. but until we believe that is necessary,
        // it will go with simpler approach
        var documentProvider = filePath != null && _documentationService != null ?
            _documentationService.GetDocumentationProvider(filePath) : DocumentationProvider.Default;
 
        return new SerializedPortableExecutableReference(
            properties, filePath, metadata, storageHandles, documentProvider);
    }
 
    private static void WriteTo(MetadataReferenceProperties properties, ObjectWriter writer)
    {
        writer.WriteInt32((int)properties.Kind);
        writer.WriteArray(properties.Aliases, static (w, a) => w.WriteString(a));
        writer.WriteBoolean(properties.EmbedInteropTypes);
    }
 
    private static MetadataReferenceProperties ReadMetadataReferencePropertiesFrom(ObjectReader reader)
    {
        var kind = (MetadataImageKind)reader.ReadInt32();
        var aliases = reader.ReadArray(static r => r.ReadRequiredString());
        var embedInteropTypes = reader.ReadBoolean();
 
        return new MetadataReferenceProperties(kind, aliases, embedInteropTypes);
    }
 
    private static void WriteTo(Metadata? metadata, ObjectWriter writer)
    {
        if (metadata == null)
        {
            // handle error case where metadata failed to load
            writer.WriteInt32(MetadataFailed);
            return;
        }
 
        if (metadata is AssemblyMetadata assemblyMetadata)
        {
            if (!TryGetModules(assemblyMetadata, out var modules))
            {
                // Gracefully handle error case where unable to get modules.
                writer.WriteInt32(MetadataFailed);
                return;
            }
 
            writer.WriteInt32((int)assemblyMetadata.Kind);
            writer.WriteInt32(modules.Length);
 
            foreach (var module in modules)
                WriteTo(module, writer);
 
            return;
        }
 
        WriteTo((ModuleMetadata)metadata, writer);
    }
 
    private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(
        PortableExecutableReference reference,
        IReadOnlyList<ITemporaryStorageStreamHandle> handles,
        ObjectWriter writer)
    {
        Contract.ThrowIfTrue(handles.Count == 0);
 
        WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.MemoryMapFile, writer);
 
        writer.WriteInt32((int)MetadataImageKind.Assembly);
        writer.WriteInt32(handles.Count);
 
        foreach (var handle in handles)
        {
            writer.WriteInt32((int)MetadataImageKind.Module);
            handle.Identifier.WriteTo(writer);
        }
 
        return true;
    }
 
    private (Metadata metadata, ImmutableArray<TemporaryStorageStreamHandle> storageHandles)? TryReadMetadataFrom(
        ObjectReader reader, SerializationKinds kind)
    {
        var imageKind = reader.ReadInt32();
        if (imageKind == MetadataFailed)
        {
            // error case
            return null;
        }
 
        var metadataKind = (MetadataImageKind)imageKind;
        if (metadataKind == MetadataImageKind.Assembly)
        {
            var count = reader.ReadInt32();
 
            var allMetadata = new FixedSizeArrayBuilder<ModuleMetadata>(count);
            var allHandles = new FixedSizeArrayBuilder<TemporaryStorageStreamHandle>(count);
 
            for (var i = 0; i < count; i++)
            {
                metadataKind = (MetadataImageKind)reader.ReadInt32();
                Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
 
                var (metadata, storageHandle) = ReadModuleMetadataFrom(reader, kind);
 
                allMetadata.Add(metadata);
                allHandles.Add(storageHandle);
            }
 
            return (AssemblyMetadata.Create(allMetadata.MoveToImmutable()), allHandles.MoveToImmutable());
        }
        else
        {
            Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
 
            var moduleInfo = ReadModuleMetadataFrom(reader, kind);
            return (moduleInfo.metadata, [moduleInfo.storageHandle]);
        }
    }
 
    private (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFrom(
        ObjectReader reader, SerializationKinds kind)
    {
        Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile);
 
        return kind == SerializationKinds.Bits
            ? ReadModuleMetadataFromBits()
            : ReadModuleMetadataFromMemoryMappedFile();
 
        (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromMemoryMappedFile()
        {
            // Host passed us a segment of its own memory mapped file.  We can just refer to that segment directly as it
            // will not be released by the host.
            var storageIdentifier = TemporaryStorageIdentifier.ReadFrom(reader);
            var storageHandle = TemporaryStorageService.GetStreamHandle(storageIdentifier);
            return ReadModuleMetadataFromStorage(storageHandle);
        }
 
        (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromBits()
        {
            // Host is sending us all the data as bytes.  Take that and write that out to a memory mapped file on the
            // server side so that we can refer to this data uniformly.
            using var stream = SerializableBytes.CreateWritableStream();
            CopyByteArrayToStream(reader, stream);
 
            var length = stream.Length;
            var storageHandle = _storageService.Value.WriteToTemporaryStorage(stream);
            Contract.ThrowIfTrue(length != storageHandle.Identifier.Size);
            return ReadModuleMetadataFromStorage(storageHandle);
        }
 
        (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromStorage(
            TemporaryStorageStreamHandle storageHandle)
        {
            // Now read in the module data using that identifier.  This will either be reading from the host's memory if
            // they passed us the information about that memory segment.  Or it will be reading from our own memory if they
            // sent us the full contents.
            //
            // The ITemporaryStorageStreamHandle should have given us an UnmanagedMemoryStream
            // since this only runs on Windows for VS.
            var unmanagedStream = (UnmanagedMemoryStream)storageHandle.ReadFromTemporaryStorage();
            Contract.ThrowIfFalse(storageHandle.Identifier.Size == unmanagedStream.Length);
 
            // For an unmanaged memory stream, ModuleMetadata can take ownership directly.  Stream will be kept alive as
            // long as the ModuleMetadata is alive due to passing its .Dispose method in as the onDispose callback of
            // the metadata.
            unsafe
            {
                var metadata = ModuleMetadata.CreateFromMetadata(
                    (IntPtr)unmanagedStream.PositionPointer, (int)unmanagedStream.Length, unmanagedStream.Dispose);
                return (metadata, storageHandle);
            }
        }
    }
 
    private static void CopyByteArrayToStream(ObjectReader reader, Stream stream)
    {
        // TODO: make reader be able to read byte[] chunk
        var content = reader.ReadByteArray();
        stream.Write(content, 0, content.Length);
    }
 
    private static void WriteTo(ModuleMetadata metadata, ObjectWriter writer)
    {
        writer.WriteInt32((int)metadata.Kind);
        WriteTo(metadata.GetMetadataReader(), writer);
    }
 
    private static unsafe void WriteTo(MetadataReader reader, ObjectWriter writer)
    {
        writer.WriteSpan(new ReadOnlySpan<byte>(reader.MetadataPointer, reader.MetadataLength));
    }
 
    private static void WriteUnresolvedAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer)
    {
        writer.WriteString(nameof(UnresolvedAnalyzerReference));
        writer.WriteString(reference.FullPath);
    }
 
    private static Metadata? TryGetMetadata(PortableExecutableReference reference)
    {
        try
        {
            return reference.GetMetadata();
        }
        catch
        {
            // We have a reference but the file the reference is pointing to might not actually exist on disk. In that
            // case, rather than crashing, we will handle it gracefully.
            return null;
        }
    }
 
    private sealed class MissingMetadataReference : PortableExecutableReference
    {
        private readonly DocumentationProvider _provider;
 
        public MissingMetadataReference(
            MetadataReferenceProperties properties, string? fullPath, DocumentationProvider initialDocumentation)
            : base(properties, fullPath, initialDocumentation)
        {
            // TODO: doc comment provider is a bit weird.
            _provider = initialDocumentation;
        }
 
        protected override DocumentationProvider CreateDocumentationProvider()
        {
            // TODO: properly implement this
            throw new NotImplementedException();
        }
 
        protected override Metadata GetMetadataImpl()
        {
            // we just throw "FileNotFoundException" even if it might not be actual reason
            // why metadata has failed to load. in this context, we don't care much on actual
            // reason. we just need to maintain failure when re-constructing solution to maintain
            // snapshot integrity. 
            //
            // if anyone care actual reason, he should get that info from original Solution.
            throw new FileNotFoundException(FilePath);
        }
 
        protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
            => new MissingMetadataReference(properties, FilePath, _provider);
    }
 
    public static class TestAccessor
    {
        public static void AddAnalyzerImageReference(AnalyzerImageReference analyzerImageReference)
        {
            lock (s_analyzerImageReferenceMapGate)
            {
                if (!s_analyzerImageReferenceMap.ContainsKey(analyzerImageReference))
                    s_analyzerImageReferenceMap = s_analyzerImageReferenceMap.Add(analyzerImageReference, Guid.NewGuid());
            }
        }
    }
}