diff --git a/README.md b/README.md index d89c274..080f02f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ 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. + +```csharp + +using Silhouette; + +internal partial class CorProfiler : 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; + } +} +``` + +You also need to expose 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. + +```csharp +using Silhouette; +using System.Runtime.InteropServices; + +internal class DllMain +{ + private static ClassFactory Instance; + + [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.E_NOINTERFACE; + } + + Instance = new ClassFactory(new CorProfiler()); + *ppv = Instance.IClassFactory; + + return 0; + } +} +``` + +`CorProfilerXxBase` offers base virtual methods for all `ICorProfilerCallback` methods, so override the ones you're interested in: + +```csharp + 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: + +```csharp + private unsafe string ResolveMethodName(nint ip) + { + try + { + var functionId = ICorProfilerInfo11.GetFunctionFromIP(ip).ThrowIfFailed(); + var functionInfo = ICorProfilerInfo2.GetFunctionInfo(functionId).ThrowIfFailed(); + using var metaDataImport = ICorProfilerInfo2.GetModuleMetaData(functionInfo.ModuleId, CorOpenFlags.ofRead, KnownGuids.IMetaDataImport).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 ""; + } + } +``` + +Most methods return an instance of `HResult`. 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`. + diff --git a/src/ManagedDotnetProfiler/CorProfiler.cs b/src/ManagedDotnetProfiler/CorProfiler.cs index c0590bc..c7820ed 100644 --- a/src/ManagedDotnetProfiler/CorProfiler.cs +++ b/src/ManagedDotnetProfiler/CorProfiler.cs @@ -677,7 +677,13 @@ private string GetTypeNameFromClassId(ClassId classId) private string GetFunctionFullName(FunctionId functionId) { - var functionInfo = ICorProfilerInfo2.GetFunctionInfo(functionId).ThrowIfFailed(); + var (result, functionInfo) = ICorProfilerInfo2.GetFunctionInfo(functionId); + + if (!result) + { + return $"Failed ({result})"; + } + var metaDataImport = ICorProfilerInfo2.GetModuleMetaData(functionInfo.ModuleId, CorOpenFlags.ofRead, KnownGuids.IMetaDataImport).ThrowIfFailed(); var methodProperties = metaDataImport.GetMethodProps(new MdMethodDef(functionInfo.Token)).ThrowIfFailed(); var typeDefProps = metaDataImport.GetTypeDefProps(methodProperties.Class).ThrowIfFailed(); diff --git a/src/ManagedDotnetProfiler/DllMain.cs b/src/ManagedDotnetProfiler/DllMain.cs index de60487..affbda3 100644 --- a/src/ManagedDotnetProfiler/DllMain.cs +++ b/src/ManagedDotnetProfiler/DllMain.cs @@ -1,4 +1,5 @@ -using System.Runtime.InteropServices; +using System; +using System.Runtime.InteropServices; using Silhouette; namespace ManagedDotnetProfiler; @@ -8,8 +9,13 @@ public class DllMain private static ClassFactory Instance; [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")] - public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv) + public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv) { + if (*rclsid != new Guid("0A96F866-D763-4099-8E4E-ED1801BE9FBC")) + { + return HResult.E_NOINTERFACE; + } + Instance = new ClassFactory(new CorProfiler()); *ppv = Instance.IClassFactory; diff --git a/src/TestApp/launch.cmd b/src/TestApp/launch.cmd index eb8149e..4626967 100644 --- a/src/TestApp/launch.cmd +++ b/src/TestApp/launch.cmd @@ -1,4 +1,4 @@ @set CORECLR_ENABLE_PROFILING=1 -@set CORECLR_PROFILER={846F5F1C-F9AE-4B07-969E-05C26BC060D8} -@set CORECLR_PROFILER_PATH=E:\git\ManagedDotnetProfiler\ManagedDotnetProfiler\bin\Release\net9.0\win-x64\publish\ManagedDotnetProfiler.dll +@set CORECLR_PROFILER={0A96F866-D763-4099-8E4E-ED1801BE9FBC} +@set CORECLR_PROFILER_PATH=..\..\..\..\ManagedDotnetProfiler\bin\Release\net9.0\win-x64\publish\ManagedDotnetProfiler.dll @TestApp.exe \ No newline at end of file