A prototype CLR profiler written in C# for learning and fun.
Warning
🚧 WIP WIP WIP 🚧
- NativeAOT native compilation to build and link the native profiler library.
- ComWrappers API to expose the
ICorProfilerCallbackX
to the CLR. - CsWin32 to generate C# bindings for the native Profiling API.
///
/// A sample profiler callback that prints the loaded modules
///
[ProfilerCallback("1E040027-162F-489B-B12F-F113E6AF40CF")]
internal unsafe class MyProfiler : CorProfilerCallback2
{
private ICorProfilerInfo2* _corProfilerInfo;
public override HRESULT Initialize(IUnknown* pICorProfilerInfoUnk)
{
// Query for a pointer to the ICorProfilerCallback2 interface
var hr = pICorProfilerInfoUnk->QueryInterface(ICorProfilerInfo2.IID_Guid, out var pinfo);
if (hr.Failed)
{
Console.WriteLine($"FAIL QueryInterface hr={hr}");
return HRESULT.E_FAIL;
}
// Track our reference
_corProfilerInfo = (ICorProfilerInfo2*)pinfo;
_corProfilerInfo->AddRef();
// Specify our profiler is interested in module load events (Module* callbacks)
hr = _corProfilerInfo->SetEventMask((uint)COR_PRF_MONITOR.COR_PRF_MONITOR_MODULE_LOADS);
if (hr.Failed)
{
Console.WriteLine($"FAIL SetEventMask hr={hr}");
return HRESULT.E_FAIL;
}
return HRESULT.S_OK;
}
public override HRESULT ModuleLoadFinished(nuint moduleId, HRESULT hrStatus)
{
if (hrStatus.Failed)
{
return HRESULT.S_OK;
}
const int NameBufferLength = 256; // char
// Allocate a buffer to hold the module name.
using var szNameBuffer = NativeBuffer<char>.Alloc(NameBufferLength);
// A pointer to a string of wide-characters to pass in input to GetModuleInfo.
var szName = new PWSTR(szNameBuffer.Pointer);
uint pcchName = 0;
// Retrieve the file name of the module
var hr = _corProfilerInfo->GetModuleInfo(
moduleId, // the target moduleId
null,
NameBufferLength, // The length, in characters, of the szName return buffer
&pcchName, // A pointer to the total character length of the module's file name that is returned
szName, // A caller-provided wide character buffer
null);
if (hr.Failed)
{
Console.WriteLine($"FAIL GetModuleInfo hr={hr}");
return hr;
}
var moduleName = szName.CopyToString(length: (int)pcchName);
Console.WriteLine($"loaded module 0x{moduleId:x8} <{moduleName}>");
return HRESULT.S_OK;
}
public override HRESULT Shutdown()
{
_corProfilerInfo->Release();
return HRESULT.S_OK;
}
}
C:\ManagedCorProfiler\Samples\ModuleLoadsProfiler> .\run.cmd [... OMITTED ...] Loaded Module -> 0x7ffeac1b4000 C:\Users\dev\Source\Repos\runtime\artifacts\bin\coreclr\windows.x64.Debug\System.Private.CoreLib.dll Loaded Module -> 0x7ffeac702148 C:\ManagedCorProfiler\Samples\SampleApp\bin\Debug\net8.0\SampleApp.dll Loaded Module -> 0x7ffeac703e40 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\system.runtime.dll Loaded Module -> 0x7ffeac8f9798 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\system.console.dll Loaded Module -> 0x7ffeac8fc1f0 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\system.threading.dll Loaded Module -> 0x7ffeac9317d0 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\system.text.encoding.extensions.dll Loaded Module -> 0x7ffeac9389a0 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\system.runtime.interopservices.dll Hello World! C:\ManagedCorProfiler\Samples\ModuleLoadsProfiler> █
C:\ManagedCorProfiler\Samples\ModuleLoadsProfiler> dumpbin.exe /EXPORTS bin\Release\net8.0\publish\win-x64\ModuleLoadsProfiler.dll [...OMITTED FOR BREVITY...] ordinal hint RVA name 1 0 00232660 DllCanUnloadNow = DllCanUnloadNow 2 1 002323A0 DllGetClassObject = DllGetClassObject 3 2 002326A0 DllMain = DllMain [...OMITTED FOR BREVITY...] C:\ManagedCorProfiler\Samples\ModuleLoadsProfiler> █
.
.
.
Any contribution is welcome. I'm actually working on porting the tests at dotnet/runtime/tree/main/src/tests/profiler, this is a good place to start contributing.