diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.targets b/src/BenchmarkDotNet/BenchmarkDotNet.targets
index 110a9cde82..62c85d5b99 100644
--- a/src/BenchmarkDotNet/BenchmarkDotNet.targets
+++ b/src/BenchmarkDotNet/BenchmarkDotNet.targets
@@ -25,6 +25,16 @@
Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone' AND '%(RuntimeTargetsCopyLocalItems.RuntimeIdentifier)' != '$(BenchmarkDotNetTargetPlatform)'" />
+
+
+
+ true
+
+
+
Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax)
{
const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm;
- using (CapstoneArm64Disassembler disassembler = CapstoneDisassembler.CreateArm64Disassembler(disassembleMode))
+ using (CapstoneArm64Disassembler disassembler = CapstoneFactory.CreateArm64Disassembler(disassembleMode))
{
// Enables disassemble details, which are disabled by default, to provide more detailed information on
// disassembled binary code.
diff --git a/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs b/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs
new file mode 100644
index 0000000000..f0d73557bc
--- /dev/null
+++ b/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs
@@ -0,0 +1,58 @@
+using Gee.External.Capstone;
+using Gee.External.Capstone.Arm64;
+#if !NET
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using System.Runtime.InteropServices;
+using RuntimeInformation = BenchmarkDotNet.Portability.RuntimeInformation;
+#endif
+
+namespace BenchmarkDotNet.Disassemblers;
+
+internal static class CapstoneFactory
+{
+ internal static CapstoneArm64Disassembler CreateArm64Disassembler(Arm64DisassembleMode disassembleMode)
+ {
+#if !NET
+ _ = FrameworkNativeLibrary.Handle.Value;
+#endif
+ return CapstoneDisassembler.CreateArm64Disassembler(disassembleMode);
+ }
+
+#if !NET
+ ///
+ /// .NET Framework does not probe runtimes/{rid}/native, where BenchmarkDotNet.targets copies the native capstone.
+ /// Once it is loaded by its full path, the P/Invokes that name the module bind to the loaded library.
+ /// When the file is not there, the default search applies - a build for a specific RuntimeIdentifier puts it next to the application.
+ ///
+ private static class FrameworkNativeLibrary
+ {
+ internal static readonly Lazy Handle = new(Load);
+
+ private static IntPtr Load()
+ {
+ if (!RuntimeInformation.IsFullFramework)
+ return IntPtr.Zero;
+
+ string? rid = RuntimeInformation.GetCurrentPlatform() switch
+ {
+ Platform.X86 => "win-x86",
+ Platform.X64 => "win-x64",
+ Platform.Arm64 => "win-arm64",
+ _ => null
+ };
+ var assembly = typeof(CapstoneDisassembler).Assembly;
+ string location = ShadowCopyHelper.TryGetOriginalLocation(assembly, out var originalLocation) ? originalLocation : assembly.Location;
+ if (rid is null || location.IsBlank())
+ return IntPtr.Zero;
+
+ string path = Path.Combine(Path.GetDirectoryName(location)!, "runtimes", rid, "native", "capstone.dll");
+ return File.Exists(path) ? LoadLibraryW(path) : IntPtr.Zero;
+ }
+
+ [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode)]
+ private static extern IntPtr LoadLibraryW(string lpLibFileName);
+ }
+#endif
+}
diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
index ec59b9bef8..cbc6b9406f 100644
--- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
+++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
@@ -142,7 +142,7 @@ public override Arm64AsmData Read(ref Utf8JsonReader reader, Type typeToConvert,
if (reader.TokenType != JsonTokenType.EndObject)
throw new JsonException("Invalid JSON");
- using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
+ using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm);
disassembler.EnableInstructionDetails = true;
disassembler.DisassembleSyntax = syntax;
var instruction = disassembler.Disassemble(instructionBytes, instructionAddress).SingleOrDefault();
diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
index 42959a5e2f..db140707a4 100644
--- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
+++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
@@ -121,12 +121,6 @@ public async IAsyncEnumerable ValidateAsync(ValidationParameter
yield break;
}
- if (currentPlatform == Platform.Arm64 && OsDetector.IsWindows())
- {
- yield return new ValidationError(true, $"{nameof(DisassemblyDiagnoser)} does not support Arm on Windows");
- yield break;
- }
-
if (Config.RunInHost && OsDetector.IsMacOS())
{
yield return new ValidationError(true, "Disassembling in the host process is not supported on MacOS");
diff --git a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs
index 86dbebf5c1..1dfdf87de6 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs
+++ b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs
@@ -1,4 +1,4 @@
-using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Detectors;
@@ -10,7 +10,6 @@
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Tests.Loggers;
-using BenchmarkDotNet.Tests.XUnit;
using BenchmarkDotNet.Toolchains;
using BenchmarkDotNet.Toolchains.Framework;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
@@ -32,6 +31,12 @@ public static IEnumerable