From a04c0e4cab1793d14246159f3e11ad69553b4a8c Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:42:39 -0800 Subject: [PATCH 1/2] Improve decompilation of unmanaged function pointers This improves how function pointers are decompiled. * ExpressionBuilder::VisitLdFtn now properly constructs the calling conventions. * FunctionPointerType::FromSignature now checks whether a modopt type affects the calling convention. --- .../TestCases/Pretty/FunctionPointers.cs | 133 +++++++++++++++++- .../CSharp/ExpressionBuilder.cs | 44 +++++- .../CSharp/Syntax/TypeSystemAstBuilder.cs | 2 +- .../TypeSystem/FunctionPointerType.cs | 22 ++- .../Implementation/KnownAttributes.cs | 2 + 5 files changed, 195 insertions(+), 8 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs index 93ca7236c..9ebcc0524 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs @@ -1,6 +1,6 @@ -#if !(CS110 && NET70) using System; -#endif +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -46,15 +46,142 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty return (delegate*)(&VarianceTest); } - public unsafe delegate* AddressOfLocalFunction() + public unsafe delegate* AddressOfLocalFunction_Managed() + { + return &LocalFunction; + + static void LocalFunction() + { + } + } + + public unsafe delegate* unmanaged AddressOfLocalFunction_Unmanaged() + { + return &LocalFunction; + + [UnmanagedCallersOnly] + static void LocalFunction() + { + } + } + + public unsafe delegate* unmanaged[Cdecl] AddressOfLocalFunction_CDecl() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + static void LocalFunction() + { + } + } + + public unsafe delegate* unmanaged[Fastcall] AddressOfLocalFunction_Fastcall() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvFastcall) })] + static void LocalFunction() + { + } + } + +#if NET60 + public unsafe delegate* unmanaged[MemberFunction] AddressOfLocalFunction_MemberFunction() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvMemberFunction) })] + static void LocalFunction() + { + } + } +#endif + + public unsafe delegate* unmanaged[Stdcall] AddressOfLocalFunction_Stdcall() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })] + static void LocalFunction() + { + } + } + +#if NET60 + public unsafe delegate* unmanaged[SuppressGCTransition] AddressOfLocalFunction_SuppressGCTransition() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvSuppressGCTransition) })] + static void LocalFunction() + { + } + } +#endif + + public unsafe delegate* unmanaged[Thiscall] AddressOfLocalFunction_Thiscall() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvThiscall) })] + static void LocalFunction() + { + } + } + +#if NET60 + public unsafe delegate* unmanaged[Cdecl, SuppressGCTransition] AddressOfLocalFunction_CDeclAndSuppressGCTransition() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvCdecl), + typeof(CallConvSuppressGCTransition) + })] + static void LocalFunction() + { + } + } + + public unsafe delegate* unmanaged[Fastcall, SuppressGCTransition] AddressOfLocalFunction_FastcallAndSuppressGCTransition() { return &LocalFunction; + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvFastcall), + typeof(CallConvSuppressGCTransition) + })] static void LocalFunction() { + } + } + + public unsafe delegate* unmanaged[Stdcall, SuppressGCTransition] AddressOfLocalFunction_StdcallAndSuppressGCTransition() + { + return &LocalFunction; + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvStdcall), + typeof(CallConvSuppressGCTransition) + })] + static void LocalFunction() + { } } + + public unsafe delegate* unmanaged[Thiscall, SuppressGCTransition] AddressOfLocalFunction_ThiscallAndSuppressGCTransition() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvThiscall), + typeof(CallConvSuppressGCTransition) + })] + static void LocalFunction() + { + } + } +#endif } internal class FunctionPointersWithCallingConvention diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 350a8287c..d4990a944 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4598,10 +4598,50 @@ namespace ICSharpCode.Decompiler.CSharp .WithILInstruction(inst); } // C# 9 function pointer + SignatureCallingConvention callingConvention = SignatureCallingConvention.Default; + ImmutableArray customCallingConventions; + var unmanagedCallersOnlyAttribute = inst.Method.GetAttributes().FirstOrDefault(m => m.AttributeType.IsKnownType(KnownAttribute.UnmanagedCallersOnly)); + if (unmanagedCallersOnlyAttribute != null) + { + callingConvention = SignatureCallingConvention.Unmanaged; + + var callingConventionsArgument = unmanagedCallersOnlyAttribute.NamedArguments.FirstOrDefault(a => a.Name == "CallConvs"); + if (callingConventionsArgument.Value is ImmutableArray> array) + { + var builder = ImmutableArray.CreateBuilder(array.Length); + foreach (var type in array.Select(a => a.Value).OfType()) + { + SignatureCallingConvention? foundCallingConvention = type.Namespace is not "System.Runtime.CompilerServices" ? null : type.Name switch { + "CallConvCdecl" => SignatureCallingConvention.CDecl, + "CallConvFastcall" => SignatureCallingConvention.FastCall, + "CallConvStdcall" => SignatureCallingConvention.StdCall, + "CallConvThiscall" => SignatureCallingConvention.ThisCall, + _ => null, + }; + if (foundCallingConvention is not null) + { + callingConvention = foundCallingConvention.Value; + } + else + { + builder.Add(type); + } + } + customCallingConventions = builder.ToImmutable(); + } + else + { + customCallingConventions = []; + } + } + else + { + callingConvention = SignatureCallingConvention.Default; + customCallingConventions = []; + } var ftp = new FunctionPointerType( typeSystem.MainModule, - // TODO: calling convention - SignatureCallingConvention.Default, ImmutableArray.Create(), + callingConvention, customCallingConventions, inst.Method.ReturnType, inst.Method.ReturnTypeIsRefReadOnly, inst.Method.Parameters.SelectImmutableArray(p => p.Type), inst.Method.Parameters.SelectImmutableArray(p => p.ReferenceKind) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index dae49f6d0..d8c29dbe7 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -368,7 +368,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax foreach (var customCallConv in fpt.CustomCallingConventions) { AstType callConvSyntax; - if (customCallConv.Name.StartsWith("CallConv", StringComparison.Ordinal) && customCallConv.Name.Length > 8) + if (customCallConv.Namespace == "System.Runtime.CompilerServices" && customCallConv.Name.StartsWith("CallConv", StringComparison.Ordinal) && customCallConv.Name.Length > 8) { callConvSyntax = new PrimitiveType(customCallConv.Name.Substring(8)); if (AddResolveResultAnnotations) diff --git a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs index 78500e6f4..79abcd92c 100644 --- a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs @@ -33,6 +33,7 @@ namespace ICSharpCode.Decompiler.TypeSystem { IType returnType = signature.ReturnType; bool returnIsRefReadOnly = false; + SignatureCallingConvention callingConvention = signature.Header.CallingConvention; var customCallConvs = ImmutableArray.CreateBuilder(); while (returnType is ModifiedType modReturn) { @@ -45,7 +46,24 @@ namespace ICSharpCode.Decompiler.TypeSystem && modReturn.Modifier.Namespace == "System.Runtime.CompilerServices") { returnType = modReturn.ElementType; - customCallConvs.Add(modReturn.Modifier); + switch (modReturn.Modifier.Name) + { + case "CallConvCdecl": + callingConvention = SignatureCallingConvention.CDecl; + break; + case "CallConvFastcall": + callingConvention = SignatureCallingConvention.FastCall; + break; + case "CallConvStdcall": + callingConvention = SignatureCallingConvention.StdCall; + break; + case "CallConvThiscall": + callingConvention = SignatureCallingConvention.ThisCall; + break; + default: + customCallConvs.Add(modReturn.Modifier); + break; + } } else { @@ -89,7 +107,7 @@ namespace ICSharpCode.Decompiler.TypeSystem parameterReferenceKinds.Add(kind); } return new FunctionPointerType( - module, signature.Header.CallingConvention, customCallConvs.ToImmutable(), + module, callingConvention, customCallConvs.ToImmutable(), returnType, returnIsRefReadOnly, parameterTypes.MoveToImmutable(), parameterReferenceKinds.MoveToImmutable()); } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 065864daf..52a20c337 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -110,6 +110,7 @@ namespace ICSharpCode.Decompiler.TypeSystem // C# 9 attributes: NativeInteger, PreserveBaseOverrides, + UnmanagedCallersOnly, // C# 11 attributes: Required, @@ -192,6 +193,7 @@ namespace ICSharpCode.Decompiler.TypeSystem // C# 9 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "NativeIntegerAttribute"), new TopLevelTypeName("System.Runtime.CompilerServices", "PreserveBaseOverridesAttribute"), + new TopLevelTypeName("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute"), // C# 11 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "RequiredMemberAttribute"), // C# 12 attributes: From 83df0ab9b9dfd74dd3440d5d27d544c88a7bc205 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Fri, 21 Nov 2025 19:08:09 -0800 Subject: [PATCH 2/2] Handle multiple calling conventions at once --- .../TestCases/Pretty/FunctionPointers.cs | 26 +++++++++++++ .../CSharp/ExpressionBuilder.cs | 2 +- .../TypeSystem/FunctionPointerType.cs | 39 +++++++++++-------- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs index 9ebcc0524..5379e4948 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs @@ -129,6 +129,32 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + public unsafe delegate* unmanaged[Cdecl, Fastcall] AddressOfLocalFunction_CDeclAndFastcall() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvCdecl), + typeof(CallConvFastcall) + })] + static void LocalFunction() + { + } + } + + public unsafe delegate* unmanaged[Fastcall, Cdecl] AddressOfLocalFunction_FastcallAndCDecl() + { + return &LocalFunction; + + [UnmanagedCallersOnly(CallConvs = new Type[] { + typeof(CallConvFastcall), + typeof(CallConvCdecl) + })] + static void LocalFunction() + { + } + } + #if NET60 public unsafe delegate* unmanaged[Cdecl, SuppressGCTransition] AddressOfLocalFunction_CDeclAndSuppressGCTransition() { diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index d4990a944..0e79d9b5d 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4611,7 +4611,7 @@ namespace ICSharpCode.Decompiler.CSharp var builder = ImmutableArray.CreateBuilder(array.Length); foreach (var type in array.Select(a => a.Value).OfType()) { - SignatureCallingConvention? foundCallingConvention = type.Namespace is not "System.Runtime.CompilerServices" ? null : type.Name switch { + SignatureCallingConvention? foundCallingConvention = type.Namespace is not "System.Runtime.CompilerServices" || callingConvention != SignatureCallingConvention.Unmanaged ? null : type.Name switch { "CallConvCdecl" => SignatureCallingConvention.CDecl, "CallConvFastcall" => SignatureCallingConvention.FastCall, "CallConvStdcall" => SignatureCallingConvention.StdCall, diff --git a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs index 79abcd92c..75fe0fd75 100644 --- a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs @@ -46,23 +46,30 @@ namespace ICSharpCode.Decompiler.TypeSystem && modReturn.Modifier.Namespace == "System.Runtime.CompilerServices") { returnType = modReturn.ElementType; - switch (modReturn.Modifier.Name) + if (callingConvention == SignatureCallingConvention.Unmanaged) { - case "CallConvCdecl": - callingConvention = SignatureCallingConvention.CDecl; - break; - case "CallConvFastcall": - callingConvention = SignatureCallingConvention.FastCall; - break; - case "CallConvStdcall": - callingConvention = SignatureCallingConvention.StdCall; - break; - case "CallConvThiscall": - callingConvention = SignatureCallingConvention.ThisCall; - break; - default: - customCallConvs.Add(modReturn.Modifier); - break; + switch (modReturn.Modifier.Name) + { + case "CallConvCdecl": + callingConvention = SignatureCallingConvention.CDecl; + break; + case "CallConvFastcall": + callingConvention = SignatureCallingConvention.FastCall; + break; + case "CallConvStdcall": + callingConvention = SignatureCallingConvention.StdCall; + break; + case "CallConvThiscall": + callingConvention = SignatureCallingConvention.ThisCall; + break; + default: + customCallConvs.Add(modReturn.Modifier); + break; + } + } + else + { + customCallConvs.Add(modReturn.Modifier); } } else