File: PortableCallHelpers\InternalCallSignatureCollector.cs
Web Access
Project: ILCompiler.ReadyToRun.csproj (ILCompiler.ReadyToRun)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

using Internal.TypeSystem;
using Internal.TypeSystem.Ecma;

namespace ILCompiler.PortableCallHelpers
{
    /// <summary>
    /// Reports a condition that should fail the build, with a message that is complete on its own.
    /// </summary>
    internal sealed class LogAsErrorException(string message) : Exception(message);

    /// <summary>
    /// Emits generator diagnostics in the canonical MSBuild format, so that a build driving
    /// crossgen2 through Exec still reports them with their codes.
    /// </summary>
    internal sealed class InteropLogger(Logger logger)
    {
        private readonly HashSet<string> _reportedInfo = [];

        public void Warning(string code, string message)
            => logger.LogMessage($"crossgen2 : warning {code}: {message}");

        /// <summary>
        /// Reports an informational diagnostic once per distinct message, so that a type used by
        /// many signatures does not produce the same line repeatedly.
        /// </summary>
        public void InfoHigh(string code, string message)
        {
            if (_reportedInfo.Add($"{code}:{message}"))
                logger.LogMessage($"crossgen2 : message {code}: {message}");
        }

        public void Verbose(string message)
        {
            if (logger.IsVerbose)
                logger.LogMessage(message);
        }
    }

    /// <summary>
    /// Scans assemblies for methods marked with <c>MethodImplAttributes.InternalCall</c> and
    /// collects the portable entry point signatures the interpreter-to-native thunks are generated
    /// from.
    /// </summary>
    internal sealed class InternalCallSignatureCollector(InteropLogger log)
    {
        private readonly Dictionary<string, MethodDesc> _signatures = [];

        public IReadOnlyDictionary<string, MethodDesc> Signatures => _signatures;

        public void ScanType(EcmaType type)
        {
            foreach (MethodDesc method in type.GetMethods())
            {
                if (!method.IsInternalCall)
                    continue;

                // String constructors never reach a signature-derived thunk. They are compiled as static
                // factories ("String Ctor(args)", see WasmLowering.GetStringCtorActualSignature), and the
                // runtime special-cases them in both directions with hardcoded keys before it consults
                // this table: GetCookieForCalliSig and GetPortableEntryPointToInterpreterThunk in
                // src/coreclr/vm/wasm/helpers.cpp. Emitting the declared "void .ctor(this, args)" shape
                // here would only add entries nothing can look up.
                if (method.IsConstructor && method.OwningType.IsWellKnownType(WellKnownType.String))
                    continue;

                // A generic has no single signature to generate a thunk from, so the interpreter
                // would find none at call time - and a release build does not even assert on the
                // miss, it takes the null cookie. CoreLib declares no such method today.
                if (method.HasInstantiation || method.OwningType.HasInstantiation)
                {
                    throw new LogAsErrorException(
                        $"Generic InternalCall method '{type}::{method.Name.ToString()}' has no single signature to generate a thunk from.");
                }

                try
                {
                    // A managed signature: the lowering adds the 'T' for an instance method and the
                    // trailing 'p' for the portable entry point parameter.
                    string signature = InteropSignature.GetMethodSignature(method);
                    if (_signatures.TryAdd(signature, method))
                        log.Verbose($"Adding InternalCall signature {signature} for method '{type}.{method.Name.ToString()}'");
                }
                catch (Exception ex) when (ex is not LogAsErrorException)
                {
                    // Every non-generic InternalCall has a signature the lowering can describe, so a
                    // failure here is a bug in the generator rather than something the assembly did.
                    // Skipping it would silently drop a thunk and only fail once the interpreter tries
                    // to call the method.
                    throw new LogAsErrorException($"Could not get the signature for InternalCall method '{type}::{method.Name.ToString()}': {ex.Message}");
                }
            }
        }
    }
}