Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial MaxStack calculation #93244

Merged
merged 12 commits into from
Oct 25, 2023
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers.Binary;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Reflection.Emit
Expand All @@ -17,6 +15,7 @@ internal sealed class ILGeneratorImpl : ILGenerator
private readonly InstructionEncoder _il;
private bool _hasDynamicStackAllocation;
private int _maxStackSize;
private int _currentStack;

internal ILGeneratorImpl(MethodBuilder methodBuilder, int size)
{
Expand All @@ -42,40 +41,88 @@ internal ILGeneratorImpl(MethodBuilder methodBuilder, int size)
public override LocalBuilder DeclareLocal(Type localType, bool pinned) => throw new NotImplementedException();
public override Label DefineLabel() => throw new NotImplementedException();

public override void Emit(OpCode opcode)
private static int GetStackChangeFor(StackBehaviour stackBehaviour) =>
stackBehaviour switch
{
StackBehaviour.Pop0 or
StackBehaviour.Push0 => 0,
StackBehaviour.Pop1 or
StackBehaviour.Popi or
StackBehaviour.Popref or
StackBehaviour.Varpop => -1,
StackBehaviour.Pop1_pop1 or
StackBehaviour.Popi_pop1 or
StackBehaviour.Popi_popi or
StackBehaviour.Popi_popi8 or
StackBehaviour.Popi_popr4 or
StackBehaviour.Popi_popr8 or
StackBehaviour.Popref_pop1 or
StackBehaviour.Popref_popi => -2,
StackBehaviour.Popi_popi_popi or
StackBehaviour.Popref_popi_pop1 or
StackBehaviour.Popref_popi_popi or
StackBehaviour.Popref_popi_popi8 or
StackBehaviour.Popref_popi_popr4 or
StackBehaviour.Popref_popi_popr8 or
StackBehaviour.Popref_popi_popref => -3,
StackBehaviour.Push1 or
StackBehaviour.Pushi or
StackBehaviour.Pushi8 or
StackBehaviour.Pushr4 or
StackBehaviour.Pushr8 or
StackBehaviour.Pushref or
StackBehaviour.Varpush => 1,
StackBehaviour.Push1_push1 => 2,
_ => throw new InvalidOperationException()
};

private void UpdateStackSize(OpCode opCode)
{
_currentStack += GetStackChangeFor(opCode.StackBehaviourPush) + GetStackChangeFor(opCode.StackBehaviourPop);

if (_currentStack > _maxStackSize)
{
_maxStackSize = _currentStack;
}
}

public void EmitOpcode(OpCode opcode)
{
if (opcode == OpCodes.Localloc)
{
_hasDynamicStackAllocation = true;
}

_il.OpCode((ILOpCode)opcode.Value);
UpdateStackSize(opcode);
}

// TODO: for now only count the Opcodes emitted, in order to calculate it correctly we might need to make internal Opcode APIs public
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/Opcode.cs#L48
_maxStackSize++;
public override void Emit(OpCode opcode)
{
EmitOpcode(opcode);
}

public override void Emit(OpCode opcode, byte arg)
{
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_builder.WriteByte(arg);
}

public override void Emit(OpCode opcode, double arg)
{
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_builder.WriteDouble(arg);
}

public override void Emit(OpCode opcode, float arg)
{
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_builder.WriteSingle(arg);
}

public override void Emit(OpCode opcode, short arg)
{
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_builder.WriteInt16(arg);
}

Expand All @@ -86,98 +133,91 @@ public override void Emit(OpCode opcode, int arg)
{
if (arg >= -1 && arg <= 8)
{
_il.OpCode(arg switch
EmitOpcode(arg switch
{
-1 => ILOpCode.Ldc_i4_m1,
0 => ILOpCode.Ldc_i4_0,
1 => ILOpCode.Ldc_i4_1,
2 => ILOpCode.Ldc_i4_2,
3 => ILOpCode.Ldc_i4_3,
4 => ILOpCode.Ldc_i4_4,
5 => ILOpCode.Ldc_i4_5,
6 => ILOpCode.Ldc_i4_6,
7 => ILOpCode.Ldc_i4_7,
_ => ILOpCode.Ldc_i4_8,
-1 => OpCodes.Ldc_I4_M1,
0 => OpCodes.Ldc_I4_0,
1 => OpCodes.Ldc_I4_1,
2 => OpCodes.Ldc_I4_2,
3 => OpCodes.Ldc_I4_3,
4 => OpCodes.Ldc_I4_4,
5 => OpCodes.Ldc_I4_5,
6 => OpCodes.Ldc_I4_6,
7 => OpCodes.Ldc_I4_7,
_ => OpCodes.Ldc_I4_8
});
return;
}

if (arg >= -128 && arg <= 127)
{
_il.OpCode(ILOpCode.Ldc_i4_s);
_builder.WriteSByte((sbyte)arg) ;
Emit(OpCodes.Ldc_I4_S, (sbyte)arg);
return;
}
}
else if (opcode.Equals(OpCodes.Ldarg))
{
if ((uint)arg <= 3)
{
_il.OpCode(arg switch
EmitOpcode(arg switch
{
0 => ILOpCode.Ldarg_0,
1 => ILOpCode.Ldarg_1,
2 => ILOpCode.Ldarg_2,
_ => ILOpCode.Ldarg_3,
0 => OpCodes.Ldarg_0,
1 => OpCodes.Ldarg_1,
2 => OpCodes.Ldarg_2,
_ => OpCodes.Ldarg_3,
});
return;
}

if ((uint)arg <= byte.MaxValue)
{
_il.OpCode(ILOpCode.Ldarg_s);
_builder.WriteByte((byte)arg);
Emit(OpCodes.Ldarg_S, (byte)arg);
return;
}

if ((uint)arg <= ushort.MaxValue) // this will be true except on misuse of the opcode
{
_il.OpCode(ILOpCode.Ldarg);
_builder.WriteInt16((short)arg);
Emit(OpCodes.Ldarg, (short)arg);
return;
}
}
else if (opcode.Equals(OpCodes.Ldarga))
{
if ((uint)arg <= byte.MaxValue)
{
_il.OpCode(ILOpCode.Ldarga_s);
_builder.WriteByte((byte)arg);
Emit(OpCodes.Ldarga_S, (byte)arg);
return;
}

if ((uint)arg <= ushort.MaxValue) // this will be true except on misuse of the opcode
{
_il.OpCode(ILOpCode.Ldarga);
_builder.WriteInt16((short)arg);
Emit(OpCodes.Ldarga, (short)arg);
return;
}
}
else if (opcode.Equals(OpCodes.Starg))
{
if ((uint)arg <= byte.MaxValue)
{
_il.OpCode(ILOpCode.Starg_s);
_builder.WriteByte((byte)arg);
Emit(OpCodes.Starg_S, (byte)arg);
return;
}

if ((uint)arg <= ushort.MaxValue) // this will be true except on misuse of the opcode
{
_il.OpCode(ILOpCode.Starg);
_builder.WriteInt16((short)arg);
Emit(OpCodes.Starg, (short)arg);
return;
}
}

// For everything else, put the opcode followed by the arg onto the stream of instructions.
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_builder.WriteInt32(arg);
}

public override void Emit(OpCode opcode, long arg)
{
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_il.CodeBuilder.WriteInt64(arg);
}

Expand All @@ -187,7 +227,7 @@ public override void Emit(OpCode opcode, string str)
// represented by str.
ModuleBuilder modBuilder = (ModuleBuilder)_methodBuilder.Module;
int tempVal = modBuilder.GetStringMetadataToken(str);
_il.OpCode((ILOpCode)opcode.Value);
EmitOpcode(opcode);
_il.Token(tempVal);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,81 @@ public void MultipleTypesWithMultipleMethods()
Assert.Equal(OpCodes.Ldc_I8.Value, longBody[0]);
}
}

[Fact]
public void ILOffset_Test()
{
AssemblyBuilder ab = AssemblySaveTools.PopulateAssemblyBuilderTypeBuilderAndSaveMethod(out TypeBuilder type, out MethodInfo saveMethod);
MethodBuilder method = type.DefineMethod("Method1", MethodAttributes.Public | MethodAttributes.Static, typeof(Type), new Type[0]);
ILGenerator ilGenerator = method.GetILGenerator();

Assert.Equal(0, ilGenerator.ILOffset);
ilGenerator.Emit(OpCodes.Ret);
Assert.Equal(1, ilGenerator.ILOffset);
}

[Fact]
public void ILMaxStack_Test()
{
using (TempFile file = TempFile.Create())
{
AssemblyBuilder ab = AssemblySaveTools.PopulateAssemblyBuilderTypeBuilderAndSaveMethod(out TypeBuilder type, out MethodInfo saveMethod);
MethodBuilder method1 = type.DefineMethod("Method1", MethodAttributes.Public, typeof(long), new Type[] { typeof(int), typeof(long), typeof(short), typeof(byte) });
ILGenerator il1 = method1.GetILGenerator();

// public int Method1(int x, int y, short z, byte r) =>
// x + (z + 2 * (r + (8 * y + 3 * (y - (5 + x))))
il1.Emit(OpCodes.Ldarg_1); // push 1 MaxStack 1
il1.Emit(OpCodes.Ldarg_3); // push 1 MaxStack 2
il1.Emit(OpCodes.Ldc_I4_2); // push 1 MaxStack 3
il1.Emit(OpCodes.Ldarg_S, 4); // push 1 MaxStack 4
il1.Emit(OpCodes.Ldc_I4_8); // push 1 MaxStack 5
il1.Emit(OpCodes.Ldarg_2); // push 1 MaxStack 6
il1.Emit(OpCodes.Mul); // pop 2 push 1 MaxStack 5
il1.Emit(OpCodes.Ldc_I4_3); // push 1 MaxStack 6
il1.Emit(OpCodes.Ldarg_2); // push 1 MaxStack 7
il1.Emit(OpCodes.Ldc_I4_5); // push 1 MaxStack 8
il1.Emit(OpCodes.Ldarg_1); // push 1 MaxStack 9
il1.Emit(OpCodes.Add); // pop 2 push 1 stack size 8
il1.Emit(OpCodes.Sub); // pop 2 push 1 stack size 7
il1.Emit(OpCodes.Mul); // pop 2 push 1 stack size 6
il1.Emit(OpCodes.Add); // pop 2 push 1 stack size 5
il1.Emit(OpCodes.Add); // pop 2 push 1 stack size 4
il1.Emit(OpCodes.Mul); // pop 2 push 1 stack size 3
il1.Emit(OpCodes.Add); // pop 2 push 1 stack size 2
il1.Emit(OpCodes.Add); // pop 2 push 1 stack size 1
il1.Emit(OpCodes.Ret); // pop 1 stack size 0

MethodBuilder method2 = type.DefineMethod("Method2", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(byte) });
ILGenerator il2 = method2.GetILGenerator();

// int Method2(int x, int y) => x + (y + 18);
il2.Emit(OpCodes.Ldarg_1); // push 1 MaxStack 1
il2.Emit(OpCodes.Ldarg_2); // push 1 MaxStack 2
il2.Emit(OpCodes.Ldc_I4_S, 8); // push 1 MaxStack 3
il2.Emit(OpCodes.Add); // pop 2 push 1 stack size 2
il2.Emit(OpCodes.Add); // pop 2 push 1 stack size 1
il2.Emit(OpCodes.Ret); // pop 1 stack size 0

saveMethod.Invoke(ab, new object[] { file.Path });

MethodInfo getMaxStackSizeMethod = LoadILGenerator_GetMaxStackSizeMethod();
Assert.Equal(9, getMaxStackSizeMethod.Invoke(il1, new object[0]));
Assert.Equal(3, getMaxStackSizeMethod.Invoke(il2, new object[0]));

Assembly assemblyFromDisk = AssemblySaveTools.LoadAssemblyFromPath(file.Path);
Type typeFromDisk = assemblyFromDisk.Modules.First().GetType("MyType");
MethodBody body1 = typeFromDisk.GetMethod("Method1").GetMethodBody();
MethodBody body2 = typeFromDisk.GetMethod("Method2").GetMethodBody();
Assert.Equal(9, body1.MaxStackSize);
Assert.Equal(8, body2.MaxStackSize); // apparently doesn't write lower than 8
}
}

private MethodInfo LoadILGenerator_GetMaxStackSizeMethod()
{
Type ilgType = Type.GetType("System.Reflection.Emit.ILGeneratorImpl, System.Reflection.Emit", throwOnError: true)!;
return ilgType.GetMethod("GetMaxStackSize", BindingFlags.NonPublic | BindingFlags.Instance, Type.EmptyTypes);
}
}
}
}