Creating a .NET CLR profiler with C# NativeAOT and Silhouette
Andrew Lock demonstrates how to create a simple .NET CLR profiler using C# and NativeAOT with the Silhouette library, showing how to hook into assembly load events for custom profiling.
Creating a .NET CLR profiler with C# NativeAOT and Silhouette
Andrew Lock explores how to build a simple .NET profiler using C# rather than C++, leveraging the Silhouette library and NativeAOT compilation. The article starts by introducing profiling APIs within .NET, which are typically accessed via native C++ code, but here, Silhouette makes managed C# development practical.
Key Concepts
- .NET Profiling APIs: Three families—debugging APIs, metadata APIs, and profiling APIs. Profiling APIs are central for monitoring and instrumenting application behavior at runtime.
- NativeAOT: Allows .NET apps to compile to native binaries, a requirement for attaching as a CLR profiler.
- Silhouette Library: Simplifies exposing managed types as C++ interfaces and handles the boilerplate for the profiler DLL interface.
Project Setup
- Create Solution:
- Class library for the profiler (
SilhouetteProf) - Console app as test target (
TestApp) - Added Silhouette via NuGet to the profiler project
- Class library for the profiler (
- Modify Project Properties:
PublishAot=true,AllowUnsafeBlocks=true(NativeAOT requirements)
- Add Profiler Class:
- Implements
CorProfilerCallbackBasederivatives - Decorated with
[Profiler(GUID)]for registration
- Implements
Profiler Implementation
- Initialize Method: Checks interface compatibility and sets event mask for profiling events:
protected override HResult Initialize(int iCorProfilerInfoVersion) {
Console.WriteLine("[SilhouetteProf] Initialize");
if (iCorProfilerInfoVersion < 5) {
return HResult.E_FAIL;
}
return ICorProfilerInfo5.SetEventMask(COR_PRF_MONITOR.COR_PRF_MONITOR_ALL);
}
- Assembly Load Hook: Logs loaded assemblies using the profiling info API and catches exceptions:
protected override HResult AssemblyLoadFinished(AssemblyId assemblyId, HResult hrStatus) {
try {
AssemblyInfoWithName assemblyInfo = ICorProfilerInfo5.GetAssemblyInfo(assemblyId).ThrowIfFailed();
Console.WriteLine($"[SilhouetteProf] AssemblyLoadFinished: {assemblyInfo.AssemblyName}");
return HResult.S_OK;
} catch (Win32Exception ex) {
Console.WriteLine($"[SilhouetteProf] AssemblyLoadFinished failed: {ex}");
return ex.NativeErrorCode;
}
}
- Other Event Overrides: Additional events like shutdown, class load, etc., are implemented similarly.
Publishing and Testing
- Publish Projects:
- Test app published using
dotnet publishfor correct isolation - Profiler published with NativeAOT for target runtime (e.g., win-x64)
- Test app published using
- Set Environment Variables:
- Example for .NET Core:
CORECLR_ENABLE_PROFILING=1CORECLR_PROFILER={GUID}CORECLR_PROFILER_PATH=abs/path/to/profiler.dll
- Example for .NET Core:
- Run Application:
- Profiler logs assembly load events as .NET runtime executes the test app
Insights and Trade-Offs
- Silhouette makes prototyping and custom CLR profiling accessible in C#.
- Understanding profiling APIs is still necessary for advanced scenarios; library helps manage entrypoints and event hooks.
- NativeAOT and managed profilers are practical for proof-of-concept and developer-focused tools but may not yet be optimal for production-level diagnostics.
Summary
This guide demonstrates that CLR profiling can be approachable for C# developers. Silhouette streamlines interop with unmanaged APIs, and NativeAOT removes the typical C++ barrier. While this implementation is basic, it lays groundwork for deeper exploration and custom profilers tailored to .NET apps.
This post appeared first on “Andrew Lock’s Blog”. Read the entire article here