diff --git a/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs b/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs index 120634338bcb9..96b32b1dec4c7 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs @@ -64,6 +64,13 @@ private enum IndirectReturnState : byte private LocalDefinition _returnTemp; + /// + /// True if there was a anywhere in the method. This will + /// affect whether or not we require the locals init flag to be marked, since locals init + /// affects . + /// + private bool _sawStackalloc; + public CodeGenerator( MethodSymbol method, BoundStatement boundBody, @@ -188,18 +195,24 @@ internal static bool IsStackLocal(LocalSymbol local, HashSet stackL private bool IsStackLocal(LocalSymbol local) => IsStackLocal(local, _stackLocals); - public void Generate() + public void Generate(out bool hasStackalloc) { this.GenerateImpl(); + hasStackalloc = _sawStackalloc; Debug.Assert(_asyncCatchHandlerOffset < 0); Debug.Assert(_asyncYieldPoints == null); Debug.Assert(_asyncResumePoints == null); } - public void Generate(out int asyncCatchHandlerOffset, out ImmutableArray asyncYieldPoints, out ImmutableArray asyncResumePoints) + public void Generate( + out int asyncCatchHandlerOffset, + out ImmutableArray asyncYieldPoints, + out ImmutableArray asyncResumePoints, + out bool hasStackAlloc) { this.GenerateImpl(); + hasStackAlloc = _sawStackalloc; Debug.Assert(_asyncCatchHandlerOffset >= 0); asyncCatchHandlerOffset = _builder.GetILOffsetFromMarker(_asyncCatchHandlerOffset); diff --git a/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs b/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs index 87eb97ff0ea28..6bcfd38473ed9 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs @@ -1916,6 +1916,7 @@ private void EmitConvertedStackAllocExpression(BoundConvertedStackAllocExpressio // we can ignore that if the actual result is unused if (used) { + _sawStackalloc = true; _builder.EmitOpCode(ILOpCode.Localloc); } diff --git a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs index 172279f2e86ac..f58217cfc2b49 100644 --- a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs +++ b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs @@ -1411,6 +1411,7 @@ private static MethodBody GenerateMethodBody( var optimizations = compilation.Options.OptimizationLevel; ILBuilder builder = new ILBuilder(moduleBuilder, localSlotManager, optimizations, method.AreLocalsZeroed); + bool hasStackalloc; DiagnosticBag diagnosticsForThisMethod = DiagnosticBag.GetInstance(); try { @@ -1447,7 +1448,7 @@ private static MethodBody GenerateMethodBody( if (isAsyncStateMachine) { - codeGen.Generate(out int asyncCatchHandlerOffset, out var asyncYieldPoints, out var asyncResumePoints); + codeGen.Generate(out int asyncCatchHandlerOffset, out var asyncYieldPoints, out var asyncResumePoints, out hasStackalloc); // The exception handler IL offset is used by the debugger to treat exceptions caught by the marked catch block as "user unhandled". // This is important for async void because async void exceptions generally result in the process being terminated, @@ -1468,7 +1469,7 @@ private static MethodBody GenerateMethodBody( } else { - codeGen.Generate(); + codeGen.Generate(out hasStackalloc); if ((object)kickoffMethod != null) { @@ -1531,6 +1532,7 @@ private static MethodBody GenerateMethodBody( debugDocumentProvider, builder.RealizedExceptionHandlers, builder.AreLocalsZeroed, + hasStackalloc, builder.GetAllScopes(), builder.HasDynamicLocal, importScopeOpt, diff --git a/src/Compilers/CSharp/Test/Emit/Attributes/AttributeTests_WellKnownAttributes.cs b/src/Compilers/CSharp/Test/Emit/Attributes/AttributeTests_WellKnownAttributes.cs index 60f5f6980e70a..4a52b7688c757 100644 --- a/src/Compilers/CSharp/Test/Emit/Attributes/AttributeTests_WellKnownAttributes.cs +++ b/src/Compilers/CSharp/Test/Emit/Attributes/AttributeTests_WellKnownAttributes.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis.CSharp.Symbols; @@ -20,19 +21,67 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests { public static class WellKnownAttributeTestsUtil { - public static bool? HasLocalsInit(this CompilationVerifier verifier, string methodName, bool realIL = false) + public static bool? HasLocalsInit(this CompilationVerifier verifier, string qualifiedMethodName, bool realIL = false) { - var il = verifier.VisualizeIL(methodName, realIL); - - if (il.Contains(".locals init (")) + if (realIL) { - return true; + var peReader = new PEReader(verifier.EmittedAssemblyData); + var metadataReader = peReader.GetMetadataReader(); + + int lastDotIndex = qualifiedMethodName.LastIndexOf('.'); + var spanName = qualifiedMethodName.AsSpan(); + var typeName = spanName.Slice(0, lastDotIndex); + var methodName = spanName.Slice(lastDotIndex + 1); + + TypeDefinition typeDef = default; + + foreach (var typeHandle in metadataReader.TypeDefinitions) + { + var type = metadataReader.GetTypeDefinition(typeHandle); + var name = metadataReader.GetString(type.Name); + + if (name.AsSpan().Equals(typeName, StringComparison.Ordinal)) + { + typeDef = type; + break; + } + } + + Assert.NotEqual(default, typeDef); + + MethodDefinition methodDef = default; + + foreach (var methodHandle in typeDef.GetMethods()) + { + var method = metadataReader.GetMethodDefinition(methodHandle); + var name = metadataReader.GetString(method.Name); + + if (name.AsSpan().Equals(methodName, StringComparison.Ordinal)) + { + methodDef = method; + break; + } + } + + Assert.NotEqual(default, methodDef); + + var block = peReader.GetMethodBody(methodDef.RelativeVirtualAddress); + return block.LocalVariablesInitialized; } - if (il.Contains(".locals (")) + else { - return false; + var il = verifier.VisualizeIL(qualifiedMethodName, realIL); + + if (il.Contains(".locals init (")) + { + return true; + } + if (il.Contains(".locals (")) + { + return false; + } + return null; } - return null; } } @@ -8929,6 +8978,20 @@ public static int Main() #region SkipLocalsInitAttribute + private CompilationVerifier CompileAndVerifyWithSkipLocalsInit(string src) + { + const string skipLocalsInitDef = @" +namespace System.Runtime.CompilerServices +{ + public class SkipLocalsInitAttribute : System.Attribute + { + } +}"; + + var comp = CreateCompilation(new[] { src, skipLocalsInitDef }, options: TestOptions.UnsafeReleaseDll); + return CompileAndVerify(comp, verify: Verification.Fails); + } + [Fact] public void SkipLocalsInitRequiresUnsafe() { @@ -9040,6 +9103,39 @@ partial class C Assert.False(comp.HasLocalsInit("C.M")); } + [Fact] + public unsafe void StackallocWithSkipLocalsInit() + { + var src = @" +public class C +{ + [System.Runtime.CompilerServices.SkipLocalsInitAttribute] + public unsafe void M1() + { + int *ptr = stackalloc int[10]; + System.Console.WriteLine(ptr[0]); + } + + public unsafe void M2() + { + int *ptr = stackalloc int[10]; + System.Console.WriteLine(ptr[0]); + } + + public unsafe void M3() + { + int* p = stackalloc int[10]; // unused + } +}"; + var verifier = CompileAndVerifyWithSkipLocalsInit(src); + Assert.Null(verifier.HasLocalsInit("C.M1")); // no locals + Assert.False(verifier.HasLocalsInit("C.M1", realIL: true)); + Assert.Null(verifier.HasLocalsInit("C.M2")); // no locals + Assert.True(verifier.HasLocalsInit("C.M2", realIL: true)); + Assert.Null(verifier.HasLocalsInit("C.M3")); // no locals + Assert.False(verifier.HasLocalsInit("C.M3", realIL: true)); + } + [Fact] public void WhenMethodsDifferBySkipLocalsInitAttributeTheyMustHaveDifferentRVA() { diff --git a/src/Compilers/Core/Portable/CodeGen/MethodBody.cs b/src/Compilers/Core/Portable/CodeGen/MethodBody.cs index 9f3c675c4989f..674f49b4c1c28 100644 --- a/src/Compilers/Core/Portable/CodeGen/MethodBody.cs +++ b/src/Compilers/Core/Portable/CodeGen/MethodBody.cs @@ -52,6 +52,7 @@ public MethodBody( DebugDocumentProvider debugDocumentProvider, ImmutableArray exceptionHandlers, bool areLocalsZeroed, + bool hasStackalloc, ImmutableArray localScopes, bool hasDynamicLocalVariables, Cci.IImportScope importScopeOpt, @@ -75,6 +76,7 @@ public MethodBody( _locals = locals; _exceptionHandlers = exceptionHandlers; _areLocalsZeroed = areLocalsZeroed; + HasStackalloc = hasStackalloc; _localScopes = localScopes; _hasDynamicLocalVariables = hasDynamicLocalVariables; _importScopeOpt = importScopeOpt; @@ -144,5 +146,10 @@ ImmutableArray Cci.IMethodBody.StateMachineHoistedLocalSlot public ImmutableArray LambdaDebugInfo => _lambdaDebugInfo; public ImmutableArray ClosureDebugInfo => _closureDebugInfo; + + /// + /// True if there's a stackalloc somewhere in the method. + /// + public bool HasStackalloc { get; } } } diff --git a/src/Compilers/Core/Portable/Emit/NoPia/CommonEmbeddedMethod.cs b/src/Compilers/Core/Portable/Emit/NoPia/CommonEmbeddedMethod.cs index 558515b147649..5d4895bb63299 100644 --- a/src/Compilers/Core/Portable/Emit/NoPia/CommonEmbeddedMethod.cs +++ b/src/Compilers/Core/Portable/Emit/NoPia/CommonEmbeddedMethod.cs @@ -113,6 +113,8 @@ public EmptyBody(CommonEmbeddedMethod method) ImmutableArray Cci.IMethodBody.ExceptionRegions => ImmutableArray.Empty; + bool Cci.IMethodBody.HasStackalloc => false; + bool Cci.IMethodBody.AreLocalsZeroed => false; ImmutableArray Cci.IMethodBody.LocalVariables => diff --git a/src/Compilers/Core/Portable/PEWriter/Members.cs b/src/Compilers/Core/Portable/PEWriter/Members.cs index 18b2aa143e4a0..442cddb0187fa 100644 --- a/src/Compilers/Core/Portable/PEWriter/Members.cs +++ b/src/Compilers/Core/Portable/PEWriter/Members.cs @@ -328,6 +328,11 @@ ImmutableArray ExceptionRegions /// bool AreLocalsZeroed { get; } + /// + /// True if there's a stackalloc somewhere in the method. + /// + bool HasStackalloc { get; } + /// /// The local variables of the method. /// diff --git a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs index 83769fc261fd2..b2c32d04147b0 100644 --- a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs +++ b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs @@ -2933,7 +2933,8 @@ private int SerializeMethodBody(MethodBodyStreamEncoder encoder, IMethodBody met exceptionRegionCount: exceptionRegions.Length, hasSmallExceptionRegions: MayUseSmallExceptionHeaders(exceptionRegions), localVariablesSignature: localSignatureHandleOpt, - attributes: (methodBody.AreLocalsZeroed ? MethodBodyAttributes.InitLocals : 0)); + attributes: (methodBody.AreLocalsZeroed ? MethodBodyAttributes.InitLocals : 0), + hasDynamicStackAllocation: methodBody.HasStackalloc); // Don't do small body method caching during deterministic builds until this issue is fixed // https://github.com/dotnet/roslyn/issues/7595 diff --git a/src/Compilers/VisualBasic/Portable/Compilation/MethodCompiler.vb b/src/Compilers/VisualBasic/Portable/Compilation/MethodCompiler.vb index b5ef4a0b67aeb..98507a7b45bd2 100644 --- a/src/Compilers/VisualBasic/Portable/Compilation/MethodCompiler.vb +++ b/src/Compilers/VisualBasic/Portable/Compilation/MethodCompiler.vb @@ -1611,6 +1611,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic debugDocumentProvider, builder.RealizedExceptionHandlers, areLocalsZeroed:=True, + hasStackalloc:=False, localScopes, hasDynamicLocalVariables:=False, importScopeOpt:=importScopeOpt, diff --git a/src/Test/Utilities/Portable/Metadata/ILValidation.cs b/src/Test/Utilities/Portable/Metadata/ILValidation.cs index 9b0fd37ba38cb..ddcb0754b6e76 100644 --- a/src/Test/Utilities/Portable/Metadata/ILValidation.cs +++ b/src/Test/Utilities/Portable/Metadata/ILValidation.cs @@ -300,6 +300,22 @@ public static unsafe string GetMethodIL(this ImmutableArray ilArray) return result.ToString(); } + public static unsafe MethodBodyBlock GetMethodBodyBlock(this ImmutableArray ilArray) + { + fixed (byte* ilPtr = ilArray.AsSpan()) + { + int offset = 0; + // skip padding: + while (offset < ilArray.Length && ilArray[offset] == 0) + { + offset++; + } + + var reader = new BlobReader(ilPtr + offset, ilArray.Length - offset); + return MethodBodyBlock.Create(reader); + } + } + public static Dictionary GetSequencePointMarkers(string pdbXml, string source = null) { var doc = new XmlDocument() { XmlResolver = null };