Silhouette by Kevin Gosse
NuGet / site data
Details
Info
Name: Silhouette
A library to build .NET profilers in .NET. No need for C++ anymore, just C#.
Author: Kevin Gosse
NuGet: https://www.nuget.org/packages/Silhouette/
You can find more details at https://github.com/kevingosse/Silhouette
Author
Kevin Gosse

Original Readme
Silhouette - A library to build .NET profilers in .NET
Quick start
Create a new C# NativeAOT project. Reference the Silhouette nuget package and add a class inheriting from Silhouette.CorProfilerCallback11Base (you can use a different version of CorProfilerCallbackBase depending on the version of .NET you're targeting). Override the Initialize method. It will be called with the highest version number of ICorProfilerInfo supported by the target runtime.
using Silhouette;
[Profiler("0A96F866-D763-4099-8E4E-ED1801BE9FBC")] // Use your own profiler GUID here
internal partial class CorProfilerCallback : CorProfilerCallback11Base
{
protected override HResult Initialize(int iCorProfilerInfoVersion)
{
if (iCorProfilerInfoVersion < 11)
{
return HResult.E_FAIL;
}
var result = ICorProfilerInfo11.SetEventMask(COR_PRF_MONITOR.COR_PRF_ENABLE_STACK_SNAPSHOT | COR_PRF_MONITOR.COR_PRF_MONITOR_THREADS);
return result;
}
}
The Profiler attribute triggers a source-generator that emits the proper DllGetClassObject function and validates that the user is using the matching guid for the profiler. Alternatively, you can manually implement a DllGetClassObject method that will be called by the .NET runtime when initializing the profiler. Use the built-in ClassFactory implementation and give it an instance of your CorProfiler class.
using Silhouette;
using System.Runtime.InteropServices;
internal class DllMain
{
// This code is automatically generated when using the `[Profiler]` attribute on `CorProfilerCallback`
[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv)
{
// Use your own profiler GUID here
if (*rclsid != new Guid("0A96F866-D763-4099-8E4E-ED1801BE9FBC"))
{
return HResult.CORPROF_E_PROFILER_CANCEL_ACTIVATION;
}
*ppv = ClassFactory.For(new CorProfilerBase());
return HResult.S_OK;
}
}
CorProfilerXxBase offers base virtual methods for all ICorProfilerCallback methods, so override the ones you're interested in:
protected override HResult ThreadCreated(ThreadId threadId)
{
Console.WriteLine($"Thread created: {threadId.Value}");
return HResult.S_OK;
}
Use the ICorProfilerInfoXx fields to access the ICorProfilerInfo APIs:
private unsafe string ResolveMethodName(nint ip)
{
try
{
var functionId = ICorProfilerInfo11.GetFunctionFromIP(ip).ThrowIfFailed();
var functionInfo = ICorProfilerInfo2.GetFunctionInfo(functionId).ThrowIfFailed();
using var metaDataImport = ICorProfilerInfo2.GetModuleMetaDataImport(functionInfo.ModuleId, CorOpenFlags.ofRead).ThrowIfFailed().Wrap();
var methodProperties = metaDataImport.Value.GetMethodProps(new MdMethodDef(functionInfo.Token)).ThrowIfFailed();
var typeDefProps = metaDataImport.Value.GetTypeDefProps(methodProperties.Class).ThrowIfFailed();
return $"{typeDefProps.TypeName}.{methodProperties.Name}";
}
catch (Win32Exception)
{
return "<unknown>";
}
}
Most methods return an instance of HResult<T>. You can deconstruct it into a (HResult error, T result) and manually check the error code. You can also use the ThrowIfFailed() method that will return only the result and throw a Win32Exception if the error code is not S_OK.
About
Profiling .net applications
Measuring performance improvements
How to use
Example (source csproj, source files)
- CSharp Project
- Program.cs
- MyProfiler.cs
This is the CSharp Project that references Silhouette
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Silhouette" Version="3.2.0" />
</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>
This is the use of Silhouette in Program.cs
// See https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/
Console.WriteLine("Please read https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/");
This is the use of Silhouette in MyProfiler.cs
using Silhouette;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
namespace ProfilerDemo;
[Profiler("8AD62131-BF21-47C1-A4D4-3AEF5D7C75C6")]
internal class MyProfiler : CorProfilerCallback5Base
{
protected override HResult Initialize(int iCorProfilerInfoVersion)
{
Console.WriteLine("[SilhouetteProf] Initialize");
if (iCorProfilerInfoVersion < 5)
{
// we need at least ICorProfilerInfo5 and we got < 5
return HResult.E_FAIL;
}
// Call SetEventMask to tell the .NET runtime which events we're interested in
return ICorProfilerInfo5.SetEventMask(COR_PRF_MONITOR.COR_PRF_MONITOR_ALL);
}
protected override HResult ClassLoadStarted(ClassId classId)
{
try
{
ClassIdInfo classIdInfo = ICorProfilerInfo.GetClassIdInfo(classId).ThrowIfFailed();
using ComPtr<IMetaDataImport>? metaDataImport = ICorProfilerInfo2
.GetModuleMetaDataImport(classIdInfo.ModuleId, CorOpenFlags.ofRead)
.ThrowIfFailed()
.Wrap();
TypeDefPropsWithName classProps = metaDataImport.Value.GetTypeDefProps(classIdInfo.TypeDef).ThrowIfFailed();
Console.WriteLine($"[SilhouetteProf] ClassLoadStarted: {classProps.TypeName}");
return HResult.S_OK;
}
catch (Win32Exception ex)
{
Console.WriteLine($"[SilhouetteProf] ClassLoadStarted failed: {ex}");
return ex.NativeErrorCode;
}
}
protected override HResult Shutdown()
{
Console.WriteLine("[SilhouetteProf] Shutdown");
return HResult.S_OK;
}
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- silhouette.dllmain.g.cs
namespace Silhouette._Generated
{
using System;
using System.Runtime.InteropServices;
file static class DllMain
{
[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv)
{
if (*rclsid != new Guid("8ad62131-bf21-47c1-a4d4-3aef5d7c75c6"))
{
return HResult.CORPROF_E_PROFILER_CANCEL_ACTIVATION;
}
*ppv = ClassFactory.For(new global::ProfilerDemo.MyProfiler());
return HResult.S_OK;
}
}
}
Useful
Download Example (.NET C#)
Share Silhouette
https://ignatandrei.github.io/RSCG_Examples/v2/docs/Silhouette
Category "Profiler" has the following generators:
1 Silhouette
2025-12-16