A prototype .NET profiler written in C# leveraging NativeAOT + ComWrappers.
Warning Work in progress
///
/// A sample profiler callback that prints the loaded modules
///
[ProfilerCallback("090B7720-6605-462B-86A0-C4D4C444D3F5")]
internal unsafe class MyProfiler : ICorProfilerCallback2
{
private ICorProfilerInfo4* _profilerInfo;
int ICorProfilerCallback.Initialize(CorProf.Bindings.IUnknown* unknown)
{
var guid_ = CorProfConsts.IID_ICorProfilerInfo4;
var hr = Marshal.QueryInterface((nint)unknown, ref guid_, out var pinfo);
if (hr < 0)
{
return HResult.E_FAIL;
}
var eventMask = COR_PRF_MONITOR.COR_PRF_MONITOR_MODULE_LOADS;
_profilerInfo = (ICorProfilerInfo4*)pinfo;
hr = _profilerInfo->SetEventMask((uint)eventMask);
if (hr < 0)
{
return HResult.E_FAIL;
}
}
int ICorProfilerCallback.ModuleLoadFinished(ulong moduleId, int hrStatus)
{
var pbBaseLoadAddr = (byte*)null;
uint pcchName = 0;
ulong assemblyId = 0;
var szName = (ushort*)NativeMemory.Alloc(sizeof(ushort) * 300);
var hr = _profilerInfo->GetModuleInfo(
moduleId,
&pbBaseLoadAddr,
300,
&pcchName,
szName,
&assemblyId);
if (hr < 0)
{
return HResult.E_FAIL;
}
var module = Marshal.PtrToStringUni((nint)szName);
Console.WriteLine($"Loaded Moudle -> '{module}'");
NativeMemory.Free(szName);
return HResult.S_OK;
}
int ICorProfilerCallback.Shutdown()
{
return HResult.S_OK;
}
}Sample
The sample produces a native DLL that can be loaded as a CLR Profiler.To test the concept, the example does a few basic things:
- Exposes
DllGetClassObjectDLL entry point - Implements
ICorProfilerCallback+ICorProfilerCallback2interfaces - hooks to
ICorProfilerCallback::ModuleLoadFinishedandICorProfilerInfo2::SetEnterLeaveFunctionHooks2 - logs the name of every loaded module
C:\ManagedCorProfiler\ManagedCorProfiler> .\run.cmd
DllMain(reason=DLL_PROCESS_ATTACH)
DllGetClassObject(reason=DLL_THREAD_ATTACH)
RCLSID = cf0d821e-299b-5307-a3d8-b283c03916dd
RIID = 00000001-0000-0000-c000-000000000046
MyProfiler!Initialize:::ICorProfilerCallback!Initialize()
Loaded Moudle -> 'C:\Users\user\..\runtime\artifacts\bin\coreclr\windows.x64.Debug\System.Private.CoreLib.dll'
Loaded Moudle -> 'C:\Users\user\..\SampleApp\bin\Debug\net7.0\SampleApp.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.runtime.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.console.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.threading.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.text.encoding.extensions.dll'
Loaded Moudle -> 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.0\system.runtime.interopservices.dll'
[... OMITTED ...]
=> GetPinnableReference()
=> get_Length()
=> WriteFile()
=> SetLastSystemError()
Hello World!
=> GetLastSystemError()
=> Flush()
=> Flush()
Hello World!
=> OnProcessExit()
[... OMITTED ...]
C:\ManagedCorProfiler\ManagedCorProfiler> █
output is actually interleaved, formatted and trimmed for clarity
not tested, just off the top of my head
- DNNE + managed assembly implementing the profiler itself
- A native (C++) library using
nethostto host an instance of the CRL + a managed library implementing the profiler - do not use
ComWrappersbut directly mess withUnmanagedCallersOnly, function pointers and co to manually implement COM ABI vtables.
C:\ManagedCorProfiler\ManagedCorProfiler> dumpbin.exe /EXPORTS bin\Debug\net7.0\win-x64\native\ManagedCorProfiler.dll
[...OMITTED FOR BREVITY...]
ordinal hint RVA name
1 0 00232660 DllCanUnloadNow = DllCanUnloadNow
2 1 002323A0 DllGetClassObject = DllGetClassObject
3 2 002326A0 DllMain = DllMain
4 3 00497430 DotNetRuntimeDebugHeader = DotNetRuntimeDebugHeader
[...OMITTED FOR BREVITY...]
C:\ManagedCorProfiler\ManagedCorProfiler> █
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.
- code contained in the
*.Bindingsprojects is generated using ClangSharpPInvokeGenerator - at this point, some manual tweaks are needed to make the generated bindings for an header compile (e.g fixing callconvs)
