File: Compiler\SubstitutionProvider.cs
Web Access
Project: src\src\runtime\src\coreclr\tools\aot\ILCompiler.Compiler\ILCompiler.Compiler.csproj (ILCompiler.Compiler)
// 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 System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Resources;

using ILCompiler.DependencyAnalysis;

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

using Debug = System.Diagnostics.Debug;

namespace ILCompiler
{
    public class SubstitutionProvider
    {
        private readonly FeatureSwitchHashtable _hashtable;

        public SubstitutionProvider(Logger logger, IReadOnlyDictionary<string, bool> switchValues, BodyAndFieldSubstitutions globalSubstitutions)
        {
            _hashtable = new FeatureSwitchHashtable(logger, switchValues, globalSubstitutions);
        }

        public BodySubstitution GetSubstitution(MethodDesc method)
        {
            if (method.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod)
            {
                AssemblyFeatureInfo info = _hashtable.GetOrCreateValue(ecmaMethod.Module);
                if (info.BodySubstitutions != null && info.BodySubstitutions.TryGetValue(ecmaMethod, out BodySubstitution result))
                    return result;

                if (TryGetFeatureCheckValue(ecmaMethod, out bool value))
                    return BodySubstitution.Create(value ? 1 : 0);
            }

            return null;
        }

        private bool TryGetFeatureCheckValue(EcmaMethod method, out bool value)
        {
            value = false;

            if (!method.Signature.IsStatic)
                return false;

            if (!method.Signature.ReturnType.IsWellKnownType(WellKnownType.Boolean))
                return false;

            if (FindProperty(method) is not PropertyPseudoDesc property)
                return false;

            if (property.SetMethod != null)
                return false;

            foreach (var featureSwitchDefinitionAttribute in property.GetDecodedCustomAttributes("System.Diagnostics.CodeAnalysis", "FeatureSwitchDefinitionAttribute"))
            {
                if (featureSwitchDefinitionAttribute.FixedArguments is not [CustomAttributeTypedArgument<TypeDesc> { Value: string switchName }])
                    continue;

                // If there's a FeatureSwitchDefinition, don't continue looking for FeatureGuard.
                // We don't want to infer feature switch settings from FeatureGuard.
                return _hashtable._switchValues.TryGetValue(switchName, out value);
            }

            foreach (var featureGuardAttribute in property.GetDecodedCustomAttributes("System.Diagnostics.CodeAnalysis", "FeatureGuardAttribute"))
            {
                if (featureGuardAttribute.FixedArguments is not [CustomAttributeTypedArgument<TypeDesc> { Value: EcmaType featureType }])
                    continue;

                if (featureType.Namespace.SequenceEqual("System.Diagnostics.CodeAnalysis"u8))
                {
                    switch (featureType.GetName())
                    {
                        case "RequiresAssemblyFilesAttribute":
                        case "RequiresUnreferencedCodeAttribute":
                        case "RequiresDynamicCodeAttribute":
                            return true;
                    }
                }
            }

            return false;

            static PropertyPseudoDesc FindProperty(EcmaMethod method)
            {
                if ((method.Attributes & MethodAttributes.SpecialName) == 0)
                    return null;

                if (method.OwningType is not EcmaType declaringType)
                    return null;

                var reader = declaringType.MetadataReader;
                foreach (PropertyDefinitionHandle propertyHandle in reader.GetTypeDefinition(declaringType.Handle).GetProperties())
                {
                    PropertyDefinition propertyDef = reader.GetPropertyDefinition(propertyHandle);
                    var property = new PropertyPseudoDesc(declaringType, propertyHandle);
                    if (property.GetMethod == method)
                        return property;
                }

                return null;
            }
        }

        public object GetSubstitution(FieldDesc field)
        {
            if (field.GetTypicalFieldDefinition() is EcmaField ecmaField)
            {
                AssemblyFeatureInfo info = _hashtable.GetOrCreateValue(ecmaField.Module);
                if (info.BodySubstitutions != null && info.FieldSubstitutions.TryGetValue(ecmaField, out object result))
                    return result;
            }

            return null;
        }

        public bool HasSubstitutedBody(MethodDesc method)
        {
            return GetSubstitution(method) != null;
        }

        public bool HasSubstitutedValue(FieldDesc field)
        {
            return GetSubstitution(field) != null;
        }

        internal bool ShouldInlineResourceStrings => !_hashtable._switchValues.TryGetValue("System.Resources.UseSystemResourceKeys", out bool useResourceKeys) || !useResourceKeys;

        internal string GetResourceStringForAccessor(EcmaMethod method)
        {
            Debug.Assert(method.Name.StartsWith("get_"u8));
            string resourceStringName = System.Text.Encoding.UTF8.GetString(method.Name.Slice(4));

            Dictionary<string, string> dict = _hashtable.GetOrCreateValue(method.Module).InlineableResourceStrings;
            if (dict != null
                && dict.TryGetValue(resourceStringName, out string result))
            {
                return result;
            }

            return null;
        }

        private sealed class FeatureSwitchHashtable : LockFreeReaderHashtable<EcmaModule, AssemblyFeatureInfo>
        {
            internal readonly IReadOnlyDictionary<string, bool> _switchValues;
            private readonly Logger _logger;
            private readonly BodyAndFieldSubstitutions _globalSubstitutions;

            public FeatureSwitchHashtable(Logger logger, IReadOnlyDictionary<string, bool> switchValues, BodyAndFieldSubstitutions globalSubstitutions)
            {
                _logger = logger;
                _switchValues = switchValues;
                _globalSubstitutions = globalSubstitutions;
            }

            protected override bool CompareKeyToValue(EcmaModule key, AssemblyFeatureInfo value) => key == value.Module;
            protected override bool CompareValueToValue(AssemblyFeatureInfo value1, AssemblyFeatureInfo value2) => value1.Module == value2.Module;
            protected override int GetKeyHashCode(EcmaModule key) => key.GetHashCode();
            protected override int GetValueHashCode(AssemblyFeatureInfo value) => value.Module.GetHashCode();

            protected override AssemblyFeatureInfo CreateValueFromKey(EcmaModule key)
            {
                return new AssemblyFeatureInfo(key, _logger, _switchValues, _globalSubstitutions);
            }
        }

        private sealed class AssemblyFeatureInfo
        {
            public EcmaModule Module { get; }

            public IReadOnlyDictionary<MethodDesc, BodySubstitution> BodySubstitutions { get; }
            public IReadOnlyDictionary<FieldDesc, object> FieldSubstitutions { get; }
            public Dictionary<string, string> InlineableResourceStrings { get; }

            public AssemblyFeatureInfo(EcmaModule module, Logger logger, IReadOnlyDictionary<string, bool> featureSwitchValues, BodyAndFieldSubstitutions globalSubstitutions)
            {
                Module = module;

                PEMemoryBlock resourceDirectory = module.PEReader.GetSectionData(module.PEReader.PEHeaders.CorHeader.ResourcesDirectory.RelativeVirtualAddress);

                BodyAndFieldSubstitutions substitutions = default;

                foreach (var resourceHandle in module.MetadataReader.ManifestResources)
                {
                    ManifestResource resource = module.MetadataReader.GetManifestResource(resourceHandle);

                    // Don't try to process linked resources or resources in other assemblies
                    if (!resource.Implementation.IsNil)
                    {
                        continue;
                    }

                    string resourceName = module.MetadataReader.GetString(resource.Name);
                    if (resourceName == "ILLink.Substitutions.xml")
                    {
                        BlobReader reader = resourceDirectory.GetReader((int)resource.Offset, resourceDirectory.Length - (int)resource.Offset);
                        int length = (int)reader.ReadUInt32();

                        UnmanagedMemoryStream ms;
                        unsafe
                        {
                            ms = new UnmanagedMemoryStream(reader.CurrentPointer, length);
                        }

                        substitutions = BodySubstitutionsParser.GetSubstitutions(logger, module.Context, ms, resource, module, "name", featureSwitchValues);
                    }
                    else if (InlineableStringsResourceNode.IsInlineableStringsResource(module, resourceName))
                    {
                        BlobReader reader = resourceDirectory.GetReader((int)resource.Offset, resourceDirectory.Length - (int)resource.Offset);
                        int length = (int)reader.ReadUInt32();

                        UnmanagedMemoryStream ms;
                        unsafe
                        {
                            ms = new UnmanagedMemoryStream(reader.CurrentPointer, length);
                        }

                        InlineableResourceStrings = new Dictionary<string, string>();

                        using var resReader = new ResourceReader(ms);
                        var enumerator = resReader.GetEnumerator();
                        while (enumerator.MoveNext())
                        {
                            if (enumerator.Key is string key && enumerator.Value is string value)
                                InlineableResourceStrings[key] = value;
                        }
                    }
                }

                // Also apply any global substitutions
                // Note we allow these to overwrite substitutions in the assembly
                substitutions.AppendFrom(globalSubstitutions);

                (BodySubstitutions, FieldSubstitutions) = (substitutions.BodySubstitutions, substitutions.FieldSubstitutions);
            }
        }
    }
}