Skip to content

Commit 32697da

Browse files
timcassellclaude
andauthored
Fix and enable windows+arm disassembly (#3262)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3f9b853 commit 32697da

7 files changed

Lines changed: 84 additions & 18 deletions

File tree

‎src/BenchmarkDotNet/BenchmarkDotNet.targets‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone' AND '%(RuntimeTargetsCopyLocalItems.RuntimeIdentifier)' != '$(BenchmarkDotNetTargetPlatform)'" />
2626
</ItemGroup>
2727

28+
<!--
29+
The SDK copies `runtimes/{RuntimeIdentifier}` assets only for .NETCoreApp.
30+
Copy the native capstone for .NET Framework too, where the Arm64 disassembler loads it from that folder itself.
31+
-->
32+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
33+
<RuntimeTargetsCopyLocalItems Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone'">
34+
<CopyLocal>true</CopyLocal>
35+
</RuntimeTargetsCopyLocalItems>
36+
</ItemGroup>
37+
2838
<!-- Remove unnecessary satellite assemblies. -->
2939
<ItemGroup>
3040
<ResourceCopyLocalItems

‎src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ internal class Arm64Disassembler : ClrMdDisassembler
1111
protected override IEnumerable<Asm> Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax)
1212
{
1313
const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm;
14-
using (CapstoneArm64Disassembler disassembler = CapstoneDisassembler.CreateArm64Disassembler(disassembleMode))
14+
using (CapstoneArm64Disassembler disassembler = CapstoneFactory.CreateArm64Disassembler(disassembleMode))
1515
{
1616
// Enables disassemble details, which are disabled by default, to provide more detailed information on
1717
// disassembled binary code.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Gee.External.Capstone;
2+
using Gee.External.Capstone.Arm64;
3+
#if !NET
4+
using BenchmarkDotNet.Environments;
5+
using BenchmarkDotNet.Extensions;
6+
using BenchmarkDotNet.Helpers;
7+
using System.Runtime.InteropServices;
8+
using RuntimeInformation = BenchmarkDotNet.Portability.RuntimeInformation;
9+
#endif
10+
11+
namespace BenchmarkDotNet.Disassemblers;
12+
13+
internal static class CapstoneFactory
14+
{
15+
internal static CapstoneArm64Disassembler CreateArm64Disassembler(Arm64DisassembleMode disassembleMode)
16+
{
17+
#if !NET
18+
_ = FrameworkNativeLibrary.Handle.Value;
19+
#endif
20+
return CapstoneDisassembler.CreateArm64Disassembler(disassembleMode);
21+
}
22+
23+
#if !NET
24+
/// <summary>
25+
/// .NET Framework does not probe <c>runtimes/{rid}/native</c>, where BenchmarkDotNet.targets copies the native capstone.
26+
/// Once it is loaded by its full path, the P/Invokes that name the module bind to the loaded library.
27+
/// When the file is not there, the default search applies - a build for a specific RuntimeIdentifier puts it next to the application.
28+
/// </summary>
29+
private static class FrameworkNativeLibrary
30+
{
31+
internal static readonly Lazy<IntPtr> Handle = new(Load);
32+
33+
private static IntPtr Load()
34+
{
35+
if (!RuntimeInformation.IsFullFramework)
36+
return IntPtr.Zero;
37+
38+
string? rid = RuntimeInformation.GetCurrentPlatform() switch
39+
{
40+
Platform.X86 => "win-x86",
41+
Platform.X64 => "win-x64",
42+
Platform.Arm64 => "win-arm64",
43+
_ => null
44+
};
45+
var assembly = typeof(CapstoneDisassembler).Assembly;
46+
string location = ShadowCopyHelper.TryGetOriginalLocation(assembly, out var originalLocation) ? originalLocation : assembly.Location;
47+
if (rid is null || location.IsBlank())
48+
return IntPtr.Zero;
49+
50+
string path = Path.Combine(Path.GetDirectoryName(location)!, "runtimes", rid, "native", "capstone.dll");
51+
return File.Exists(path) ? LoadLibraryW(path) : IntPtr.Zero;
52+
}
53+
54+
[DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode)]
55+
private static extern IntPtr LoadLibraryW(string lpLibFileName);
56+
}
57+
#endif
58+
}

‎src/BenchmarkDotNet/Disassemblers/DataContracts.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public override Arm64AsmData Read(ref Utf8JsonReader reader, Type typeToConvert,
142142
if (reader.TokenType != JsonTokenType.EndObject)
143143
throw new JsonException("Invalid JSON");
144144

145-
using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
145+
using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
146146
disassembler.EnableInstructionDetails = true;
147147
disassembler.DisassembleSyntax = syntax;
148148
var instruction = disassembler.Disassemble(instructionBytes, instructionAddress).SingleOrDefault();

‎src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,6 @@ public async IAsyncEnumerable<ValidationError> ValidateAsync(ValidationParameter
121121
yield break;
122122
}
123123

124-
if (currentPlatform == Platform.Arm64 && OsDetector.IsWindows())
125-
{
126-
yield return new ValidationError(true, $"{nameof(DisassemblyDiagnoser)} does not support Arm on Windows");
127-
yield break;
128-
}
129-
130124
if (Config.RunInHost && OsDetector.IsMacOS())
131125
{
132126
yield return new ValidationError(true, "Disassembling in the host process is not supported on MacOS");

‎tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using BenchmarkDotNet.Attributes;
1+
using BenchmarkDotNet.Attributes;
22
using BenchmarkDotNet.Columns;
33
using BenchmarkDotNet.Configs;
44
using BenchmarkDotNet.Detectors;
@@ -10,7 +10,6 @@
1010
using BenchmarkDotNet.Jobs;
1111
using BenchmarkDotNet.Portability;
1212
using BenchmarkDotNet.Tests.Loggers;
13-
using BenchmarkDotNet.Tests.XUnit;
1413
using BenchmarkDotNet.Toolchains;
1514
using BenchmarkDotNet.Toolchains.Framework;
1615
using BenchmarkDotNet.Toolchains.InProcess.Emit;
@@ -32,6 +31,12 @@ public static IEnumerable<object[]> GetAllJits()
3231

3332
if (RuntimeInformation.IsFullFramework)
3433
{
34+
if (RuntimeInformation.GetCurrentPlatform() is Platform.Arm64)
35+
{
36+
// RyuJit for desktop .NET arm64. Supported only on net481, but we have to match the tfm in our test project.
37+
yield return [Jit.RyuJit, Platform.Arm64, CsProjFrameworkToolchain.Net472];
38+
}
39+
// Framework on arm emulates x86, so these platform targets should work on both.
3540
yield return [Jit.LegacyJit, Platform.X86, CsProjFrameworkToolchain.Net472]; // 32bit LegacyJit for desktop .NET
3641
yield return [Jit.LegacyJit, Platform.X64, CsProjFrameworkToolchain.Net472]; // 64bit LegacyJit for desktop .NET
3742
yield return [Jit.RyuJit, Platform.X64, CsProjFrameworkToolchain.Net472]; // RyuJit for desktop .NET
@@ -87,7 +92,7 @@ public void Recursive()
8792
[MethodImpl(MethodImplOptions.NoInlining)] public void Benchmark(bool justAnOverload) { } // we need to test overloads (#562)
8893
}
8994

90-
[TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)]
95+
[Theory]
9196
[MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)]
9297
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
9398
public void CanDisassembleAllMethodCalls(Jit jit, Platform platform, IToolchain toolchain)
@@ -107,7 +112,7 @@ public void CanDisassembleAllMethodCalls(Jit jit, Platform platform, IToolchain
107112
AssertDisassemblyResult(result, $"{nameof(WithCalls.Recursive)}()");
108113
}
109114

110-
[TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)]
115+
[Theory]
111116
[MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)]
112117
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
113118
public void CanDisassembleAllMethodCallsUsingFilters(Jit jit, Platform platform, IToolchain toolchain)
@@ -133,7 +138,7 @@ public void CanDisassembleAllMethodCallsUsingFilters(Jit jit, Platform platform,
133138
public T Create() => new T();
134139
}
135140

136-
[TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)]
141+
[Theory]
137142
[MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)]
138143
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
139144
public void CanDisassembleGenericTypes(Jit jit, Platform platform, IToolchain toolchain)
@@ -154,7 +159,7 @@ public class WithInlineable
154159
[Benchmark] public void JustReturn() { }
155160
}
156161

157-
[TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)]
162+
[Theory]
158163
[MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)]
159164
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
160165
public void CanDisassembleInlinableBenchmarks(Jit jit, Platform platform, IToolchain toolchain)
@@ -188,7 +193,7 @@ public class WithGeneratedMemberNames
188193
[MethodImpl(MethodImplOptions.NoInlining)] public virtual void ForDisassemblyDiagnoser() { }
189194
}
190195

191-
[TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)]
196+
[Theory]
192197
[MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)]
193198
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
194199
public void CanDisassembleWhenBenchmarkDeclaresGeneratedMemberNames(Jit jit, Platform platform, IToolchain toolchain)

‎tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using AwesomeAssertions;
22
using BenchmarkDotNet.Disassemblers;
33
using BenchmarkDotNet.Serialization;
4-
using BenchmarkDotNet.Tests.XUnit;
54
using Gee.External.Capstone;
65
using Gee.External.Capstone.Arm64;
76
using Iced.Intel;
@@ -105,15 +104,15 @@ public void IntelAsmSerializationTest()
105104
Assert.Equivalent(model, result, strict: true);
106105
}
107106

108-
[FactEnvSpecific("ARM64 disassembler is not supported on .NET Framework or Windows+Arm environment", EnvRequirement.NonFullFramework, EnvRequirement.NonWindowsArm)]
107+
[Fact]
109108
public void Arm64AsmSerializationTest()
110109
{
111110
// Arrange
112111
byte[] instructionBytes = [0xE1, 0x0B, 0x40, 0xB9]; // ldr w1, [sp, #8]
113112
var disassembleSyntax = DisassembleSyntax.Intel;
114113

115114
// Create instruction instance by using disassembler.
116-
using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
115+
using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
117116
disassembler.EnableInstructionDetails = true;
118117
disassembler.DisassembleSyntax = disassembleSyntax;
119118

0 commit comments

Comments
 (0)