{
// Passing a default string for "at" in case SR.UsingResourceKeys() is true
// as this is a special case and we don't want to have "Word_At" on stack traces.
- string word_At = SR.GetResourceString(nameof(SR.Word_At), defaultString: "at");
+ string word_At = SR.UsingResourceKeys() ? "at" : SR.Word_At;
builder.Append(" ").Append(word_At).Append(' ');
builder.AppendLine(DeveloperExperience.Default.CreateStackTraceString(_ipAddress, _needFileInfo));
}
if (_isLastFrameFromForeignExceptionStackTrace)
{
// Passing default for Exception_EndStackTraceFromPreviousThrow in case SR.UsingResourceKeys is set.
- builder.AppendLine(SR.GetResourceString(nameof(SR.Exception_EndStackTraceFromPreviousThrow),
- defaultString: "--- End of stack trace from previous location ---"));
+ builder.AppendLine(SR.UsingResourceKeys() ?
+ "--- End of stack trace from previous location ---" :
+ SR.Exception_EndStackTraceFromPreviousThrow);
}
}
}
--- /dev/null
+// 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 ILCompiler.DependencyAnalysisFramework;
+using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
+
+namespace ILCompiler.DependencyAnalysis
+{
+ /// <summary>
+ /// Represents a resource blob used by the SR class in the BCL.
+ /// If this node is present in the graph, it means we were not able to optimize its use away
+ /// and the blob has to be generated.
+ /// </summary>
+ internal sealed class InlineableStringsResourceNode : DependencyNodeCore<NodeFactory>
+ {
+ private readonly EcmaModule _module;
+
+ public const string ResourceAccessorTypeName = "SR";
+ public const string ResourceAccessorTypeNamespace = "System";
+ public const string ResourceAccessorGetStringMethodName = "GetResourceString";
+
+ public InlineableStringsResourceNode(EcmaModule module)
+ {
+ _module = module;
+ }
+
+ public override bool InterestingForDynamicDependencyAnalysis => false;
+
+ public override bool HasDynamicDependencies => false;
+
+ public override bool HasConditionalStaticDependencies => false;
+
+ public override bool StaticDependenciesAreComputed => true;
+
+ public static bool IsInlineableStringsResource(EcmaModule module, string resourceName)
+ {
+ if (!resourceName.EndsWith(".resources", StringComparison.Ordinal))
+ return false;
+
+ // Make a guess at the name of the resource Arcade tooling generated for the resource
+ // strings.
+ // https://github.com/dotnet/runtime/issues/81385 tracks not having to guess this.
+ string simpleName = module.Assembly.GetName().Name;
+ string resourceName1 = $"{simpleName}.Strings.resources";
+ string resourceName2 = $"FxResources.{simpleName}.SR.resources";
+
+ if (resourceName != resourceName1 && resourceName != resourceName2)
+ return false;
+
+ MetadataType srType = module.GetType(ResourceAccessorTypeNamespace, ResourceAccessorTypeName, throwIfNotFound: false);
+ if (srType == null)
+ return false;
+
+ return srType.GetMethod(ResourceAccessorGetStringMethodName, null) != null;
+ }
+
+ public static void AddDependenciesDueToResourceStringUse(ref DependencyList dependencies, NodeFactory factory, MethodDesc method)
+ {
+ if (method.Name == ResourceAccessorGetStringMethodName && method.OwningType is MetadataType mdType
+ && mdType.Name == ResourceAccessorTypeName && mdType.Namespace == ResourceAccessorTypeNamespace)
+ {
+ dependencies ??= new DependencyList();
+ dependencies.Add(factory.InlineableStringResource((EcmaModule)mdType.Module), "Using the System.SR class");
+ }
+ }
+
+ public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory context) => null;
+ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory context) => null;
+ public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null;
+ protected override string GetName(NodeFactory context)
+ => $"String resources for {_module.Assembly.GetName().Name}";
+ }
+}
return new ModuleMetadataNode(module);
});
+ _inlineableStringResources = new NodeCache<EcmaModule, InlineableStringsResourceNode>(module =>
+ {
+ return new InlineableStringsResourceNode(module);
+ });
+
_customAttributesWithMetadata = new NodeCache<ReflectableCustomAttribute, CustomAttributeMetadataNode>(ca =>
{
return new CustomAttributeMetadataNode(ca);
return _modulesWithMetadata.GetOrAdd(module);
}
+ private NodeCache<EcmaModule, InlineableStringsResourceNode> _inlineableStringResources;
+ internal InlineableStringsResourceNode InlineableStringResource(EcmaModule module)
+ {
+ return _inlineableStringResources.GetOrAdd(module);
+ }
+
private NodeCache<ReflectableCustomAttribute, CustomAttributeMetadataNode> _customAttributesWithMetadata;
internal CustomAttributeMetadataNode CustomAttributeMetadata(ReflectableCustomAttribute ca)
string resourceName = module.MetadataReader.GetString(resource.Name);
// Check if emitting the manifest resource is blocked by policy.
- if (factory.MetadataManager.IsManifestResourceBlocked(module, resourceName))
+ if (factory.MetadataManager.IsManifestResourceBlocked(factory, module, resourceName))
continue;
string assemblyName = module.GetName().FullName;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
+using System.Resources;
+
+using ILCompiler.DependencyAnalysis;
using Internal.IL;
using Internal.TypeSystem;
// (Lets us avoid seeing lots of small basic blocks within eliminated chunks.)
VisibleBasicBlockStart = 0x10,
+ // This is a potential SR.get_SomeResourceString call.
+ GetResourceStringCall = 0x20,
+
// The instruction at this offset is reachable
Mark = 0x80,
}
// Last step is a sweep - we replace the tail of all unreachable blocks with "br $-2"
// and nop out the rest. If the basic block is smaller than 2 bytes, we don't touch it.
// We also eliminate any EH records that correspond to the stubbed out basic block.
+ //
+ // We also attempt to rewrite calls to SR.SomeResourceString accessors with string
+ // literals looked up from the managed resources.
Debug.Assert(method.GetMethodILDefinition() == method);
+ // Do not attempt to inline resource strings if we only want to use resource keys.
+ // The optimizations are not compatible.
+ bool shouldInlineResourceStrings =
+ !_hashtable._switchValues.TryGetValue("System.Resources.UseSystemResourceKeys", out bool useResourceKeys) || !useResourceKeys;
+
ILExceptionRegion[] ehRegions = method.GetExceptionRegions();
byte[] methodBytes = method.GetILBytes();
OpcodeFlags[] flags = new OpcodeFlags[methodBytes.Length];
}
}
+ bool hasGetResourceStringCall = false;
+
// Mark all reachable basic blocks
//
// We also do another round of basic block marking to mark beginning of visible basic blocks
if (reader.HasNext)
flags[reader.Offset] |= OpcodeFlags.VisibleBasicBlockStart;
}
+ else if (shouldInlineResourceStrings && opcode == ILOpcode.call)
+ {
+ var callee = method.GetObject(reader.ReadILToken(), NotFoundBehavior.ReturnNull) as EcmaMethod;
+ if (callee != null && callee.IsSpecialName && callee.OwningType is EcmaType calleeType
+ && calleeType.Name == InlineableStringsResourceNode.ResourceAccessorTypeName
+ && calleeType.Namespace == InlineableStringsResourceNode.ResourceAccessorTypeNamespace
+ && callee.Signature is { Length: 0, IsStatic: true }
+ && callee.Name.StartsWith("get_", StringComparison.Ordinal))
+ {
+ flags[offset] |= OpcodeFlags.GetResourceStringCall;
+ hasGetResourceStringCall = true;
+ }
+ }
else
{
reader.Skip(opcode);
}
}
- if (!hasUnmarkedInstruction)
+ if (!hasUnmarkedInstruction && !hasGetResourceStringCall)
return method;
byte[] newBody = (byte[])methodBytes.Clone();
debugInfo = new SubstitutedDebugInformation(debugInfo, sequencePoints.ToArray());
}
- return new SubstitutedMethodIL(method, newBody, newEHRegions.ToArray(), debugInfo);
+ // We only optimize EcmaMethods because there we can find out the highest string token RID
+ // in use.
+ ArrayBuilder<string> newStrings = default;
+ if (hasGetResourceStringCall && method.GetMethodILDefinition() is EcmaMethodIL ecmaMethodIL)
+ {
+ // We're going to inject new string tokens. Start where the last token of the module left off.
+ // We don't need this token to be globally unique because all token resolution happens in the context
+ // of a MethodIL and we're making a new one here. It just has to be unique to the MethodIL.
+ int tokenRid = ecmaMethodIL.Module.MetadataReader.GetHeapSize(HeapIndex.UserString);
+
+ for (int offset = 0; offset < flags.Length; offset++)
+ {
+ if ((flags[offset] & OpcodeFlags.GetResourceStringCall) == 0)
+ continue;
+
+ Debug.Assert(newBody[offset] == (byte)ILOpcode.call);
+ var getter = (EcmaMethod)method.GetObject(new ILReader(newBody, offset + 1).ReadILToken());
+
+ // If we can't get the string, this might be something else.
+ string resourceString = GetResourceStringForAccessor(getter);
+ if (resourceString == null)
+ continue;
+
+ // If we ran out of tokens, we can't optimize anymore.
+ if (tokenRid > 0xFFFFFF)
+ continue;
+
+ newStrings.Add(resourceString);
+
+ // call and ldstr are both 5-byte instructions: opcode followed by a token.
+ newBody[offset] = (byte)ILOpcode.ldstr;
+ newBody[offset + 1] = (byte)tokenRid;
+ newBody[offset + 2] = (byte)(tokenRid >> 8);
+ newBody[offset + 3] = (byte)(tokenRid >> 16);
+ newBody[offset + 4] = TokenTypeString;
+
+ tokenRid++;
+ }
+ }
+
+ return new SubstitutedMethodIL(method, newBody, newEHRegions.ToArray(), debugInfo, newStrings.ToArray());
}
private bool TryGetConstantArgument(MethodIL methodIL, byte[] body, OpcodeFlags[] flags, int offset, int argIndex, out int constant)
return false;
}
+ private string GetResourceStringForAccessor(EcmaMethod method)
+ {
+ Debug.Assert(method.Name.StartsWith("get_", StringComparison.Ordinal));
+ string resourceStringName = method.Name.Substring(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 SubstitutedMethodIL : MethodIL
{
private readonly byte[] _body;
private readonly ILExceptionRegion[] _ehRegions;
private readonly MethodIL _wrappedMethodIL;
private readonly MethodDebugInformation _debugInfo;
+ private readonly string[] _newStrings;
- public SubstitutedMethodIL(MethodIL wrapped, byte[] body, ILExceptionRegion[] ehRegions, MethodDebugInformation debugInfo)
+ public SubstitutedMethodIL(MethodIL wrapped, byte[] body, ILExceptionRegion[] ehRegions, MethodDebugInformation debugInfo, string[] newStrings)
{
_wrappedMethodIL = wrapped;
_body = body;
_ehRegions = ehRegions;
_debugInfo = debugInfo;
+ _newStrings = newStrings;
}
public override MethodDesc OwningMethod => _wrappedMethodIL.OwningMethod;
public override ILExceptionRegion[] GetExceptionRegions() => _ehRegions;
public override byte[] GetILBytes() => _body;
public override LocalVariableDefinition[] GetLocals() => _wrappedMethodIL.GetLocals();
- public override object GetObject(int token, NotFoundBehavior notFoundBehavior) => _wrappedMethodIL.GetObject(token, notFoundBehavior);
+ public override object GetObject(int token, NotFoundBehavior notFoundBehavior)
+ {
+ // If this is a string token, it could be one of the new string tokens we injected.
+ if ((token >>> 24) == TokenTypeString
+ && _wrappedMethodIL.GetMethodILDefinition() is EcmaMethodIL ecmaMethodIL)
+ {
+ int rid = token & 0xFFFFFF;
+ int maxRealTokenRid = ecmaMethodIL.Module.MetadataReader.GetHeapSize(HeapIndex.UserString);
+ if (rid >= maxRealTokenRid)
+ {
+ // Yep, string injected by us.
+ return _newStrings[rid - maxRealTokenRid];
+ }
+ }
+
+ return _wrappedMethodIL.GetObject(token, notFoundBehavior);
+ }
public override MethodDebugInformation GetDebugInfo() => _debugInfo;
}
public override IEnumerable<ILSequencePoint> GetSequencePoints() => _sequencePoints;
}
+ private const int TokenTypeString = 0x70; // CorTokenType for strings
+
private sealed class FeatureSwitchHashtable : LockFreeReaderHashtable<EcmaModule, AssemblyFeatureInfo>
{
- private readonly Dictionary<string, bool> _switchValues;
+ internal readonly Dictionary<string, bool> _switchValues;
private readonly Logger _logger;
public FeatureSwitchHashtable(Logger logger, Dictionary<string, bool> switchValues)
public Dictionary<MethodDesc, BodySubstitution> BodySubstitutions { get; }
public Dictionary<FieldDesc, object> FieldSubstitutions { get; }
+ public Dictionary<string, string> InlineableResourceStrings { get; }
public AssemblyFeatureInfo(EcmaModule module, Logger logger, IReadOnlyDictionary<string, bool> featureSwitchValues)
{
(BodySubstitutions, FieldSubstitutions) = 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;
+ }
+ }
}
}
}
ExactMethodInstantiationsNode.GetExactMethodInstantiationDependenciesForMethod(ref dependencies, factory, method);
}
+ InlineableStringsResourceNode.AddDependenciesDueToResourceStringUse(ref dependencies, factory, method);
+
GetDependenciesDueToMethodCodePresenceInternal(ref dependencies, factory, method, methodIL);
}
return _blockingPolicy.IsBlocked(typicalMethodDefinition);
}
- public bool IsManifestResourceBlocked(ModuleDesc module, string resourceName)
+ public bool IsManifestResourceBlocked(NodeFactory factory, Internal.TypeSystem.Ecma.EcmaModule module, string resourceName)
{
- return _resourceBlockingPolicy.IsManifestResourceBlocked(module, resourceName);
+ if (_resourceBlockingPolicy.IsManifestResourceBlocked(module, resourceName))
+ return true;
+
+ // If this is a resource strings resource but we don't actually need it, block it.
+ if (InlineableStringsResourceNode.IsInlineableStringsResource(module, resourceName)
+ && !factory.InlineableStringResource(module).Marked)
+ return true;
+
+ return false;
}
public bool CanGenerateMetadata(MetadataType type)
<Compile Include="Compiler\DependencyAnalysis\FieldRvaDataNode.cs" />
<Compile Include="Compiler\DependencyAnalysis\GenericStaticBaseInfoNode.cs" />
<Compile Include="Compiler\DependencyAnalysis\IndirectionExtensions.cs" />
+ <Compile Include="Compiler\DependencyAnalysis\InlineableStringsResourceNode.cs" />
<Compile Include="Compiler\DependencyAnalysis\InterfaceDispatchCellSectionNode.cs" />
<Compile Include="Compiler\DependencyAnalysis\MethodExceptionHandlingInfoNode.cs" />
<Compile Include="Compiler\DependencyAnalysis\ModuleInitializerListNode.cs" />
// by default it returns the value of System.Resources.UseSystemResourceKeys AppContext switch or false if not specified.
// Native code generators can replace the value this returns based on user input at the time of native code generation.
// The Linker is also capable of replacing the value of this method when the application is being trimmed.
- private static bool UsingResourceKeys() => s_usingResourceKeys;
+ internal static bool UsingResourceKeys() => s_usingResourceKeys;
internal static string GetResourceString(string resourceKey)
{
56
};
- //
- // EnglishNames is the concatenation of the English names for each codepage.
- // It is used in retrieving the value for System.Text.Encoding.EncodingName
- // given System.Text.Encoding.CodePage.
- // This is done rather than using a large readonly array of strings to avoid
- // generating a large amount of code in the static constructor.
- //
- private const string EnglishNames =
- "Unicode" + // 1200
- "Unicode (Big-Endian)" + // 1201
- "Unicode (UTF-32)" + // 12000
- "Unicode (UTF-32 Big-Endian)" + // 12001
- "US-ASCII" + // 20127
- "Western European (ISO)" + // 28591
- "Unicode (UTF-7)" + // 65000
- "Unicode (UTF-8)"; // 65001
-
- //
- // EnglishNameIndices contains the start index of each code page's English
- // name in the string EnglishNames. It is indexed by an index into
- // MappedCodePages.
- //
- private static ReadOnlySpan<int> EnglishNameIndices => new int[]
- {
- 0, // Unicode (1200)
- 7, // Unicode (Big-Endian) (1201)
- 27, // Unicode (UTF-32) (12000)
- 43, // Unicode (UTF-32 Big-Endian) (12001)
- 70, // US-ASCII (20127)
- 78, // Western European (ISO) (28591)
- 100, // Unicode (UTF-7) (65000)
- 115, // Unicode (UTF-8) (65001)
- 130
- };
-
// redeclaring these constants here for readability below
private const uint MIMECONTF_MAILNEWS = Encoding.MIMECONTF_MAILNEWS;
private const uint MIMECONTF_BROWSER = Encoding.MIMECONTF_BROWSER;
arrayEncodingInfo[arrayEncodingInfoIdx++] = new EncodingInfo(
codePage,
webNames[webNameIndices[i]..webNameIndices[i + 1]],
- GetDisplayName(codePage, i)
+ GetDisplayName(codePage)
);
}
if (codePage != Encoding.CodePageUTF7 || LocalAppContextSwitches.EnableUnsafeUTF7Encoding)
{
encodingInfoList[codePage] = new EncodingInfo(codePage, webNames[webNameIndices[i]..webNameIndices[i + 1]],
- GetDisplayName(codePage, i));
+ GetDisplayName(codePage));
}
}
}
// All supported code pages have identical header names, and body names.
string headerName = webName;
string bodyName = webName;
- string displayName = GetDisplayName(codePage, index);
+ string displayName = GetDisplayName(codePage);
uint flags = Flags[index];
return new CodePageDataItem(uiFamilyCodePage, webName, headerName, bodyName, displayName, flags);
}
- private static string GetDisplayName(int codePage, int englishNameIndex)
+ private static string GetDisplayName(int codePage)
{
- string? displayName = SR.GetResourceString("Globalization_cp_" + codePage.ToString());
- if (string.IsNullOrEmpty(displayName))
- displayName = EnglishNames[EnglishNameIndices[englishNameIndex]..EnglishNameIndices[englishNameIndex + 1]];
-
- return displayName;
+ switch (codePage)
+ {
+ case 1200: return SR.Globalization_cp_1200;
+ case 1201: return SR.Globalization_cp_1201;
+ case 12000: return SR.Globalization_cp_12000;
+ case 12001: return SR.Globalization_cp_12001;
+ case 20127: return SR.Globalization_cp_20127;
+ case 28591: return SR.Globalization_cp_28591;
+ case 65000: return SR.Globalization_cp_65000;
+ case 65001: return SR.Globalization_cp_65001;
+ };
+
+ Debug.Fail("Unexpected code page");
+ return "";
}
}
}
Console.WriteLine("Resources in reflection library:");
string[] refNames;
const string reflectionAssembly = "System.Private.Reflection.Execution";
-#if RESOURCE_KEYS
try
{
refNames = System.Reflection.Assembly.Load(reflectionAssembly).GetManifestResourceNames();
{
refNames = Array.Empty<string>();
}
-#else
-refNames = System.Reflection.Assembly.Load(reflectionAssembly).GetManifestResourceNames();
-#endif
foreach (var name in refNames)
Console.WriteLine(name);