Handle legacy VB delegate wrappers

VB can lower event-handler closures through generated wrapper methods, closure copy constructors, and immediately invoked VB anonymous delegate helpers. Recognizing and unwrapping those shapes leaves the expected lambda form and lets captured locals be scalar-replaced instead of exposing generated closure artifacts.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
This commit is contained in:
Siegfried Pammer
2026-10-08 17:52:13 +02:00
parent 2e296e7186
commit 565e1a3442
6 changed files with 202 additions and 17 deletions
@@ -0,0 +1,35 @@
using System;
using System.Runtime.CompilerServices;
public class Issue2199
{
private class Item
{
public string Text;
}
public event EventHandler Click;
public event EventHandler DoubleClick;
public void Register(string value)
{
Item item = new Item();
item.Text = value.Trim();
#if EXPECTED_OUTPUT
Click += [SpecialName] () => {
Console.WriteLine(item.Text);
};
DoubleClick += [SpecialName] () => {
Console.WriteLine(item.Text);
};
#else
Click += delegate {
Console.WriteLine(item.Text);
};
DoubleClick += delegate {
Console.WriteLine(item.Text);
};
#endif
}
}
@@ -0,0 +1,17 @@
Imports System
Public Class Issue2199
Public Event Click As EventHandler
Public Event DoubleClick As EventHandler
Private Class Item
Public Text As String
End Class
Public Sub Register(value As String)
Dim item As New Item()
item.Text = value.Trim()
AddHandler Click, Sub() Console.WriteLine(item.Text)
AddHandler DoubleClick, Sub() Console.WriteLine(item.Text)
End Sub
End Class
@@ -151,6 +151,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run(options: options | CompilerOptions.Library);
}
[Test]
public async Task Issue2199([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
await Run(options: options | CompilerOptions.Library);
}
[Test]
public async Task VBOnError([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
@@ -216,7 +222,7 @@ namespace ICSharpCode.Decompiler.Tests
settings ??= new DecompilerSettings { FileScopedNamespaces = false, SortCustomAttributes = true };
var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings).ConfigureAwait(false);
CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).ToArray());
CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).Append("EXPECTED_OUTPUT").ToArray());
Tester.RepeatOnIOError(() => File.Delete(decompiled));
}
}
@@ -478,6 +478,8 @@ namespace ICSharpCode.Decompiler.CSharp
return true;
if (settings.AnonymousTypes && type.IsAnonymousType(metadata))
return true;
if (settings.AnonymousMethods && type.IsDelegate(metadata) && name.StartsWith("VB$AnonymousDelegate_", StringComparison.Ordinal))
return true;
if (settings.Dynamic && type.IsDelegate(metadata) && (name.StartsWith("<>A", StringComparison.Ordinal) || name.StartsWith("<>F", StringComparison.Ordinal)))
return true;
}
@@ -16,6 +16,7 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
@@ -195,6 +196,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
var ilReader = context.CreateILReader();
var body = context.PEFile.GetMethodBody(methodDefinition.RelativeVirtualAddress);
var function = ilReader.ReadIL((MethodDefinitionHandle)targetMethod.MetadataToken, body, genericContext.Value, ILFunctionKind.Delegate, context.CancellationToken);
if (TryUnwrapVisualBasicDelegateWrapper(function, targetMethod, out var unwrappedMethod))
{
targetMethod = unwrappedMethod;
methodDefinition = context.PEFile.Metadata.GetMethodDefinition((MethodDefinitionHandle)targetMethod.MetadataToken);
if (!methodDefinition.HasBody())
return null;
genericContext = GenericContextFromTypeArguments(targetMethod.Substitution);
if (genericContext == null)
return null;
body = context.PEFile.GetMethodBody(methodDefinition.RelativeVirtualAddress);
function = ilReader.ReadIL((MethodDefinitionHandle)targetMethod.MetadataToken, body, genericContext.Value, ILFunctionKind.Delegate, context.CancellationToken);
}
function.DelegateType = delegateType;
// Embed the lambda into the parent function's ILAst, so that "Show steps" can show
// how the lambda body is being transformed.
@@ -215,6 +228,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
nestedContext.StepStartGroup("DelegateConstruction (nested lambdas)", function);
((IILTransform)this).Run(function, nestedContext);
nestedContext.StepEndGroup();
if (TryUnwrapTransformedVisualBasicDelegateWrapper(function, delegateType, out var unwrappedFunction))
{
function.ReplaceWith(unwrappedFunction);
function = unwrappedFunction;
}
function.AddILRange(target);
function.AddILRange(value);
if (value is Call call)
@@ -222,6 +240,64 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return function;
}
static bool TryUnwrapTransformedVisualBasicDelegateWrapper(ILFunction function, IType delegateType, out ILFunction unwrappedFunction)
{
unwrappedFunction = null;
if (function.Body is not BlockContainer container)
return false;
var block = container.EntryPoint;
if (block.Instructions.Count != 2 || !block.Instructions[1].MatchLeave(container, out _))
return false;
if (block.Instructions[0] is not CallVirt callVirt)
return false;
if (callVirt.Method.Name != "Invoke" || callVirt.Method.DeclaringType.Kind != TypeKind.Delegate)
return false;
if (callVirt.Arguments.Count != 1 || callVirt.Arguments[0] is not ILFunction innerFunction)
return false;
if (innerFunction.DelegateType?.Name.StartsWith("VB$AnonymousDelegate_", StringComparison.Ordinal) != true)
return false;
innerFunction.DelegateType = delegateType;
unwrappedFunction = innerFunction;
return true;
}
static bool TryUnwrapVisualBasicDelegateWrapper(ILFunction function, IMethod wrapperMethod, out IMethod unwrappedMethod)
{
unwrappedMethod = null;
if (!wrapperMethod.Name.Contains("$__R", StringComparison.Ordinal))
return false;
var thisVariable = function.Variables.SingleOrDefault(VariableKindExtensions.IsThis);
if (thisVariable == null || function.Body is not BlockContainer container)
return false;
var block = container.EntryPoint;
if (block.Instructions.Count != 2 || !block.Instructions[1].MatchLeave(container, out _))
return false;
if (block.Instructions[0] is Call call)
{
if (call.Arguments.Count != 1 || !call.Arguments[0].MatchLdLoc(thisVariable))
return false;
unwrappedMethod = call.Method;
}
else if (block.Instructions[0] is CallVirt callVirt)
{
if (callVirt.Method.Name != "Invoke" || callVirt.Method.DeclaringType.Kind != TypeKind.Delegate)
return false;
if (callVirt.Arguments.Count != 1 || !MatchDelegateConstruction(callVirt.Arguments[0], out unwrappedMethod, out var target, out _))
return false;
if (!target.MatchLdLoc(thisVariable))
return false;
}
else
{
return false;
}
if (!object.Equals(unwrappedMethod.DeclaringTypeDefinition, wrapperMethod.DeclaringTypeDefinition))
return false;
if (!IsAnonymousMethod(wrapperMethod.DeclaringTypeDefinition, unwrappedMethod))
return false;
return true;
}
private static bool ValidateDelegateTarget(ILInstruction inst)
{
switch (inst)
@@ -238,6 +314,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// TODO : ldfld chains must be validated more thoroughly, i.e., we should make sure
// that the value of the field is never changed.
ILInstruction target = ldobj;
// Match delegate targets stored in nested display classes, e.g.
// ldobj(ldflda field(ldobj(ldflda field(ldloc displayClass)))).
while (target is LdObj || target is LdFlda)
{
if (target is LdObj o)
@@ -297,6 +375,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
break;
case LdObj lo:
ILInstruction inner = lo.Target;
// Match the same display-class field chain after ValidateDelegateTarget
// accepted it: ldobj(ldflda field(ldflda field(ldloc displayClass))).
while (inner is LdFlda ldf)
{
inner = ldf.Target;
@@ -466,7 +466,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
try
{
if (method.Parameters.Count != 0)
if (method.Parameters.Count > 1)
return false;
var handle = (MethodDefinitionHandle)method.MetadataToken;
var module = (MetadataModule)method.ParentModule;
@@ -489,20 +489,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// IL_0000: ldarg.0
// IL_0001: call instance void [mscorlib]System.Object::.ctor()
// IL_0006: ret
var opCode = DecodeOpCodeSkipNop(ref reader);
switch (opCode)
{
case ILOpCode.Ldarg:
case ILOpCode.Ldarg_s:
if (reader.DecodeIndex(opCode) != 0)
return false;
break;
case ILOpCode.Ldarg_0:
// OK
break;
default:
return false;
}
if (!DecodeLdArg(ref reader, 0))
return false;
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Call)
return false;
var baseCtorHandle = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
@@ -513,7 +501,43 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!objectCtor.IsConstructor || objectCtor.Parameters.Count != 0)
return false;
return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret;
if (method.Parameters.Count == 0)
return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret;
if (!object.Equals(method.Parameters[0].Type.GetDefinition(), method.DeclaringTypeDefinition))
return false;
if (!DecodeLdArg(ref reader, 1))
return false;
switch (DecodeOpCodeSkipNop(ref reader))
{
case ILOpCode.Brfalse:
reader.ReadInt32();
break;
case ILOpCode.Brfalse_s:
reader.ReadSByte();
break;
default:
return false;
}
bool copiedField = false;
while (true)
{
var opCode = DecodeOpCodeSkipNop(ref reader);
if (opCode == ILOpCode.Ret)
return copiedField;
if (!DecodeLdArg(opCode, ref reader, 0))
return false;
if (!DecodeLdArg(ref reader, 1))
return false;
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Ldfld)
return false;
var sourceField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Stfld)
return false;
var targetField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
if (sourceField.IsNil || sourceField != targetField)
return false;
copiedField = true;
}
}
catch (BadImageFormatException)
{
@@ -521,6 +545,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
static bool DecodeLdArg(ref BlobReader reader, int expectedIndex)
{
return DecodeLdArg(DecodeOpCodeSkipNop(ref reader), ref reader, expectedIndex);
}
static bool DecodeLdArg(ILOpCode opCode, ref BlobReader reader, int expectedIndex)
{
switch (opCode)
{
case ILOpCode.Ldarg:
case ILOpCode.Ldarg_s:
return reader.DecodeIndex(opCode) == expectedIndex;
case ILOpCode.Ldarg_0:
return expectedIndex == 0;
case ILOpCode.Ldarg_1:
return expectedIndex == 1;
default:
return false;
}
}
static ILOpCode DecodeOpCodeSkipNop(ref BlobReader reader)
{
ILOpCode code;