├── src ├── Silhouette │ ├── AssemblyInfo.cs │ ├── Interfaces │ │ ├── IMethodMalloc.cs │ │ ├── IUnknown.cs │ │ ├── ICorProfilerCallback11.cs │ │ ├── IClassFactory.cs │ │ ├── INativeEnumerator.cs │ │ ├── ICorProfilerModuleEnum.cs │ │ ├── ICorProfilerCallback9.cs │ │ ├── ICorProfilerCallback3.cs │ │ ├── ICorProfilerInfo13.cs │ │ ├── ICorProfilerFunctionControl.cs │ │ ├── ICorProfilerCallback10.cs │ │ ├── ICorProfilerCallback7.cs │ │ ├── ICorProfilerInfo9.cs │ │ ├── ICorProfilerCallback8.cs │ │ ├── ICorProfilerInfo11.cs │ │ ├── ICorProfilerInfo5.cs │ │ ├── ICorProfilerCallback5.cs │ │ ├── ICorProfilerInfo6.cs │ │ ├── ICorProfilerInfo10.cs │ │ ├── ICorProfilerInfo12.cs │ │ ├── ICorProfilerInfo15.cs │ │ ├── ICorProfilerInfo14.cs │ │ ├── ICorProfilerInfo8.cs │ │ ├── ICorProfilerInfo7.cs │ │ ├── ICorProfilerCallback6.cs │ │ ├── IMetaDataEmit2.cs │ │ ├── IMetaDataImport2.cs │ │ ├── ICorProfilerInfo4.cs │ │ ├── ICorProfilerCallback.cs │ │ ├── ICorProfilerCallback4.cs │ │ ├── ICorProfilerInfo3.cs │ │ └── ICorProfilerCallback2.cs │ ├── ICorProfilerInfoFactory.cs │ ├── Silhouette.csproj.DotSettings │ ├── Extensions.cs │ ├── ProfilerAttribute.cs │ ├── IMethodMalloc.cs │ ├── Unknown.cs │ ├── ComPtr.cs │ ├── ICorProfilerInfo5.cs │ ├── ICorProfilerInfo6.cs │ ├── CorProfilerCallback9Base.cs │ ├── CorProfilerCallback7Base.cs │ ├── ICorProfilerInfo13.cs │ ├── CorProfilerCallback6Base.cs │ ├── CorProfilerCallback11Base.cs │ ├── CorProfilerCallback5Base.cs │ ├── ICorProfilerInfo7.cs │ ├── ICorProfilerInfo9.cs │ ├── ICorProfilerInfo11.cs │ ├── CorProfilerCallback3Base.cs │ ├── ICorProfilerFunctionControl.cs │ ├── CorProfilerCallback8Base.cs │ ├── INativeEnumerator.cs │ ├── ClassFactory.cs │ ├── BufferedNativeEnumerable.cs │ ├── CorProfilerCallback10Base.cs │ ├── ICorProfilerInfo10.cs │ ├── ICorProfilerInfo8.cs │ ├── ICorProfilerInfo15.cs │ ├── Silhouette.csproj │ ├── IMetaDataEmit2.cs │ ├── ICorProfilerInfo14.cs │ ├── CorProfilerCallback4Base.cs │ ├── ICorProfilerInfo12.cs │ ├── ICorProfilerInfo4.cs │ ├── CorProfilerCallback2Base.cs │ ├── IMetaDataImport2.cs │ ├── ICorProfilerInfo3.cs │ └── ICorProfilerInfo2.cs ├── TestApp │ ├── ITest.cs │ ├── launch.cmd │ ├── ComTests.cs │ ├── ConditionalWeakTableTests.cs │ ├── PInvokeTests.cs │ ├── NgenTests.cs │ ├── HandleTests.cs │ ├── launch.sh │ ├── NativeMethods.cs │ ├── TestApp.csproj │ ├── FinalizationTests.cs │ ├── GarbageCollectionTests.cs │ ├── ClassLoadTests.cs │ ├── AssemblyLoadContextTests.cs │ ├── DynamicMethodTests.cs │ ├── ProfilerPInvokes.cs │ ├── JitCompilationTests.cs │ ├── IlRewriteTest.cs │ ├── GenericArgumentsTests.cs │ ├── ModuleTests.cs │ ├── Logs.cs │ ├── Program.cs │ ├── ThreadTests.cs │ └── ExceptionTests.cs ├── ManagedDotnetProfiler │ ├── publish.sh │ ├── publish.cmd │ ├── NativeMethods.cs │ ├── ManagedDotnetProfiler.csproj │ ├── NativeMethods.Windows.cs │ ├── NativeMethods.Linux.cs │ └── PInvoke.cs ├── Silhouette.sln.DotSettings ├── Silhouette.slnx ├── Silhouette.SourceGenerator │ ├── Silhouette.SourceGenerator.csproj │ └── ProfilerAttributeSourceGenerator.cs └── Silhouette.IL │ ├── Silhouette.IL.csproj │ ├── IlRewriter.cs │ └── CorLibTypes.cs ├── qodana.yaml ├── test.sh ├── test.ps1 ├── .github └── workflows │ ├── ci.yml │ └── qodana_code_quality.yml ├── .gitattributes ├── README.md └── .gitignore /src/Silhouette/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/TestApp/ITest.cs: -------------------------------------------------------------------------------- 1 | namespace TestApp; 2 | 3 | internal interface ITest 4 | { 5 | void Run(); 6 | } 7 | -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/publish.sh: -------------------------------------------------------------------------------- 1 | dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r linux-x64 -c Release -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/publish.cmd: -------------------------------------------------------------------------------- 1 | dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c Release 2 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/IMethodMalloc.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public interface IMethodMalloc : IUnknown 5 | { 6 | IntPtr Alloc(uint size); 7 | } 8 | -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfoFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | internal interface ICorProfilerInfoFactory 4 | { 5 | static abstract T Create(nint ptr); 6 | static abstract Guid Guid { get; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/IUnknown.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public interface IUnknown 5 | { 6 | HResult QueryInterface(in Guid guid, out nint ptr); 7 | int AddRef(); 8 | int Release(); 9 | } -------------------------------------------------------------------------------- /src/TestApp/launch.cmd: -------------------------------------------------------------------------------- 1 | @set CORECLR_ENABLE_PROFILING=1 2 | @set CORECLR_PROFILER={0A96F866-D763-4099-8E4E-ED1801BE9FBC} 3 | @set CORECLR_PROFILER_PATH=..\..\..\..\ManagedDotnetProfiler\bin\Release\net9.0\win-x64\publish\ManagedDotnetProfiler.dll 4 | @TestApp.exe -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback11.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerCallback11 : ICorProfilerCallback10 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("42350846-AAED-47F7-B128-FD0C98881CDE"); 7 | 8 | HResult LoadAsNotificationOnly(out int pbNotificationOnly); 9 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/IClassFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public interface IClassFactory : IUnknown 5 | { 6 | public static readonly Guid Guid = new("00000001-0000-0000-C000-000000000046"); 7 | 8 | HResult CreateInstance(nint outer, in Guid guid, out nint instance); 9 | 10 | HResult LockServer(bool @lock); 11 | 12 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/INativeEnumerator.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface INativeEnumerator : IUnknown 5 | { 6 | HResult Skip(uint count); 7 | HResult Reset(); 8 | HResult Clone(out IntPtr clone); 9 | HResult GetCount(out uint count); 10 | HResult Next(uint count, void* items, out uint fetched); 11 | } 12 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerModuleEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public unsafe interface ICorProfilerModuleEnum : IUnknown 5 | { 6 | HResult Skip(uint celt); 7 | 8 | HResult Reset(); 9 | 10 | HResult Clone(out void* ppEnum); 11 | 12 | HResult GetCount(out uint pcelt); 13 | 14 | HResult Next(uint celt, ModuleId* ids, out uint pceltFetched); 15 | } -------------------------------------------------------------------------------- /src/Silhouette.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | GC -------------------------------------------------------------------------------- /src/Silhouette/Silhouette.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | InternalsOnly -------------------------------------------------------------------------------- /src/Silhouette/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public static class Extensions 4 | { 5 | internal static string WithoutNullTerminator(this Span buffer) 6 | { 7 | if (buffer.Length == 0) 8 | { 9 | return string.Empty; 10 | } 11 | 12 | if (buffer[^1] == '\0') 13 | { 14 | return new string(buffer[..^1]); 15 | } 16 | 17 | return new string(buffer); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback9.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerCallback9 : ICorProfilerCallback8 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("27583EC3-C8F5-482F-8052-194B8CE4705A"); 7 | 8 | // This event is triggered whenever a dynamic method is garbage collected 9 | // and subsequently unloaded. 10 | HResult DynamicMethodUnloaded(FunctionId functionId); 11 | } -------------------------------------------------------------------------------- /qodana.yaml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | linter: 'jetbrains/qodana-dotnet:2025.1' 3 | profile: 4 | name: qodana.recommended 5 | include: 6 | - name: CheckDependencyLicenses 7 | exclude: 8 | - name: ArrangeObjectCreationWhenTypeNotEvident 9 | - name: SuggestVarOrType_BuiltInTypes 10 | - name: ConvertToPrimaryConstructor 11 | - name: SYSLIB1054 12 | - name: ConvertIfStatementToReturnStatement 13 | - name: InvertIf 14 | - name: PreferConcreteValueOverDefault 15 | - name: CA1816 16 | -------------------------------------------------------------------------------- /src/TestApp/ComTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Runtime.Versioning; 3 | 4 | namespace TestApp; 5 | 6 | internal class ComTests : ITest 7 | { 8 | [SupportedOSPlatform("windows")] 9 | public void Run() 10 | { 11 | _ = Marshal.GetIUnknownForObject(new object()); 12 | 13 | var logs = Logs.Fetch().ToList(); 14 | Logs.AssertContains(logs, "COMClassicVTableCreated - System.Object - fbec27f0-fc44-396c-8a8c-4c1993516f2d - 11"); 15 | } 16 | } -------------------------------------------------------------------------------- /src/TestApp/ConditionalWeakTableTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace TestApp; 4 | 5 | internal class ConditionalWeakTableTests : ITest 6 | { 7 | public void Run() 8 | { 9 | _ = new ConditionalWeakTable { { "hello", "world" } }; 10 | GC.Collect(2, GCCollectionMode.Forced, true); 11 | 12 | var logs = Logs.Fetch().ToList(); 13 | Logs.AssertContains(logs, "ConditionalWeakTableElementReferences - hello -> world"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback3.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerCallback3 : ICorProfilerCallback2 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("4FD2ED52-7731-4b8d-9469-03D2CC3086C5"); 7 | HResult InitializeForAttach( 8 | nint pCorProfilerInfoUnk, 9 | nint pvClientData, 10 | uint cbClientData); 11 | 12 | HResult ProfilerAttachComplete(); 13 | 14 | HResult ProfilerDetachSucceeded(); 15 | } -------------------------------------------------------------------------------- /src/TestApp/PInvokeTests.cs: -------------------------------------------------------------------------------- 1 | namespace TestApp; 2 | 3 | internal class PInvokeTests : ITest 4 | { 5 | public void Run() 6 | { 7 | _ = ProfilerPInvokes.CountFrozenObjects(); 8 | 9 | var logs = Logs.Fetch().ToList(); 10 | Logs.AssertContains(logs, "ManagedToUnmanagedTransition - TestApp.ProfilerPInvokes.CountFrozenObjects - COR_PRF_TRANSITION_CALL"); 11 | Logs.AssertContains(logs, "UnmanagedToManagedTransition - TestApp.ProfilerPInvokes.CountFrozenObjects - COR_PRF_TRANSITION_RETURN"); 12 | } 13 | } -------------------------------------------------------------------------------- /src/TestApp/NgenTests.cs: -------------------------------------------------------------------------------- 1 | namespace TestApp; 2 | 3 | internal class NgenTests : ITest 4 | { 5 | public void Run() 6 | { 7 | var allLogs = Logs.All.ToList(); 8 | 9 | Logs.AssertContains(allLogs, "JITCachedFunctionSearchStarted - System.String.ToString"); 10 | Logs.AssertContains(allLogs, "JITCachedFunctionSearchFinished - System.String.ToString - Found"); 11 | Logs.AssertContains(allLogs, "JITCachedFunctionSearchFinished - System.Text.StringBuilder.AppendFormat inlined System.String.ToString"); 12 | } 13 | } -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | PROFILER_DLL=$(realpath ./src/ManagedDotnetProfiler/bin/Release/net9.0/linux-x64/native/ManagedDotnetProfiler.so) 5 | 6 | export LD_LIBRARY_PATH=$(realpath ./src/ManagedDotnetProfiler/bin/Release/net9.0/linux-x64/native/):$LD_LIBRARY_PATH 7 | 8 | export CORECLR_ENABLE_PROFILING=1 9 | export CORECLR_PROFILER="{0A96F866-D763-4099-8E4E-ED1801BE9FBC}" 10 | export CORECLR_PROFILER_PATH="$PROFILER_DLL" 11 | 12 | echo "Running TestApp with profiler..." 13 | ./src/TestApp/bin/Release/net9.0/TestApp 14 | exit $? 15 | -------------------------------------------------------------------------------- /src/Silhouette.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Silhouette.SourceGenerator/Silhouette.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | Silhouette.SourceGenerator 7 | latest 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo13.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerInfo13 : ICorProfilerInfo12 5 | { 6 | public new static readonly Guid Guid = new("6E6C7EE2-0701-4EC2-9D29-2E8733B66934"); 7 | 8 | HResult CreateHandle( 9 | ObjectId @object, 10 | COR_PRF_HANDLE_TYPE type, 11 | out ObjectHandleId pHandle); 12 | 13 | HResult DestroyHandle( 14 | ObjectHandleId handle); 15 | 16 | HResult GetObjectIDFromHandle( 17 | ObjectHandleId handle, 18 | out ObjectId pObject); 19 | } -------------------------------------------------------------------------------- /src/TestApp/HandleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace TestApp; 4 | 5 | internal class HandleTests : ITest 6 | { 7 | public void Run() 8 | { 9 | var obj = new object(); 10 | 11 | var handle = GCHandle.Alloc(obj, GCHandleType.Normal); 12 | 13 | var handleAddress = GCHandle.ToIntPtr(handle); 14 | 15 | handle.Free(); 16 | 17 | var logs = Logs.Fetch().ToList(); 18 | 19 | Logs.AssertContains(logs, $"HandleCreated - {handleAddress:x2} - System.Object"); 20 | Logs.AssertContains(logs, $"HandleDestroyed - {handleAddress:x2}"); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedDotnetProfiler; 4 | 5 | internal static unsafe partial class NativeMethods 6 | { 7 | internal static string GetCurrentModulePath() 8 | { 9 | var address = (IntPtr)(delegate*)&GetCurrentModulePath; 10 | 11 | if (OperatingSystem.IsWindows()) 12 | { 13 | return GetModulePathWindows(address); 14 | } 15 | 16 | if (OperatingSystem.IsLinux()) 17 | { 18 | return GetModulePathLinux(address); 19 | } 20 | 21 | throw new PlatformNotSupportedException(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Silhouette/ProfilerAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | /// 4 | /// Marks a class for source generation of the DllGetClassObject export. 5 | /// The class is expected to inherit from CorProfilerCallbackBase. 6 | /// 7 | /// 8 | /// Usage example: 9 | /// 10 | /// [Profiler("12345678-1234-1234-1234-123456789abc")] 11 | /// public class MyProfiler : CorProfilerCallback11Base { } 12 | /// 13 | /// 14 | [AttributeUsage(AttributeTargets.Class)] 15 | public class ProfilerAttribute : Attribute 16 | { 17 | public ProfilerAttribute(string guid) 18 | { 19 | Guid = guid; 20 | } 21 | 22 | public string Guid { get; } 23 | } 24 | -------------------------------------------------------------------------------- /test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $profilerDll = Resolve-Path ./src/ManagedDotnetProfiler/bin/Release/net9.0/win-x64/native/ManagedDotnetProfiler.dll 4 | 5 | $envVars = @{ 6 | 'CORECLR_ENABLE_PROFILING' = '1' 7 | 'CORECLR_PROFILER' = '{0A96F866-D763-4099-8E4E-ED1801BE9FBC}' 8 | 'CORECLR_PROFILER_PATH' = "$profilerDll" 9 | } 10 | 11 | Write-Host "Running TestApp with profiler..." 12 | 13 | $p = Start-Process -FilePath ./src/TestApp/bin/Release/net9.0/TestApp.exe ` 14 | -NoNewWindow -Wait -PassThru -Environment $envVars 15 | 16 | $exitCode = $p.ExitCode 17 | 18 | if ($exitCode -ne 0) { 19 | Write-Error "TestApp failed with exit code $exitCode" 20 | } 21 | exit $exitCode 22 | -------------------------------------------------------------------------------- /src/Silhouette/IMethodMalloc.cs: -------------------------------------------------------------------------------- 1 | using NativeObjects; 2 | 3 | namespace Silhouette; 4 | public class IMethodMalloc : Interfaces.IUnknown 5 | { 6 | private readonly IMethodMallocInvoker _impl; 7 | 8 | public IMethodMalloc(nint ptr) 9 | { 10 | _impl = new(ptr); 11 | } 12 | 13 | public HResult QueryInterface(in Guid guid, out nint ptr) 14 | { 15 | return _impl.QueryInterface(in guid, out ptr); 16 | } 17 | 18 | public int AddRef() 19 | { 20 | return _impl.AddRef(); 21 | } 22 | 23 | public int Release() 24 | { 25 | return _impl.Release(); 26 | } 27 | 28 | public IntPtr Alloc(uint size) 29 | { 30 | return _impl.Alloc(size); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerFunctionControl.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerFunctionControl : IUnknown 5 | { 6 | /* 7 | * Set one or more flags from COR_PRF_CODEGEN_FLAGS to control code 8 | * generation just for this method. 9 | */ 10 | HResult SetCodegenFlags(COR_PRF_CODEGEN_FLAGS flags); 11 | 12 | /* 13 | * Override the method body. 14 | */ 15 | HResult SetILFunctionBody(uint cbNewILMethodHeader, IntPtr newILMethodHeader); 16 | 17 | /* 18 | * This is not currently implemented, and will return E_NOTIMPL 19 | */ 20 | HResult SetILInstrumentedCodeMap(uint ilMapEntriesCount, COR_IL_MAP* ilMapEntries); 21 | } 22 | -------------------------------------------------------------------------------- /src/TestApp/launch.sh: -------------------------------------------------------------------------------- 1 | export CORECLR_ENABLE_PROFILING=1 2 | 3 | # debug so loading: 4 | #export LD_DEBUG=libs 5 | 6 | export CORECLR_PROFILER={0A96F866-D763-4099-8E4E-ED1801BE9FBC} 7 | export CORECLR_PROFILER_PATH=./../ManagedDotnetProfiler/bin/Release/net9.0/linux-x64/native/ManagedDotnetProfiler.so 8 | 9 | # create dump and crash json 10 | 11 | #export DOTNET_EnableCrashReport=1 12 | #export DOTNET_CreateDumpDiagnostics=1 13 | #export DOTNET_DbgEnableMiniDump=1 14 | #export DOTNET_DbgMiniDumpType=4 15 | #export DOTNET_DbgMiniDumpName="dump.dmp" 16 | 17 | export LD_LIBRARY_PATH=./../ManagedDotnetProfiler/bin/Release/net9.0/linux-x64/native/:$LD_LIBRARY_PATH 18 | 19 | # dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r linux-x64 -c Release 20 | 21 | ./bin/Release/net9.0/TestApp -------------------------------------------------------------------------------- /src/Silhouette/Unknown.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class Unknown : IUnknown 6 | { 7 | private int _referenceCount; 8 | 9 | protected abstract HResult QueryInterface(in Guid guid, out nint ptr); 10 | 11 | HResult IUnknown.QueryInterface(in Guid guid, out nint ptr) => QueryInterface(guid, out ptr); 12 | 13 | int IUnknown.AddRef() 14 | { 15 | return Interlocked.Increment(ref _referenceCount); 16 | } 17 | 18 | int IUnknown.Release() 19 | { 20 | var value = Interlocked.Decrement(ref _referenceCount); 21 | 22 | if (value == 0) 23 | { 24 | Dispose(); 25 | } 26 | 27 | return value; 28 | } 29 | 30 | public virtual void Dispose() 31 | { 32 | } 33 | } -------------------------------------------------------------------------------- /src/TestApp/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace TestApp; 4 | 5 | internal static class NativeMethods 6 | { 7 | public static uint GetCurrentThreadId() 8 | { 9 | if (OperatingSystem.IsWindows()) 10 | { 11 | return GetCurrentThreadId_Windows(); 12 | } 13 | 14 | if (OperatingSystem.IsLinux()) 15 | { 16 | return GetCurrentThreadId_Linux(); 17 | } 18 | 19 | throw new PlatformNotSupportedException("Unsupported OS"); 20 | } 21 | 22 | [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId")] 23 | private static extern uint GetCurrentThreadId_Windows(); 24 | 25 | [DllImport("libc", EntryPoint = "gettid")] 26 | private static extern uint GetCurrentThreadId_Linux(); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-test: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [windows-latest, ubuntu-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v4 22 | with: 23 | dotnet-version: '9.0.x' 24 | 25 | - name: Build & Test 26 | shell: bash 27 | run: | 28 | if [[ "$RUNNER_OS" == "Windows" ]]; then 29 | pwsh ./build.ps1 30 | pwsh ./test.ps1 31 | else 32 | chmod +x ./build.sh ./test.sh 33 | ./build.sh 34 | ./test.sh 35 | fi 36 | -------------------------------------------------------------------------------- /src/TestApp/TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | disable 8 | true 9 | SYSLIB1054;IDE0079 10 | 11 | 12 | 13 | WINDOWS 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Silhouette/ComPtr.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public static class ComPtr 6 | { 7 | public static ComPtr Create(T value) 8 | where T : IUnknown 9 | { 10 | return new ComPtr(value); 11 | } 12 | 13 | public static ComPtr Wrap(this T value) 14 | where T : IUnknown 15 | { 16 | return new ComPtr(value); 17 | } 18 | } 19 | 20 | public class ComPtr : IDisposable 21 | where T : IUnknown 22 | { 23 | public ComPtr(T value) 24 | { 25 | Value = value; 26 | } 27 | 28 | public T Value { get; } 29 | 30 | public ComPtr Copy() 31 | { 32 | Value?.AddRef(); 33 | return new(Value); 34 | } 35 | 36 | public void Dispose() 37 | { 38 | Value?.Release(); 39 | GC.SuppressFinalize(this); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback10.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback10 : ICorProfilerCallback9 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("CEC5B60E-C69C-495F-87F6-84D28EE16FFB"); 7 | 8 | // This event is triggered whenever an EventPipe event is configured to be delivered. 9 | // 10 | // Documentation Note: All pointers are only valid during the callback 11 | 12 | HResult EventPipeEventDelivered( 13 | nint provider, 14 | int eventId, 15 | int eventVersion, 16 | uint cbMetadataBlob, 17 | byte* metadataBlob, 18 | uint cbEventData, 19 | byte* eventData, 20 | in Guid pActivityId, 21 | in Guid pRelatedActivityId, 22 | ThreadId eventThread, 23 | uint numStackFrames, 24 | nint* stackFrames); 25 | 26 | HResult EventPipeProviderCreated(nint provider); 27 | } -------------------------------------------------------------------------------- /src/TestApp/FinalizationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.ConstrainedExecution; 2 | 3 | namespace TestApp; 4 | 5 | internal class FinalizationTests : ITest 6 | { 7 | public void Run() 8 | { 9 | InnerScope(); 10 | 11 | GC.Collect(2, GCCollectionMode.Forced, true); 12 | 13 | var logs = Logs.Fetch().ToList(); 14 | 15 | Logs.AssertContains(logs, "FinalizeableObjectQueued - None - FinalizableType"); 16 | Logs.AssertContains(logs, "FinalizeableObjectQueued - COR_PRF_FINALIZER_CRITICAL - CriticalFinalizableType"); 17 | } 18 | 19 | private static void InnerScope() 20 | { 21 | _ = new FinalizableType(); 22 | _ = new CriticalFinalizableType(); 23 | } 24 | 25 | private class FinalizableType 26 | { 27 | ~FinalizableType() 28 | { 29 | GC.KeepAlive(null); 30 | } 31 | } 32 | 33 | private class CriticalFinalizableType : CriticalFinalizerObject; 34 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo5.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo5 : ICorProfilerInfo4, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo5Invoker _impl; 6 | 7 | public ICorProfilerInfo5(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo5 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo5.Guid; 14 | 15 | public HResult GetEventMask2() 16 | { 17 | var result = _impl.GetEventMask2(out var eventsLow, out var eventsHigh); 18 | return new(result, new(eventsLow, eventsHigh)); 19 | } 20 | 21 | public HResult SetEventMask2(COR_PRF_MONITOR dwEventsLow, COR_PRF_HIGH_MONITOR dwEventsHigh) 22 | { 23 | return _impl.SetEventMask2(dwEventsLow, dwEventsHigh); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo6.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo6 : ICorProfilerInfo5, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo6Invoker _impl; 6 | 7 | public ICorProfilerInfo6(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo6 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo6.Guid; 14 | 15 | public HResult EnumNgenModuleMethodsInliningThisMethod(ModuleId inlinersModuleId, ModuleId inlineeModuleId, MdMethodDef inlineeMethodId) 16 | { 17 | var result = _impl.EnumNgenModuleMethodsInliningThisMethod(inlinersModuleId, inlineeModuleId, inlineeMethodId, out var incompleteData, out var pEnum); 18 | return new(result, new(new(pEnum), incompleteData != 0)); 19 | } 20 | } -------------------------------------------------------------------------------- /src/TestApp/GarbageCollectionTests.cs: -------------------------------------------------------------------------------- 1 | namespace TestApp; 2 | 3 | internal class GarbageCollectionTests : ITest 4 | { 5 | public void Run() 6 | { 7 | var threadId = NativeMethods.GetCurrentThreadId(); 8 | 9 | GC.Collect(2, GCCollectionMode.Default, blocking: true); // reason == COR_PRF_GC_INDUCED 10 | 11 | var logs = Logs.Fetch().ToList(); 12 | 13 | Logs.AssertContains(logs, "GarbageCollectionStarted - 0, 1, 2, 3, 4 - COR_PRF_GC_INDUCED - 1"); 14 | Logs.AssertContains(logs, "GarbageCollectionFinished - 0"); 15 | 16 | Logs.AssertContains(logs, "RuntimeSuspendStarted - COR_PRF_SUSPEND_FOR_GC"); 17 | Logs.AssertContains(logs, "RuntimeSuspendFinished"); 18 | Logs.AssertContains(logs, "RuntimeResumeStarted"); 19 | Logs.AssertContains(logs, "RuntimeResumeFinished"); 20 | 21 | Logs.AssertContains(logs, $"RuntimeThreadSuspended - {threadId}"); 22 | Logs.AssertContains(logs, $"RuntimeThreadResumed - {threadId}"); 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/qodana_code_quality.yml: -------------------------------------------------------------------------------- 1 | name: Qodana 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: # Specify your branches here 7 | - master # The 'main' branch 8 | - 'releases/*' # The release branches 9 | 10 | jobs: 11 | qodana: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | checks: write 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit 21 | fetch-depth: 0 # a full history is required for pull request analysis 22 | - name: 'Qodana Scan' 23 | uses: JetBrains/qodana-action@v2025.2 24 | with: 25 | pr-mode: false 26 | args: --baseline,qodana.sarif.json 27 | env: 28 | QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1784233326 }} 29 | QODANA_ENDPOINT: 'https://qodana.cloud' 30 | -------------------------------------------------------------------------------- /src/TestApp/ClassLoadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Emit; 2 | 3 | namespace TestApp; 4 | 5 | internal class ClassLoadTests : ITest 6 | { 7 | public void Run() 8 | { 9 | CreateAndUnloadType(); 10 | 11 | GC.Collect(2, GCCollectionMode.Forced, true); 12 | GC.WaitForPendingFinalizers(); 13 | GC.Collect(2, GCCollectionMode.Forced, true); 14 | 15 | var logs = Logs.Fetch().ToList(); 16 | 17 | Logs.AssertContains(logs, "ClassLoadFinished - DynamicType"); 18 | Logs.AssertContains(logs, "ClassUnloadStarted - DynamicType"); 19 | Logs.AssertContains(logs, "ClassUnloadFinished - DynamicType"); 20 | } 21 | 22 | private static void CreateAndUnloadType() 23 | { 24 | var assembly = AssemblyBuilder.DefineDynamicAssembly(new("DynamicAssembly"), AssemblyBuilderAccess.RunAndCollect); 25 | var module = assembly.DefineDynamicModule("DynamicModule"); 26 | var type = module.DefineType("DynamicType"); 27 | type.CreateType(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/ManagedDotnetProfiler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | true 6 | true 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | NativeMethods.cs 14 | 15 | 16 | NativeMethods.cs 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback7.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerCallback7 : ICorProfilerCallback6 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("F76A2DBA-1D52-4539-866C-2AA518F9EFC3"); 7 | 8 | // This event is triggered whenever the symbol stream associated with an 9 | // in-memory module is updated. Even when symbols are provided up-front in 10 | // a call to the managed API Assembly.Load(byte[], byte[], ...) the runtime 11 | // may not actually associate the symbolic data with the module until after 12 | // the ModuleLoadFinished callback has occurred. This event provides a later 13 | // opportunity to collect symbols for such modules. 14 | // 15 | // This event is controlled by the COR_PRF_HIGH_IN_MEMORY_SYMBOLS_UPDATED 16 | // event mask flag. 17 | // 18 | // Note: This event is not currently raised for symbols implicitly created or 19 | // modified via Reflection.Emit APIs. 20 | HResult ModuleInMemorySymbolsUpdated(ModuleId moduleId); 21 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo9.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo9 : ICorProfilerInfo8 5 | { 6 | public new static readonly Guid Guid = new("008170DB-F8CC-4796-9A51-DC8AA0B47012"); 7 | 8 | //Given functionId + rejitId, enumerate the native code start address of all jitted versions of this code that currently exist 9 | HResult GetNativeCodeStartAddresses(FunctionId functionID, ReJITId reJitId, uint cCodeStartAddresses, out uint pcCodeStartAddresses, uint* codeStartAddresses); 10 | 11 | //Given the native code start address, return the native->IL mapping information for this jitted version of the code 12 | HResult GetILToNativeMapping3(nint nativeCodeStartAddress, uint cMap, out uint pcMap, COR_DEBUG_IL_TO_NATIVE_MAP* map); 13 | 14 | //Given the native code start address, return the blocks of virtual memory that store this code (method code is not necessarily stored in a single contiguous memory region) 15 | HResult GetCodeInfo4(nint nativeCodeStartAddress, uint cCodeInfos, out uint pcCodeInfos, COR_PRF_CODE_INFO* codeInfos); 16 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback8.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback8 : ICorProfilerCallback7 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("5BED9B15-C079-4D47-BFE2-215A140C07E0"); 7 | 8 | // This event is triggered whenever a dynamic method is jit compiled. 9 | // These include various IL Stubs and LCG Methods. 10 | // The goal is to provide profiler writers with enough information to identify 11 | // it to users as beyond unknown code addresses. 12 | // Note: FunctionID's provided here cannot be used to resolve to their metadata 13 | // tokens since dynamic methods have no metadata. 14 | // 15 | // Documentation Note: pILHeader is only valid during the callback 16 | 17 | HResult DynamicMethodJITCompilationStarted( 18 | FunctionId functionId, 19 | int fIsSafeToBlock, 20 | byte* pILHeader, 21 | uint cbILHeader); 22 | 23 | HResult DynamicMethodJITCompilationFinished( 24 | FunctionId functionId, 25 | HResult hrStatus, 26 | int fIsSafeToBlock); 27 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback9Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback9Base : CorProfilerCallback8Base, ICorProfilerCallback9 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback9 _corProfilerCallback9; 8 | 9 | protected CorProfilerCallback9Base() 10 | { 11 | _corProfilerCallback9 = NativeObjects.ICorProfilerCallback9.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback9.Guid) 17 | { 18 | ptr = _corProfilerCallback9; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback9 26 | 27 | HResult ICorProfilerCallback9.DynamicMethodUnloaded(FunctionId functionId) 28 | { 29 | return DynamicMethodUnloaded(functionId); 30 | } 31 | 32 | #endregion 33 | 34 | protected virtual HResult DynamicMethodUnloaded(FunctionId functionId) 35 | { 36 | return HResult.E_NOTIMPL; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo11.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo11 : ICorProfilerInfo10 5 | { 6 | public new static readonly Guid Guid = new("06398876-8987-4154-B621-40A00D6E4D04"); 7 | 8 | /* 9 | * Get environment variable for the running managed code. 10 | */ 11 | HResult GetEnvironmentVariable( 12 | char* szName, 13 | uint cchValue, 14 | out uint pcchValue, 15 | char* szValue); 16 | 17 | /* 18 | * Set environment variable for the running managed code. 19 | * 20 | * The code profiler calls this function to modify environment variables of the 21 | * current managed process. For example, it can be used in the profiler's Initialize() 22 | * or InitializeForAttach() callbacks. 23 | * 24 | * szName is the name of the environment variable, should not be NULL. 25 | * 26 | * szValue is the contents of the environment variable, or NULL if the variable should be deleted. 27 | */ 28 | HResult SetEnvironmentVariable( 29 | char* szName, 30 | char* szValue); 31 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback7Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback7Base : CorProfilerCallback6Base, ICorProfilerCallback7 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback7 _corProfilerCallback7; 8 | 9 | protected CorProfilerCallback7Base() 10 | { 11 | _corProfilerCallback7 = NativeObjects.ICorProfilerCallback7.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback7.Guid) 17 | { 18 | ptr = _corProfilerCallback7; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback7 26 | 27 | HResult ICorProfilerCallback7.ModuleInMemorySymbolsUpdated(ModuleId moduleId) 28 | { 29 | return ModuleInMemorySymbolsUpdated(moduleId); 30 | } 31 | 32 | #endregion 33 | 34 | protected virtual HResult ModuleInMemorySymbolsUpdated(ModuleId moduleId) 35 | { 36 | return HResult.E_NOTIMPL; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo13.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo13 : ICorProfilerInfo12, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo13Invoker _impl; 6 | 7 | public ICorProfilerInfo13(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo13 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo13.Guid; 14 | 15 | public HResult CreateHandle(ObjectId @object, COR_PRF_HANDLE_TYPE type) 16 | { 17 | var result = _impl.CreateHandle(@object, type, out var handle); 18 | return new(result, handle); 19 | } 20 | 21 | public HResult DestroyHandle(ObjectHandleId handle) 22 | { 23 | return _impl.DestroyHandle(handle); 24 | } 25 | 26 | public HResult GetObjectIDFromHandle(ObjectHandleId handle) 27 | { 28 | var result = _impl.GetObjectIDFromHandle(handle, out var @object); 29 | return new(result, @object); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo5.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerInfo5 : ICorProfilerInfo4 5 | { 6 | public new static readonly Guid Guid = new("07602928-CE38-4B83-81E7-74ADAF781214"); 7 | 8 | /* 9 | * The code profiler calls GetEventMask2 to obtain the current event 10 | * categories for which it is to receive event notifications from the CLR 11 | * 12 | * *pdwEventsLow is a bitwise combination of values from COR_PRF_MONITOR 13 | * *pdwEventsHigh is a bitwise combination of values from COR_PRF_HIGH_MONITOR 14 | */ 15 | HResult GetEventMask2( 16 | out COR_PRF_MONITOR pdwEventsLow, 17 | out COR_PRF_HIGH_MONITOR pdwEventsHigh); 18 | 19 | /* 20 | * The code profiler calls SetEventMask2 to set the event categories for 21 | * which it is set to receive notification from the CLR. 22 | * 23 | * dwEventsLow is a bitwise combination of values from COR_PRF_MONITOR 24 | * dwEventsHigh is a bitwise combination of values from COR_PRF_HIGH_MONITOR 25 | */ 26 | HResult SetEventMask2( 27 | COR_PRF_MONITOR dwEventsLow, 28 | COR_PRF_HIGH_MONITOR dwEventsHigh); 29 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback6Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback6Base : CorProfilerCallback5Base, ICorProfilerCallback6 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback6 _corProfilerCallback6; 8 | 9 | protected CorProfilerCallback6Base() 10 | { 11 | _corProfilerCallback6 = NativeObjects.ICorProfilerCallback6.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback6.Guid) 17 | { 18 | ptr = _corProfilerCallback6; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback6 26 | 27 | unsafe HResult ICorProfilerCallback6.GetAssemblyReferences(char* wszAssemblyPath, nint pAsmRefProvider) 28 | { 29 | return GetAssemblyReferences(wszAssemblyPath, pAsmRefProvider); 30 | } 31 | 32 | #endregion 33 | 34 | protected virtual unsafe HResult GetAssemblyReferences(char* wszAssemblyPath, nint pAsmRefProvider) 35 | { 36 | return HResult.E_NOTIMPL; 37 | } 38 | } -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/NativeMethods.Windows.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ManagedDotnetProfiler; 6 | 7 | internal static unsafe partial class NativeMethods 8 | { 9 | [MethodImpl(MethodImplOptions.NoInlining)] 10 | private static string GetModulePathWindows(nint address) 11 | { 12 | const int flags = 0x4 /* GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS */ | 0x2 /* GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT */; 13 | if (GetModuleHandleExW(flags, address, out var hModule) == 0) 14 | { 15 | return null; 16 | } 17 | 18 | const int bufferSize = 1024; 19 | var buffer = stackalloc char[bufferSize]; 20 | var length = GetModuleFileNameW(hModule, buffer, bufferSize); 21 | return length > 0 ? new string(buffer, 0, length) : null; 22 | } 23 | 24 | [LibraryImport("kernel32", SetLastError = true)] 25 | private static partial int GetModuleHandleExW(int dwFlags, IntPtr lpModuleName, out IntPtr phModule); 26 | 27 | [LibraryImport("kernel32", SetLastError = true)] 28 | private static partial int GetModuleFileNameW(IntPtr hModule, char* lpFilename, int nSize); 29 | } 30 | -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/NativeMethods.Linux.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ManagedDotnetProfiler; 6 | 7 | [SuppressMessage("ReSharper", "InconsistentNaming")] 8 | internal static unsafe partial class NativeMethods 9 | { 10 | private static string GetModulePathLinux(nint address) 11 | { 12 | var export = NativeLibrary.GetExport(NativeLibrary.GetMainProgramHandle(), "dladdr"); 13 | var dladdr = (delegate* unmanaged[Cdecl])export; 14 | 15 | if (dladdr(address, out var info) == 0 || info.dli_fname == null) 16 | { 17 | return null; 18 | } 19 | 20 | return Marshal.PtrToStringUTF8((nint)info.dli_fname); 21 | } 22 | 23 | #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value 24 | private readonly struct DlInfo 25 | { 26 | public readonly byte* dli_fname; 27 | public readonly IntPtr dli_fbase; 28 | public readonly byte* dli_sname; 29 | public readonly IntPtr dli_saddr; 30 | } 31 | #pragma warning restore CS0649 // Field is never assigned to, and will always have its default value 32 | } 33 | -------------------------------------------------------------------------------- /src/TestApp/AssemblyLoadContextTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.Loader; 4 | 5 | namespace TestApp; 6 | 7 | internal class AssemblyLoadContextTests : ITest 8 | { 9 | public void Run() 10 | { 11 | CreateAndUnloadAlc(); 12 | 13 | GC.Collect(2, GCCollectionMode.Forced, true); 14 | GC.WaitForPendingFinalizers(); 15 | GC.Collect(2, GCCollectionMode.Forced, true); 16 | 17 | var logs = Logs.Fetch().ToList(); 18 | 19 | Logs.AssertContains(logs, $"AssemblyUnloadFinished - TestApp - AppDomain clrhost - Module {typeof(Program).Assembly.Location}"); 20 | } 21 | 22 | [MethodImpl(MethodImplOptions.NoInlining)] 23 | private static void CreateAndUnloadAlc() 24 | { 25 | var alc = new TestAssemblyLoadContext(); 26 | _ = alc.LoadFromAssemblyPath(typeof(Program).Assembly.Location); 27 | alc.Unload(); 28 | } 29 | 30 | private class TestAssemblyLoadContext : AssemblyLoadContext 31 | { 32 | public TestAssemblyLoadContext() 33 | : base(true) 34 | { 35 | } 36 | protected override Assembly Load(AssemblyName name) 37 | { 38 | return null; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback11Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback11Base : CorProfilerCallback10Base, ICorProfilerCallback11 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback11 _corProfilerCallback11; 8 | 9 | protected CorProfilerCallback11Base() 10 | { 11 | _corProfilerCallback11 = NativeObjects.ICorProfilerCallback11.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback11.Guid) 17 | { 18 | ptr = _corProfilerCallback11; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback11 26 | 27 | HResult ICorProfilerCallback11.LoadAsNotificationOnly(out int pbNotificationOnly) 28 | { 29 | var result = LoadAsNotificationOnly(out var notificationOnly); 30 | 31 | pbNotificationOnly = notificationOnly ? 1 : 0; 32 | 33 | return result; 34 | } 35 | 36 | #endregion 37 | 38 | protected virtual HResult LoadAsNotificationOnly(out bool pbNotificationOnly) 39 | { 40 | pbNotificationOnly = false; 41 | return HResult.E_NOTIMPL; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback5Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback5Base : CorProfilerCallback4Base, ICorProfilerCallback5 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback5 _corProfilerCallback5; 8 | 9 | protected CorProfilerCallback5Base() 10 | { 11 | _corProfilerCallback5 = NativeObjects.ICorProfilerCallback5.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback5.Guid) 17 | { 18 | ptr = _corProfilerCallback5; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback5 26 | unsafe HResult ICorProfilerCallback5.ConditionalWeakTableElementReferences(uint cRootRefs, ObjectId* keyRefIds, ObjectId* valueRefIds, GCHandleId* rootIds) 27 | { 28 | return ConditionalWeakTableElementReferences(cRootRefs, keyRefIds, valueRefIds, rootIds); 29 | } 30 | 31 | #endregion 32 | 33 | protected virtual unsafe HResult ConditionalWeakTableElementReferences(uint cRootRefs, ObjectId* keyRefIds, ObjectId* valueRefIds, GCHandleId* rootIds) 34 | { 35 | return HResult.E_NOTIMPL; 36 | } 37 | } -------------------------------------------------------------------------------- /src/TestApp/DynamicMethodTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace TestApp; 5 | 6 | internal class DynamicMethodTests : ITest 7 | { 8 | public void Run() 9 | { 10 | var handle = InnerScope(); 11 | 12 | GC.Collect(2, GCCollectionMode.Forced, true); 13 | GC.WaitForPendingFinalizers(); 14 | GC.Collect(2, GCCollectionMode.Forced, true); 15 | GC.WaitForPendingFinalizers(); 16 | 17 | var logs = Logs.Fetch().ToList(); 18 | 19 | Logs.AssertContains(logs, $"DynamicMethodJITCompilationStarted - {handle:x2}"); 20 | Logs.AssertContains(logs, $"DynamicMethodJITCompilationFinished - {handle:x2}"); 21 | Logs.AssertContains(logs, $"DynamicMethodUnloaded - {handle:x2}"); 22 | } 23 | 24 | private static nint InnerScope() 25 | { 26 | var dynamicMethod = new DynamicMethod("test", null, null); 27 | var ilGenerator = dynamicMethod.GetILGenerator(); 28 | ilGenerator.Emit(OpCodes.Ret); 29 | 30 | var handle = (RuntimeMethodHandle)typeof(DynamicMethod) 31 | .GetMethod("GetMethodDescriptor", BindingFlags.NonPublic | BindingFlags.Instance)! 32 | .Invoke(dynamicMethod, null)!; 33 | 34 | dynamicMethod.CreateDelegate().Invoke(); 35 | 36 | return handle.Value; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo7.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo7 : ICorProfilerInfo6, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo7Invoker _impl; 6 | 7 | public ICorProfilerInfo7(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo7 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo7.Guid; 14 | 15 | public HResult ApplyMetaData(ModuleId moduleId) 16 | { 17 | return _impl.ApplyMetaData(moduleId); 18 | } 19 | 20 | public HResult GetInMemorySymbolsLength(ModuleId moduleId) 21 | { 22 | var result = _impl.GetInMemorySymbolsLength(moduleId, out var countSymbolBytes); 23 | return new(result, countSymbolBytes); 24 | } 25 | 26 | public unsafe HResult ReadInMemorySymbols(ModuleId moduleId, int symbolsReadOffset, Span symbolBytes) 27 | { 28 | fixed (byte* pSymbolBytes = symbolBytes) 29 | { 30 | var result = _impl.ReadInMemorySymbols(moduleId, symbolsReadOffset, pSymbolBytes, (uint)symbolBytes.Length, out var countSymbolBytesRead); 31 | return new(result, countSymbolBytesRead); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/TestApp/ProfilerPInvokes.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace TestApp; 4 | 5 | /// 6 | /// The p/invokes in this class are automatically rewritten by the profiler. 7 | /// 8 | internal static unsafe class ProfilerPInvokes 9 | { 10 | private const string DllName = "ManagedDotnetProfiler"; 11 | 12 | [DllImport(DllName)] 13 | public static extern int FetchLastLog(char* buffer, int bufferSize); 14 | 15 | [DllImport(DllName)] 16 | public static extern bool GetCurrentThreadInfo(out ulong threadId, out uint osId); 17 | 18 | [DllImport(DllName)] 19 | public static extern bool GetThreads(uint* array, int length, int* actualLength); 20 | 21 | [DllImport(DllName)] 22 | public static extern int GetModuleNames(char* buffer, int length); 23 | 24 | [DllImport(DllName)] 25 | public static extern int CountFrozenObjects(); 26 | 27 | [DllImport(DllName)] 28 | public static extern bool EnumJittedFunctions(int version); 29 | 30 | [DllImport(DllName)] 31 | public static extern int GetGenericArguments(nint typeHandle, int methodToken, char* buffer, int size); 32 | 33 | [DllImport(DllName)] 34 | public static extern bool RequestReJit(IntPtr module, int methodDef); 35 | 36 | [DllImport(DllName)] 37 | public static extern bool RequestRevert(IntPtr module, int methodDef); 38 | } -------------------------------------------------------------------------------- /src/TestApp/JitCompilationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace TestApp; 4 | 5 | internal class JitCompilationTests : ITest 6 | { 7 | public void Run() 8 | { 9 | _ = PrivateMethod(); 10 | 11 | var logs = Logs.Fetch().ToList(); 12 | 13 | Logs.AssertContains(logs, "JITCompilationStarted - TestApp.JitCompilationTests.PrivateMethod"); 14 | Logs.AssertContains(logs, "JITCompilationFinished - TestApp.JitCompilationTests.PrivateMethod"); 15 | Logs.AssertContains(logs, "JITInlining - TestApp.JitCompilationTests.InnerMethod -> TestApp.JitCompilationTests.PrivateMethod"); 16 | 17 | for (int i = 1; i <= 2; i++) 18 | { 19 | Logs.Clear(); 20 | ProfilerPInvokes.EnumJittedFunctions(i); 21 | 22 | Logs.AssertContains([.. Logs.Fetch()], "Jitted function: TestApp.JitCompilationTests.PrivateMethod"); 23 | } 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] 27 | private static int PrivateMethod() 28 | { 29 | if (InnerMethod() == 1) 30 | { 31 | return 2; 32 | } 33 | 34 | return 0; 35 | } 36 | 37 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 38 | private static int InnerMethod() => 1; 39 | } 40 | -------------------------------------------------------------------------------- /src/TestApp/IlRewriteTest.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace TestApp; 5 | 6 | internal class IlRewriteTest : ITest 7 | { 8 | public void Run() 9 | { 10 | Logs.Clear(); 11 | 12 | StringSubstitutionTest(); 13 | RequestReJitTest(); 14 | } 15 | 16 | private static void StringSubstitutionTest() 17 | { 18 | Logs.Assert("success".Equals("failure")); // Failure will be rewritten to success by the profiler 19 | } 20 | 21 | private static void RequestReJitTest() 22 | { 23 | var method = typeof(IlRewriteTest).GetMethod(nameof(GetValue), BindingFlags.NonPublic | BindingFlags.Static)!; 24 | var methodDef = method.MetadataToken; 25 | 26 | var module = typeof(IlRewriteTest).Module; 27 | var moduleHandle = (IntPtr)module.GetType().GetMethod("GetUnderlyingNativeHandle", BindingFlags.NonPublic | BindingFlags.Instance)!.Invoke(module, null)!; 28 | 29 | Logs.Assert(GetValue() == 10); 30 | Logs.Assert(ProfilerPInvokes.RequestReJit(moduleHandle, methodDef)); 31 | Logs.Assert(GetValue() == 12); 32 | Logs.Assert(ProfilerPInvokes.RequestRevert(moduleHandle, methodDef)); 33 | Logs.Assert(GetValue() == 10); 34 | } 35 | 36 | 37 | [MethodImpl(MethodImplOptions.NoInlining)] 38 | private static int GetValue() => 10; 39 | } 40 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback5.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback5 : ICorProfilerCallback4 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("8DFBA405-8C9F-45F8-BFFA-83B14CEF78B5"); 7 | 8 | /* 9 | * The CLR calls ConditionalWeakTableElementReferences with information 10 | * about dependent handles after a garbage collection has occurred. 11 | * 12 | * For each root ID in rootIds, keyRefIds will contain the ObjectID for 13 | * the primary element in the dependent handle pair, and valueRefIds will 14 | * contain the ObjectID for the secondary element (keyRefIds[i] keeps 15 | * valueRefIds[i] alive). 16 | * 17 | * NOTE: None of the objectIDs returned by ConditionalWeakTableElementReferences 18 | * are valid during the callback itself, as the GC may be in the middle 19 | * of moving objects from old to new. Thus profilers should not attempt 20 | * to inspect objects during a ConditionalWeakTableElementReferences call. 21 | * At GarbageCollectionFinished, all objects have been moved to their new 22 | * locations, and inspection may be done. 23 | */ 24 | HResult ConditionalWeakTableElementReferences( 25 | uint cRootRefs, 26 | ObjectId* keyRefIds, 27 | ObjectId* valueRefIds, 28 | GCHandleId* rootIds); 29 | } -------------------------------------------------------------------------------- /src/TestApp/GenericArgumentsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace TestApp; 4 | internal unsafe class GenericArgumentsTests : ITest 5 | { 6 | public void Run() 7 | { 8 | var method = typeof(Test).GetMethod(nameof(Test.Function))!; 9 | 10 | Span buffer = stackalloc char[1024]; 11 | int length; 12 | 13 | fixed (char* pBuffer = buffer) 14 | { 15 | length = ProfilerPInvokes.GetGenericArguments(typeof(Test).TypeHandle.Value, method.MetadataToken, pBuffer, 1024); 16 | } 17 | 18 | if (length < 0) 19 | { 20 | foreach (var log in Logs.Fetch()) 21 | { 22 | Console.WriteLine(log); 23 | } 24 | 25 | throw new InvalidOperationException("Failed to get module names"); 26 | } 27 | 28 | var genericArguments = new string(buffer[..length]); 29 | Logs.Assert(genericArguments == "T1(Test, System.Collections.IEnumerable, System.Collections.IEqualityComparer), T2(System.IComparable, System.ValueType)"); 30 | } 31 | 32 | private class Test 33 | { 34 | #pragma warning disable CA1822 35 | // ReSharper disable twice UnusedTypeParameter 36 | public void Function() 37 | #pragma warning restore CA1822 38 | where T1: Test, IEnumerable, IEqualityComparer 39 | where T2: struct, IComparable 40 | { 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo6.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerInfo6 : ICorProfilerInfo5 5 | { 6 | public new static readonly Guid Guid = new("F30A070D-BFFB-46A7-B1D8-8781EF7B698A"); 7 | 8 | /* 9 | * Returns an enumerator for all methods that 10 | * - belong to a given NGen or R2R module (inlinersModuleId) and 11 | * - inlined a body of a given method (inlineeModuleId / inlineeMethodId). 12 | * 13 | * If incompleteData is set to TRUE after function is called, it means that the methods enumerator 14 | * doesn't contain all methods inlining a given method. 15 | * It can happen when one or more direct or indirect dependencies of inliners module haven't been loaded yet. 16 | * If profiler needs accurate data it should retry later when more modules are loaded (preferably on each module load). 17 | * 18 | * It can be used to lift limitation on inlining for ReJIT. 19 | * 20 | * NOTE: If the inlinee method is decorated with the System.Runtime.Versioning.NonVersionable attribute then 21 | * then some inliners may not ever be reported. If you need to get a full accounting you can avoid the issue 22 | * by disabling the use of all native images. 23 | * 24 | */ 25 | HResult EnumNgenModuleMethodsInliningThisMethod( 26 | ModuleId inlinersModuleId, 27 | ModuleId inlineeModuleId, 28 | MdMethodDef inlineeMethodId, 29 | out int incompleteData, 30 | out nint ppEnum); 31 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo9.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo9 : ICorProfilerInfo8, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo9Invoker _impl; 6 | 7 | public ICorProfilerInfo9(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo9 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo9.Guid; 14 | 15 | public unsafe HResult GetNativeCodeStartAddresses(FunctionId functionID, ReJITId reJitId, Span codeStartAddresses, out uint nbCodeStartAddresses) 16 | { 17 | fixed (uint* pCodeStartAddresses = codeStartAddresses) 18 | { 19 | return _impl.GetNativeCodeStartAddresses(functionID, reJitId, (uint)codeStartAddresses.Length, out nbCodeStartAddresses, pCodeStartAddresses); 20 | } 21 | } 22 | 23 | public unsafe HResult GetILToNativeMapping3(nint pNativeCodeStartAddress, Span map, out uint mapLength) 24 | { 25 | fixed (COR_DEBUG_IL_TO_NATIVE_MAP* pMap = map) 26 | { 27 | return _impl.GetILToNativeMapping3(pNativeCodeStartAddress, (uint)map.Length, out mapLength, pMap); 28 | } 29 | } 30 | 31 | public unsafe HResult GetCodeInfo4(nint nativeCodeStartAddress, Span codeInfos, out uint nbCodeInfos) 32 | { 33 | fixed (COR_PRF_CODE_INFO* pCodeInfos = codeInfos) 34 | { 35 | return _impl.GetCodeInfo4(nativeCodeStartAddress, (uint)codeInfos.Length, out nbCodeInfos, pCodeInfos); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo11.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo11 : ICorProfilerInfo10, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo11Invoker _impl; 6 | 7 | public ICorProfilerInfo11(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo11 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo11.Guid; 14 | 15 | public unsafe HResult GetEnvironmentVariable(string name, ReadOnlySpan value, out uint valueLength) 16 | { 17 | fixed (char* pValue = value) 18 | fixed (char* pName = name) 19 | { 20 | return _impl.GetEnvironmentVariable(pName, (uint)value.Length, out valueLength, pValue); 21 | } 22 | } 23 | 24 | public unsafe HResult GetEnvironmentVariable(string name) 25 | { 26 | var result = GetEnvironmentVariable(name, [], out var length); 27 | 28 | if (!result) 29 | { 30 | return result; 31 | } 32 | 33 | Span buffer = stackalloc char[(int)length]; 34 | 35 | result = GetEnvironmentVariable(name, buffer, out _); 36 | 37 | if (!result) 38 | { 39 | return result; 40 | } 41 | 42 | return new(result, buffer.WithoutNullTerminator()); 43 | } 44 | 45 | public unsafe HResult SetEnvironmentVariable(string name, string value) 46 | { 47 | fixed (char* pName = name) 48 | fixed (char* pValue = value) 49 | { 50 | return _impl.SetEnvironmentVariable(pName, pValue); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo10.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo10 : ICorProfilerInfo9 5 | { 6 | public new static readonly Guid Guid = new("2F1B5152-C869-40C9-AA5F-3ABE026BD720"); 7 | 8 | // Given an ObjectID, callback and clientData, enumerates each object reference (if any). 9 | HResult EnumerateObjectReferences(ObjectId objectId, delegate* unmanaged callback, void* clientData); 10 | 11 | // Given an ObjectID, determines whether it is in a read only segment. 12 | HResult IsFrozenObject(ObjectId objectId, out int pbFrozen); 13 | 14 | // Gets the value of the configured LOH Threshold. 15 | HResult GetLOHObjectSizeThreshold(out uint pThreshold); 16 | 17 | /* 18 | * This method will ReJIT the methods requested, as well as any inliners 19 | * of the methods requested. 20 | * 21 | * RequestReJIT does not do any tracking of inlined methods. The profiler 22 | * was expected to track inlining and call RequestReJIT for all inliners 23 | * to make sure every instance of an inlined method was ReJITted. 24 | * This poses a problem with ReJIT on attach, since the profiler was 25 | * not present to monitor inlining. This method can be called to guarantee 26 | * that the full set of inliners will be ReJITted as well. 27 | */ 28 | HResult RequestReJITWithInliners( 29 | uint dwRejitFlags, 30 | uint cFunctions, 31 | ModuleId* moduleIds, 32 | MdMethodDef* methodIds); 33 | 34 | // Suspend the runtime without performing a GC. 35 | HResult SuspendRuntime(); 36 | 37 | // Restart the runtime from a previous suspension. 38 | HResult ResumeRuntime(); 39 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback3Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback3Base : CorProfilerCallback2Base, ICorProfilerCallback3 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback3 _corProfilerCallback3; 8 | 9 | protected CorProfilerCallback3Base() 10 | { 11 | _corProfilerCallback3 = NativeObjects.ICorProfilerCallback3.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback3.Guid) 17 | { 18 | ptr = _corProfilerCallback3; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback3 26 | 27 | HResult ICorProfilerCallback3.ProfilerAttachComplete() 28 | { 29 | return ProfilerAttachComplete(); 30 | } 31 | 32 | HResult ICorProfilerCallback3.ProfilerDetachSucceeded() 33 | { 34 | return ProfilerDetachSucceeded(); 35 | } 36 | 37 | HResult ICorProfilerCallback3.InitializeForAttach(nint pCorProfilerInfoUnk, nint pvClientData, uint cbClientData) 38 | { 39 | return InitializeForAttach(pCorProfilerInfoUnk, pvClientData, cbClientData); 40 | } 41 | 42 | #endregion 43 | 44 | protected virtual HResult InitializeForAttach(nint pCorProfilerInfoUnk, nint pvClientData, uint cbClientData) 45 | { 46 | return HResult.E_NOTIMPL; 47 | } 48 | 49 | protected virtual HResult ProfilerAttachComplete() 50 | { 51 | return HResult.E_NOTIMPL; 52 | } 53 | 54 | protected virtual HResult ProfilerDetachSucceeded() 55 | { 56 | return HResult.E_NOTIMPL; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerFunctionControl.cs: -------------------------------------------------------------------------------- 1 | using NativeObjects; 2 | 3 | namespace Silhouette; 4 | 5 | public unsafe class ICorProfilerFunctionControl : Interfaces.IUnknown 6 | { 7 | private readonly ICorProfilerFunctionControlInvoker _impl; 8 | 9 | public ICorProfilerFunctionControl(nint ptr) 10 | { 11 | _impl = new(ptr); 12 | } 13 | 14 | public HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | return _impl.QueryInterface(in guid, out ptr); 17 | } 18 | 19 | public int AddRef() 20 | { 21 | return _impl.AddRef(); 22 | } 23 | 24 | public int Release() 25 | { 26 | return _impl.Release(); 27 | } 28 | 29 | public HResult SetCodegenFlags(COR_PRF_CODEGEN_FLAGS flags) 30 | { 31 | return _impl.SetCodegenFlags(flags); 32 | } 33 | 34 | public HResult SetILFunctionBody(uint cbNewILMethodHeader, IntPtr newILMethodHeader) 35 | { 36 | return _impl.SetILFunctionBody(cbNewILMethodHeader, newILMethodHeader); 37 | } 38 | 39 | public HResult SetILFunctionBody(ReadOnlySpan newILMethodHeader) 40 | { 41 | fixed (byte* p = newILMethodHeader) 42 | { 43 | return _impl.SetILFunctionBody((uint)newILMethodHeader.Length, (IntPtr)p); 44 | } 45 | } 46 | 47 | public HResult SetILInstrumentedCodeMap(uint ilMapEntriesCount, COR_IL_MAP* ilMapEntries) 48 | { 49 | return _impl.SetILInstrumentedCodeMap(ilMapEntriesCount, ilMapEntries); 50 | } 51 | 52 | public HResult SetILInstrumentedCodeMap(ReadOnlySpan ilMapEntries) 53 | { 54 | fixed (COR_IL_MAP* p = ilMapEntries) 55 | { 56 | return _impl.SetILInstrumentedCodeMap((uint)ilMapEntries.Length, p); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback8Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback8Base : CorProfilerCallback7Base, ICorProfilerCallback8 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback8 _corProfilerCallback8; 8 | 9 | protected CorProfilerCallback8Base() 10 | { 11 | _corProfilerCallback8 = NativeObjects.ICorProfilerCallback8.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback8.Guid) 17 | { 18 | ptr = _corProfilerCallback8; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback8 26 | 27 | HResult ICorProfilerCallback8.DynamicMethodJITCompilationFinished(FunctionId functionId, HResult hrStatus, int fIsSafeToBlock) 28 | { 29 | return DynamicMethodJITCompilationFinished(functionId, hrStatus, fIsSafeToBlock != 0); 30 | } 31 | 32 | unsafe HResult ICorProfilerCallback8.DynamicMethodJITCompilationStarted(FunctionId functionId, int fIsSafeToBlock, byte* pILHeader, uint cbILHeader) 33 | { 34 | return DynamicMethodJITCompilationStarted(functionId, fIsSafeToBlock != 0, pILHeader, cbILHeader); 35 | } 36 | 37 | #endregion 38 | 39 | protected virtual unsafe HResult DynamicMethodJITCompilationStarted(FunctionId functionId, bool fIsSafeToBlock, byte* pILHeader, uint cbILHeader) 40 | { 41 | return HResult.E_NOTIMPL; 42 | } 43 | 44 | protected virtual HResult DynamicMethodJITCompilationFinished(FunctionId functionId, HResult hrStatus, bool fIsSafeToBlock) 45 | { 46 | return HResult.E_NOTIMPL; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo12.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo12 : ICorProfilerInfo11 5 | { 6 | public new static readonly Guid Guid = new("27b24ccd-1cb1-47c5-96ee-98190dc30959"); 7 | 8 | HResult EventPipeStartSession( 9 | uint cProviderConfigs, 10 | COR_PRF_EVENTPIPE_PROVIDER_CONFIG* pProviderConfigs, 11 | int requestRundown, 12 | out EVENTPIPE_SESSION pSession); 13 | 14 | HResult EventPipeAddProviderToSession( 15 | EVENTPIPE_SESSION session, 16 | COR_PRF_EVENTPIPE_PROVIDER_CONFIG providerConfig); 17 | 18 | HResult EventPipeStopSession( 19 | EVENTPIPE_SESSION session); 20 | 21 | HResult EventPipeCreateProvider( 22 | char* providerName, 23 | out EVENTPIPE_PROVIDER pProvider); 24 | 25 | HResult EventPipeGetProviderInfo( 26 | EVENTPIPE_PROVIDER provider, 27 | uint cchName, 28 | out uint pcchName, 29 | char* providerName); 30 | 31 | HResult EventPipeDefineEvent( 32 | EVENTPIPE_PROVIDER provider, 33 | char* eventName, 34 | uint eventID, 35 | ulong keywords, 36 | uint eventVersion, 37 | uint level, 38 | byte opcode, 39 | int needStack, 40 | uint cParamDescs, 41 | COR_PRF_EVENTPIPE_PARAM_DESC* pParamDescs, 42 | out EVENTPIPE_EVENT pEvent); 43 | 44 | HResult EventPipeWriteEvent( 45 | EVENTPIPE_EVENT @event, 46 | uint cData, 47 | COR_PRF_EVENT_DATA* data, 48 | in Guid pActivityId, 49 | in Guid pRelatedActivityId); 50 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo15.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal interface ICorProfilerInfo15 : ICorProfilerInfo14 5 | { 6 | public new static readonly Guid Guid = new("B446462D-BD22-41DD-872D-DC714C49EB56"); 7 | 8 | /* 9 | * EnumerateGCHeapObjects is a method that iterates over each object in the GC heap. 10 | * For each object, it invokes the provided callback function which should return a bool 11 | * indicating whether or not enumeration should continue. 12 | * Enumerating the GC heap requires suspending the runtime. The profiler may accomplish this 13 | * by starting from a state where the runtime is not suspended and by doing one of: 14 | * 15 | * From the same thread, 16 | * Invoking ICorProfilerInfo10::SuspendRuntime() 17 | * ... 18 | * Invoking ICorProfilerInfo15::EnumerateGCHeapObjects() 19 | * ... 20 | * Invoking ICorProfilerInfo10::ResumeRuntime() 21 | * 22 | * or 23 | * 24 | * Invoke ICorProfilerInfo15::EnumerateGCHeapObjects() on its own, and leverage its 25 | * built-in runtime suspension logic. 26 | * 27 | * Parameters: 28 | * - callback: A function pointer to the callback function that will be invoked for each object in the GC heap. 29 | * The callback function should accept an ObjectID and a void pointer as parameters and return a BOOL. 30 | * - callbackState: A void pointer that can be used to pass state information to the callback function. 31 | * 32 | * Returns: 33 | * - HRESULT: A code indicating the result of the operation. If the method succeeds, 34 | * it returns S_OK. If it fails, it returns an error code. 35 | */ 36 | HResult EnumerateGCHeapObjects(IntPtr callback, IntPtr callbackState); 37 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo14.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo14 : ICorProfilerInfo13 5 | { 6 | public new static readonly Guid Guid = new("F460E352-D76D-4FE9-835F-F6AF9D6E862D"); 7 | 8 | HResult EnumerateNonGCObjects(out IntPtr pEnum); 9 | 10 | HResult GetNonGCHeapBounds( 11 | uint cObjectRanges, 12 | out uint pcObjectRanges, 13 | COR_PRF_NONGC_HEAP_RANGE* ranges); 14 | 15 | 16 | // EventPipeCreateProvider2 allows you to pass in a callback which will be called whenever a 17 | // session enables your provider. The behavior of the callback matches the ETW behavior which 18 | // can be counter intuitive. You will get a callback any time a session changes with the updated 19 | // global keywords enabled for your session. The is_enabled parameter will be true if any 20 | // session has your provider enabled. The source_id parameter will be a valid id if the callback 21 | // was triggered due to a session enabling and it will be NULL if it was triggered due to a session 22 | // disabling. 23 | // 24 | // Example: 25 | // Session A enables your provider: callback with is_enabled == true, session_id == A, and keywords == Session A 26 | // Session B enables your provider: callback with is_enabled == true, session_id == B, and keywords == Session A | Session B 27 | // Session B disables your provider: callback with is_enabled == true, session_id == NULL, and keywords == Session A 28 | // Session A disables your provider: callback with is_enabled == false, session_id == NULL, and keywords == 0 29 | HResult EventPipeCreateProvider2( 30 | char* providerName, 31 | IntPtr pCallback, 32 | out EVENTPIPE_PROVIDER pProvider); 33 | } -------------------------------------------------------------------------------- /src/TestApp/ModuleTests.cs: -------------------------------------------------------------------------------- 1 | namespace TestApp; 2 | 3 | internal class ModuleTests : ITest 4 | { 5 | public unsafe void Run() 6 | { 7 | var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); 8 | 9 | var buffer = new char[1024 * 100]; 10 | 11 | int length; 12 | 13 | fixed (char* p = buffer) 14 | { 15 | length = ProfilerPInvokes.GetModuleNames(p, buffer.Length); 16 | } 17 | 18 | if (length < 0) 19 | { 20 | foreach (var log in Logs.Fetch()) 21 | { 22 | Console.WriteLine(log); 23 | } 24 | 25 | throw new InvalidOperationException("Failed to get module names"); 26 | } 27 | 28 | // buffer contains multiple null-terminated strings 29 | var moduleNames = new List(); 30 | 31 | var span = new Span(buffer, 0, length); 32 | 33 | while (span.Length > 0) 34 | { 35 | var nullIndex = span.IndexOf('\0'); 36 | 37 | if (nullIndex < 0) 38 | { 39 | break; 40 | } 41 | 42 | moduleNames.Add(span[..nullIndex].ToString()); 43 | span = span[(nullIndex + 1)..]; 44 | } 45 | 46 | var expectedModules = from assembly in currentAssemblies 47 | from module in assembly.Modules 48 | let name = assembly.IsDynamic ? module.ScopeName : module.FullyQualifiedName 49 | orderby name 50 | select name; 51 | 52 | Logs.Assert(expectedModules.SequenceEqual(moduleNames.Order())); 53 | 54 | var frozenObjects = ProfilerPInvokes.CountFrozenObjects(); 55 | Logs.Assert(frozenObjects == 0); 56 | 57 | Logs.Clear(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Silhouette/INativeEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Silhouette; 4 | 5 | public readonly struct INativeEnumerator : IDisposable 6 | where T : unmanaged 7 | { 8 | private readonly NativeObjects.INativeEnumeratorInvoker _impl; 9 | 10 | public INativeEnumerator(nint ptr) 11 | { 12 | _impl = new(ptr); 13 | } 14 | 15 | public void Dispose() 16 | { 17 | _impl.Release(); 18 | } 19 | 20 | public HResult Skip(uint count) => _impl.Skip(count); 21 | 22 | public HResult Reset() => _impl.Reset(); 23 | 24 | public HResult> Clone() 25 | { 26 | var result = _impl.Clone(out var clone); 27 | return new(result, new(clone)); 28 | } 29 | 30 | public HResult GetCount() 31 | { 32 | var result = _impl.GetCount(out var count); 33 | return new(result, count); 34 | } 35 | 36 | public unsafe HResult Next(Span items) 37 | { 38 | fixed (void* pItems = items) 39 | { 40 | var result = _impl.Next((uint)items.Length, pItems, out var fetched); 41 | return new(result, fetched); 42 | } 43 | } 44 | 45 | public IEnumerable AsEnumerable() 46 | { 47 | T buffer = default; 48 | 49 | while (true) 50 | { 51 | var result = GetNextItem(ref buffer); 52 | 53 | if (result == HResult.S_OK) 54 | { 55 | yield return buffer; 56 | } 57 | else 58 | { 59 | break; 60 | } 61 | } 62 | } 63 | 64 | public BufferedNativeEnumerable AsEnumerable(Span buffer) 65 | { 66 | return new BufferedNativeEnumerable(this, buffer); 67 | } 68 | 69 | private HResult GetNextItem(ref T buffer) 70 | { 71 | return Next(MemoryMarshal.CreateSpan(ref buffer, 1)).Error; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Silhouette.IL/Silhouette.IL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | true 7 | true 8 | true 9 | true 10 | true 11 | IDE0290;IDE0079 12 | 13 | 14 | 15 | Silhouette.IL 16 | 0.1.0.0 17 | Silhouette IL rewriter 18 | Kevin Gosse 19 | https://github.com/kevingosse/Silhouette 20 | https://github.com/kevingosse/Silhouette 21 | false 22 | MIT 23 | 24 | IL Rewriter for Silhouette. 25 | 26 | 27 | - Initial release 28 | 29 | Copyright 2024-$([System.DateTime]::UtcNow.ToString(yyyy)) 30 | profiler profiling diagnostics native interop 31 | ..\..\nugets 32 | README.md 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Silhouette/ClassFactory.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Silhouette; 5 | 6 | public class ClassFactory : IClassFactory 7 | { 8 | private readonly NativeObjects.IClassFactory _classFactory; 9 | private readonly CorProfilerCallbackBase _corProfilerCallback; 10 | 11 | private GCHandle _handle; 12 | private int _refCount; 13 | 14 | private ClassFactory(CorProfilerCallbackBase corProfilerCallback) 15 | { 16 | _classFactory = NativeObjects.IClassFactory.Wrap(this); 17 | _corProfilerCallback = corProfilerCallback; 18 | _handle = GCHandle.Alloc(this); 19 | _refCount = 1; 20 | } 21 | 22 | public nint IClassFactory => _classFactory; 23 | 24 | public static IntPtr For(CorProfilerCallbackBase corProfilerCallback) 25 | { 26 | return new ClassFactory(corProfilerCallback).IClassFactory; 27 | } 28 | 29 | public HResult CreateInstance(nint outer, in Guid guid, out nint instance) 30 | { 31 | instance = _corProfilerCallback.ICorProfilerCallback; 32 | return HResult.S_OK; 33 | } 34 | 35 | public HResult LockServer(bool @lock) 36 | { 37 | return default; 38 | } 39 | 40 | public HResult QueryInterface(in Guid guid, out nint ptr) 41 | { 42 | if (guid == Silhouette.Interfaces.IClassFactory.Guid) 43 | { 44 | ptr = IClassFactory; 45 | return HResult.S_OK; 46 | } 47 | 48 | ptr = nint.Zero; 49 | return HResult.E_NOINTERFACE; 50 | } 51 | 52 | public int AddRef() 53 | { 54 | return Interlocked.Increment(ref _refCount); 55 | } 56 | 57 | public int Release() 58 | { 59 | var newCount = Interlocked.Decrement(ref _refCount); 60 | 61 | if (newCount == 0 && _handle.IsAllocated) 62 | { 63 | _handle.Free(); 64 | } 65 | 66 | return newCount; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Silhouette/BufferedNativeEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Silhouette; 4 | 5 | public readonly ref struct BufferedNativeEnumerable 6 | where T : unmanaged 7 | { 8 | private readonly INativeEnumerator _nativeEnumerator; 9 | private readonly Span _buffer; 10 | 11 | public BufferedNativeEnumerable(INativeEnumerator nativeEnumerator, Span buffer) 12 | { 13 | _buffer = buffer; 14 | _nativeEnumerator = nativeEnumerator; 15 | } 16 | 17 | public Enumerator GetEnumerator() => new(_nativeEnumerator, _buffer); 18 | 19 | public ref struct Enumerator : IEnumerator 20 | { 21 | private readonly Span _buffer; 22 | private readonly INativeEnumerator _nativeEnumerator; 23 | private int _index; 24 | private uint _size; 25 | private HResult _result; 26 | 27 | public Enumerator(INativeEnumerator nativeEnumerator, Span buffer) 28 | { 29 | _buffer = buffer; 30 | _nativeEnumerator = nativeEnumerator; 31 | _index = -1; 32 | _size = 0; 33 | _result = HResult.S_OK; 34 | } 35 | 36 | public T Current => _buffer[_index]; 37 | 38 | object IEnumerator.Current => Current; 39 | 40 | public void Dispose() { } 41 | 42 | public bool MoveNext() 43 | { 44 | _index++; 45 | 46 | if (_index >= _size) 47 | { 48 | if (!_result) 49 | { 50 | return false; 51 | } 52 | 53 | (_result, _size) = _nativeEnumerator.Next(_buffer); 54 | 55 | _index = 0; 56 | 57 | if (!_result) 58 | { 59 | _size = 0; 60 | } 61 | } 62 | 63 | return _index < _size; 64 | } 65 | 66 | public void Reset() => _nativeEnumerator.Reset().ThrowIfFailed(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback10Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback10Base : CorProfilerCallback9Base, ICorProfilerCallback10 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback10 _corProfilerCallback10; 8 | 9 | protected CorProfilerCallback10Base() 10 | { 11 | _corProfilerCallback10 = NativeObjects.ICorProfilerCallback10.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback10.Guid) 17 | { 18 | ptr = _corProfilerCallback10; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback10 26 | 27 | HResult ICorProfilerCallback10.EventPipeProviderCreated(nint provider) 28 | { 29 | return EventPipeProviderCreated(provider); 30 | } 31 | 32 | unsafe HResult ICorProfilerCallback10.EventPipeEventDelivered(nint provider, int eventId, int eventVersion, uint cbMetadataBlob, byte* metadataBlob, uint cbEventData, byte* eventData, in Guid pActivityId, in Guid pRelatedActivityId, ThreadId eventThread, uint numStackFrames, nint* stackFrames) 33 | { 34 | return EventPipeEventDelivered(provider, eventId, eventVersion, cbMetadataBlob, metadataBlob, cbEventData, eventData, in pActivityId, in pRelatedActivityId, eventThread, numStackFrames, stackFrames); 35 | } 36 | 37 | #endregion 38 | 39 | protected virtual unsafe HResult EventPipeEventDelivered(nint provider, int eventId, int eventVersion, uint cbMetadataBlob, byte* metadataBlob, uint cbEventData, byte* eventData, in Guid pActivityId, in Guid pRelatedActivityId, ThreadId eventThread, uint numStackFrames, nint* stackFrames) 40 | { 41 | return HResult.E_NOTIMPL; 42 | } 43 | 44 | protected virtual HResult EventPipeProviderCreated(nint provider) 45 | { 46 | return HResult.E_NOTIMPL; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo10.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo10 : ICorProfilerInfo9, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo10Invoker _impl; 6 | 7 | public ICorProfilerInfo10(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo10 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo10.Guid; 14 | 15 | public unsafe HResult EnumerateObjectReferences(ObjectId objectId, delegate* unmanaged callback, void* clientData) 16 | { 17 | return _impl.EnumerateObjectReferences(objectId, callback, clientData); 18 | } 19 | 20 | public HResult IsFrozenObject(ObjectId objectId) 21 | { 22 | var result = _impl.IsFrozenObject(objectId, out var frozen); 23 | return new(result, frozen != 0); 24 | } 25 | 26 | public HResult GetLOHObjectSizeThreshold() 27 | { 28 | var result = _impl.GetLOHObjectSizeThreshold(out var threshold); 29 | return new(result, threshold); 30 | } 31 | 32 | public unsafe HResult RequestReJITWithInliners(COR_PRF_REJIT_FLAGS rejitFlags, ReadOnlySpan moduleIds, ReadOnlySpan methodIds) 33 | { 34 | if (moduleIds.Length != methodIds.Length) 35 | { 36 | throw new ArgumentException("moduleIds and methodIds must have the same length."); 37 | } 38 | 39 | fixed (ModuleId* pModuleIds = moduleIds) 40 | fixed (MdMethodDef* pMethodIds = methodIds) 41 | { 42 | return _impl.RequestReJITWithInliners((uint)rejitFlags, (uint)moduleIds.Length, pModuleIds, pMethodIds); 43 | } 44 | } 45 | 46 | public HResult SuspendRuntime() 47 | { 48 | return _impl.SuspendRuntime(); 49 | } 50 | 51 | public HResult ResumeRuntime() 52 | { 53 | return _impl.ResumeRuntime(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo8.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo8 : ICorProfilerInfo7, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo8Invoker _impl; 6 | 7 | public ICorProfilerInfo8(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo8 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo8.Guid; 14 | 15 | public HResult IsFunctionDynamic(FunctionId functionId) 16 | { 17 | var result = _impl.IsFunctionDynamic(functionId, out var isDynamic); 18 | return new(result, isDynamic != 0); 19 | } 20 | 21 | public HResult GetFunctionFromIP3(nint ip) 22 | { 23 | var result = _impl.GetFunctionFromIP3(ip, out var functionId, out var reJitId); 24 | return new(result, new(functionId, reJitId)); 25 | } 26 | 27 | public unsafe HResult GetDynamicFunctionInfo(FunctionId functionId, Span name, out uint nameLength) 28 | { 29 | fixed (char* pName = name) 30 | { 31 | var result = _impl.GetDynamicFunctionInfo(functionId, out var moduleId, out var sig, out var pbSig, (uint)name.Length, out nameLength, pName); 32 | return new(result, new(moduleId, new(sig, (int)pbSig))); 33 | } 34 | } 35 | 36 | public unsafe HResult GetDynamicFunctionInfo(FunctionId functionId) 37 | { 38 | var (result, _) = GetDynamicFunctionInfo(functionId, [], out var length); 39 | 40 | if (!result) 41 | { 42 | return result; 43 | } 44 | 45 | Span buffer = stackalloc char[(int)length]; 46 | 47 | (result, var functionInfo) = GetDynamicFunctionInfo(functionId, buffer, out _); 48 | 49 | if (!result) 50 | { 51 | return result; 52 | } 53 | 54 | return new(result, new(functionInfo.ModuleId, functionInfo.Signature, buffer.WithoutNullTerminator())); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo15.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo15 : ICorProfilerInfo14, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo15Invoker _impl; 6 | 7 | public ICorProfilerInfo15(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo15 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo15.Guid; 14 | 15 | /// 16 | /// EnumerateGCHeapObjects is a method that iterates over each object in the GC heap. 17 | /// For each object, it invokes the provided callback function which should return a bool 18 | /// indicating whether or not enumeration should continue. 19 | /// Enumerating the GC heap requires suspending the runtime.The profiler may accomplish this 20 | /// by starting from a state where the runtime is not suspended and by doing one of: 21 | /// 22 | /// From the same thread, 23 | /// Invoking ICorProfilerInfo10::SuspendRuntime() 24 | /// ... 25 | /// Invoking ICorProfilerInfo15::EnumerateGCHeapObjects() 26 | /// ... 27 | /// Invoking ICorProfilerInfo10::ResumeRuntime() 28 | /// 29 | /// or 30 | /// Invoke ICorProfilerInfo15::EnumerateGCHeapObjects() on its own, and leverage its 31 | /// built-in runtime suspension logic. 32 | /// 33 | /// A function pointer to the callback function that will be invoked for each object in the GC heap. 34 | /// The callback function should accept an ObjectID and a void pointer as parameters and return a BOOL. 35 | /// A void pointer that can be used to pass state information to the callback function. 36 | /// A code indicating the result of the operation. If the method succeeds, it returns S_OK. If it fails, it returns an error code. 37 | public HResult EnumerateGCHeapObjects(IntPtr callback, IntPtr callbackState) 38 | { 39 | return _impl.EnumerateGCHeapObjects(callback, callbackState); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo8.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo8 : ICorProfilerInfo7 5 | { 6 | public new static readonly Guid Guid = new("C5AC80A6-782E-4716-8044-39598C60CFBF"); 7 | 8 | /* 9 | * Determines if a function has associated metadata 10 | * 11 | * Certain methods like IL Stubs or LCG Methods do not have 12 | * associated metadata that can be retrieved using the IMetaDataImport APIs. 13 | * 14 | * Such methods can be encountered by profilers through instruction pointers 15 | * or by listening to ICorProfilerCallback::DynamicMethodJITCompilationStarted 16 | * 17 | * This API can be used to determine whether a FunctionID is dynamic. 18 | */ 19 | HResult IsFunctionDynamic(FunctionId functionId, out int isDynamic); 20 | 21 | /* 22 | * Maps a managed code instruction pointer to a FunctionID. 23 | * 24 | * GetFunctionFromIP2 fails for dynamic methods, this method works for 25 | * both dynamic and non-dynamic methods. It is a superset of GetFunctionFromIP2 26 | */ 27 | HResult GetFunctionFromIP3(nint ip, 28 | out FunctionId functionId, 29 | out ReJITId pReJitId); 30 | 31 | /* 32 | * Retrieves information about dynamic methods 33 | * 34 | * Certain methods like IL Stubs or LCG do not have 35 | * associated metadata that can be retrieved using the IMetaDataImport APIs. 36 | * 37 | * Such methods can be encountered by profilers through instruction pointers 38 | * or by listening to ICorProfilerCallback::DynamicMethodJITCompilationStarted 39 | * 40 | * This API can be used to retrieve information about dynamic methods 41 | * including a friendly name if available. 42 | */ 43 | HResult GetDynamicFunctionInfo(FunctionId functionId, 44 | out ModuleId moduleId, 45 | out nint pvSig, 46 | out uint pbSig, 47 | uint cchName, 48 | out uint pcchName, 49 | char* wszName); 50 | } -------------------------------------------------------------------------------- /src/TestApp/Logs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace TestApp; 5 | 6 | internal static class Logs 7 | { 8 | private static readonly ConcurrentQueue AllLogs = new(); 9 | 10 | public static IEnumerable All => AllLogs; 11 | 12 | public static void Clear() 13 | { 14 | foreach (var _ in Fetch()) 15 | { 16 | // Do nothing 17 | } 18 | } 19 | 20 | public static IEnumerable Fetch() 21 | { 22 | while (true) 23 | { 24 | var log = FetchNext(); 25 | 26 | if (log == null) 27 | { 28 | yield break; 29 | } 30 | 31 | AllLogs.Enqueue(log); 32 | 33 | if (log.StartsWith("Error:")) 34 | { 35 | throw new Exception($"Found error log: {log}"); 36 | } 37 | 38 | yield return log; 39 | } 40 | } 41 | 42 | public static void AssertContains(List logs, string expected) 43 | { 44 | if (!logs.Contains(expected)) 45 | { 46 | Fail($"Could not find log: '{expected}'", logs); 47 | } 48 | } 49 | 50 | public static void Assert(bool value, [CallerArgumentExpression(nameof(value))] string expression = null) 51 | { 52 | if (!value) 53 | { 54 | Fail(expression, null); 55 | } 56 | } 57 | 58 | private static void Fail(string message, IEnumerable logs) 59 | { 60 | Console.WriteLine("********* Assertion failed, dumping logs *********"); 61 | 62 | logs ??= Fetch(); 63 | 64 | foreach (var log in logs) 65 | { 66 | Console.WriteLine(log); 67 | } 68 | 69 | throw new Exception($"Assertion failed: {message}"); 70 | } 71 | 72 | private static unsafe string FetchNext() 73 | { 74 | const int bufferSize = 1024; 75 | Span buffer = stackalloc char[bufferSize]; 76 | 77 | fixed (char* c = buffer) 78 | { 79 | int length = ProfilerPInvokes.FetchLastLog(c, buffer.Length); 80 | return length >= 0 ? new string(buffer[..length]) : null; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo7.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo7 : ICorProfilerInfo6 5 | { 6 | public new static readonly Guid Guid = new("9AEECC0D-63E0-4187-8C00-E312F503F663"); 7 | 8 | /* 9 | * Applies the newly emitted Metadata. 10 | * 11 | * This method can be used to apply the newly defined metadata by IMetadataEmit::Define* methods 12 | * to the module. 13 | * 14 | * If metadata changes are made after ModuleLoadFinished callback, 15 | * it is required to call this method before using the new metadata 16 | */ 17 | HResult ApplyMetaData( 18 | ModuleId moduleId); 19 | 20 | /* Returns the length of an in-memory symbol stream 21 | * 22 | * If the module has in-memory symbols the length of the stream will 23 | * be placed in pCountSymbolBytes. If the module doesn't have in-memory 24 | * symbols, *pCountSymbolBytes = 0 25 | * 26 | * Returns S_OK if the length could be determined (even if it is 0) 27 | * 28 | * Note: The current implementation does not support reflection.emit. 29 | * CORPROF_E_MODULE_IS_DYNAMIC will be returned in that case. 30 | */ 31 | HResult GetInMemorySymbolsLength( 32 | ModuleId moduleId, 33 | out uint countSymbolBytes); 34 | 35 | /* Reads bytes from an in-memory symbol stream 36 | * 37 | * This function attempts to read countSymbolBytes of data starting at offset 38 | * symbolsReadOffset within the in-memory stream. The data will be copied into 39 | * pSymbolBytes which is expected to have countSymbolBytes of space available. 40 | * pCountSymbolsBytesRead contains the actual number of bytes read which 41 | * may be less than countSymbolBytes if the end of the stream is reached. 42 | * 43 | * Returns S_OK if a non-zero number of bytes were read. 44 | * 45 | * Note: The current implementation does not support reflection.emit. 46 | * CORPROF_E_MODULE_IS_DYNAMIC will be returned in that case. 47 | */ 48 | HResult ReadInMemorySymbols( 49 | ModuleId moduleId, 50 | int symbolsReadOffset, 51 | byte* pSymbolBytes, 52 | uint countSymbolBytes, 53 | out uint pCountSymbolBytesRead); 54 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback6.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback6 : ICorProfilerCallback5 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("FC13DF4B-4448-4F4F-950C-BA8D19D00C36"); 7 | 8 | // CORECLR DEPRECATION WARNING: This callback does not occur on coreclr. 9 | // Controlled by the COR_PRF_HIGH_ADD_ASSEMBLY_REFERENCES event mask flag. 10 | // Notifies the profiler of a very early stage in the loading of an Assembly, where the CLR 11 | // performs an assembly reference closure walk. This is useful ONLY if the profiler will need 12 | // to modify the metadata of the Assembly to add AssemblyRefs (later, in ModuleLoadFinished). In 13 | // such a case, the profiler should implement this callback as well, to inform the CLR that assembly references 14 | // will be added once the module has loaded. This is useful to ensure that assembly sharing decisions 15 | // made by the CLR during this early stage remain valid even though the profiler plans to modify the metadata 16 | // assembly references later on. This can be used to avoid some instances where profiler metadata 17 | // modifications can cause the SECURITY_E_INCOMPATIBLE_SHARE error to be thrown. 18 | // 19 | // The profiler uses the ICorProfilerAssemblyReferenceProvider provided to add assembly references 20 | // to the CLR assembly reference closure walker. The ICorProfilerAssemblyReferenceProvider 21 | // should only be used from within this callback. The profiler will still need to explicitly add assembly 22 | // references via IMetaDataAssemblyEmit, from within the ModuleLoadFinished callback for the referencing assembly, 23 | // even though the profiler implements this GetAssemblyReferences callback. This callback does not result in 24 | // modified metadata; only in a modified assembly reference closure walk. 25 | // 26 | // The profiler should be prepared to receive duplicate calls to this callback for the same assembly, 27 | // and should respond identically for each such duplicate call (by making the same set of 28 | // ICorProfilerAssemblyReferenceProvider::AddAssemblyReference calls). 29 | HResult GetAssemblyReferences( 30 | char* wszAssemblyPath, 31 | nint pAsmRefProvider); 32 | } -------------------------------------------------------------------------------- /src/Silhouette/Silhouette.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | true 7 | true 8 | true 9 | true 10 | true 11 | IDE0290;IDE0079 12 | 13 | 14 | 15 | Silhouette 16 | 3.2.0.0 17 | Silhouette 18 | Kevin Gosse 19 | https://github.com/kevingosse/Silhouette 20 | https://github.com/kevingosse/Silhouette 21 | false 22 | MIT 23 | 24 | A library to build .NET profilers in .NET. No need for C++ anymore, just C#. 25 | 26 | 27 | - Add ProfilerAttribute to source-generate the DllGetClassObject export 28 | 29 | Copyright 2024-$([System.DateTime]::UtcNow.ToString(yyyy)) 30 | profiler profiling diagnostics native interop 31 | ..\..\nugets 32 | README.md 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Silhouette/IMetaDataEmit2.cs: -------------------------------------------------------------------------------- 1 | using NativeObjects; 2 | 3 | namespace Silhouette; 4 | 5 | public unsafe class IMetaDataEmit2 : IMetaDataEmit 6 | { 7 | private readonly IMetaDataEmit2Invoker _impl; 8 | 9 | public IMetaDataEmit2(nint ptr) 10 | : base(ptr) 11 | { 12 | _impl = new(ptr); 13 | } 14 | 15 | public HResult DefineMethodSpec(MdToken tkParent, ReadOnlySpan sigBlob) 16 | { 17 | fixed (byte* pvSigBlob = sigBlob) 18 | { 19 | var result = _impl.DefineMethodSpec(tkParent, (IntPtr)pvSigBlob, (uint)sigBlob.Length, out var pmi); 20 | return new(result, pmi); 21 | } 22 | } 23 | 24 | public HResult GetDeltaSaveSize(CorSaveSize save) 25 | { 26 | var result = _impl.GetDeltaSaveSize(save, out var saveSize); 27 | return new(result, saveSize); 28 | } 29 | 30 | public HResult SaveDelta(string file, uint saveFlags) 31 | { 32 | fixed (char* szFile = file) 33 | { 34 | return _impl.SaveDelta(szFile, saveFlags); 35 | } 36 | } 37 | 38 | public HResult SaveDeltaToStream(IntPtr pIStream, uint saveFlags) 39 | { 40 | return _impl.SaveDeltaToStream(pIStream, saveFlags); 41 | } 42 | 43 | public HResult SaveDeltaToMemory(Span data) 44 | { 45 | fixed (byte* pbData = data) 46 | { 47 | return _impl.SaveDeltaToMemory((IntPtr)pbData, (uint)data.Length); 48 | } 49 | } 50 | 51 | public HResult DefineGenericParam(MdToken tk, uint paramSeq, uint paramFlags, string name, uint reserved, ReadOnlySpan constraints) 52 | { 53 | fixed (char* szName = name) 54 | fixed (MdToken* rtkConstraints = constraints) 55 | { 56 | var result = _impl.DefineGenericParam(tk, paramSeq, paramFlags, szName, reserved, rtkConstraints, out var pgp); 57 | return new(result, pgp); 58 | } 59 | } 60 | 61 | public HResult SetGenericParamProps(MdGenericParam gp, uint paramFlags, string name, uint reserved, ReadOnlySpan constraints) 62 | { 63 | fixed (char* szName = name) 64 | fixed (MdToken* rtkConstraints = constraints) 65 | { 66 | return _impl.SetGenericParamProps(gp, paramFlags, szName, reserved, rtkConstraints); 67 | } 68 | } 69 | 70 | public HResult ResetENCLog() 71 | { 72 | return _impl.ResetENCLog(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using TestApp; 3 | 4 | bool ngenEnabled = Environment.GetEnvironmentVariable("MONITOR_NGEN") == "1"; 5 | 6 | Console.WriteLine($"PID: {Environment.ProcessId}"); 7 | Console.WriteLine($"NGEN: {ngenEnabled}"); 8 | 9 | var logs = Logs.Fetch().ToList(); 10 | 11 | // foreach (var log in logs) 12 | // { 13 | // Console.WriteLine(log); 14 | // } 15 | 16 | Logs.AssertContains(logs, $"AssemblyLoadFinished - TestApp - AppDomain clrhost - Module {typeof(Program).Assembly.Location}"); 17 | Logs.AssertContains(logs, $"AppDomainCreationStarted - System.Private.CoreLib.dll - Process Id {Environment.ProcessId}"); 18 | Logs.AssertContains(logs, $"AppDomainCreationStarted - DefaultDomain - Process Id {Environment.ProcessId}"); 19 | Logs.AssertContains(logs, "AppDomainCreationFinished - System.Private.CoreLib.dll - HResult S_OK"); 20 | Logs.AssertContains(logs, "AppDomainCreationFinished - DefaultDomain - HResult S_OK"); 21 | 22 | // Clear the logs before the next tests 23 | Logs.Clear(); 24 | 25 | var tests = new List 26 | { 27 | new AssemblyLoadContextTests(), 28 | new ClassLoadTests(), 29 | new ConditionalWeakTableTests(), 30 | new DynamicMethodTests(), 31 | new ExceptionTests(), 32 | new FinalizationTests(), 33 | new HandleTests(), 34 | new GarbageCollectionTests(), 35 | new JitCompilationTests(), 36 | new ThreadTests(), 37 | new ModuleTests(), 38 | new GenericArgumentsTests(), 39 | new IlRewriteTest() 40 | }; 41 | 42 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 43 | { 44 | tests.Add(new ComTests()); 45 | } 46 | 47 | if (!ngenEnabled) 48 | { 49 | // p/invoke callbacks are not raised when NGEN callbacks are enabled 50 | tests.Add(new PInvokeTests()); 51 | } 52 | else 53 | { 54 | tests.Add(new NgenTests()); 55 | } 56 | 57 | foreach (var test in tests) 58 | { 59 | try 60 | { 61 | test.Run(); 62 | } 63 | catch (Exception e) 64 | { 65 | Console.ForegroundColor = ConsoleColor.Red; 66 | Console.Write("[FAILURE] "); 67 | Console.ResetColor(); 68 | 69 | Console.WriteLine($"{test.GetType().Name} failed"); 70 | Console.WriteLine(e); 71 | continue; 72 | } 73 | 74 | Console.ForegroundColor = ConsoleColor.Green; 75 | Console.Write("[SUCCESS] "); 76 | Console.ResetColor(); 77 | Console.WriteLine($"{test.GetType().Name}"); 78 | } 79 | 80 | // Dump last logs before exiting 81 | //foreach (var log in Logs.Fetch()) 82 | //{ 83 | // Console.WriteLine(log); 84 | //} 85 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/IMetaDataEmit2.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public unsafe interface IMetaDataEmit2 : IMetaDataEmit 5 | { 6 | public new static readonly Guid Guid = new("F5DD9950-F693-42e6-830E-7B833E8146A9"); 7 | 8 | HResult DefineMethodSpec( 9 | MdToken tkParent, // [IN] MethodDef or MemberRef 10 | IntPtr pvSigBlob, // [IN] point to a blob value of signature 11 | uint cbSigBlob, // [IN] count of bytes in the signature blob 12 | out MdMethodSpec pmi); // [OUT] method instantiation token 13 | 14 | HResult GetDeltaSaveSize( // S_OK or error. 15 | CorSaveSize fSave, // [IN] cssAccurate or cssQuick. 16 | out uint pdwSaveSize); // [OUT] Put the size here. 17 | 18 | HResult SaveDelta( // S_OK or error. 19 | char* szFile, // [IN] The filename to save to. 20 | uint dwSaveFlags); // [IN] Flags for the save. 21 | 22 | HResult SaveDeltaToStream( // S_OK or error. 23 | IntPtr pIStream, // [IN] A writable stream to save to. 24 | uint dwSaveFlags); // [IN] Flags for the save. 25 | 26 | HResult SaveDeltaToMemory( // S_OK or error. 27 | IntPtr pbData, // [OUT] Location to write data. 28 | uint cbData); // [IN] Max size of data buffer. 29 | 30 | HResult DefineGenericParam( // S_OK or error. 31 | MdToken tk, // [IN] TypeDef or MethodDef 32 | uint ulParamSeq, // [IN] Index of the type parameter 33 | uint dwParamFlags, // [IN] Flags, for future use (e.g. variance) 34 | char* szname, // [IN] Name 35 | uint reserved, // [IN] For future use (e.g. non-type parameters) 36 | MdToken* rtkConstraints, // [IN] Array of type constraints (TypeDef,TypeRef,TypeSpec) 37 | out MdGenericParam pgp); // [OUT] Put GenericParam token here 38 | 39 | HResult SetGenericParamProps( // S_OK or error. 40 | MdGenericParam gp, // [IN] GenericParam 41 | uint dwParamFlags, // [IN] Flags, for future use (e.g. variance) 42 | char* szName, // [IN] Optional name 43 | uint reserved, // [IN] For future use (e.g. non-type parameters) 44 | MdToken* rtkConstraints);// [IN] Array of type constraints (TypeDef,TypeRef,TypeSpec) 45 | 46 | HResult ResetENCLog(); // S_OK or error. 47 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo14.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public unsafe class ICorProfilerInfo14 : ICorProfilerInfo13, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo14Invoker _impl; 6 | 7 | public ICorProfilerInfo14(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo14 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo14.Guid; 14 | 15 | public HResult> EnumerateNonGCObjects() 16 | { 17 | var result = _impl.EnumerateNonGCObjects(out var pEnum); 18 | return new(result, new(pEnum)); 19 | } 20 | 21 | public HResult GetNonGCHeapBounds(Span ranges, out uint nbObjectRanges) 22 | { 23 | fixed (COR_PRF_NONGC_HEAP_RANGE* pRanges = ranges) 24 | { 25 | return _impl.GetNonGCHeapBounds((uint)ranges.Length, out nbObjectRanges, pRanges); 26 | } 27 | } 28 | 29 | /// 30 | /// EventPipeCreateProvider2 allows you to pass in a callback which will be called whenever a 31 | /// session enables your provider. The behavior of the callback matches the ETW behavior which 32 | /// can be counter intuitive. You will get a callback any time a session changes with the updated 33 | /// global keywords enabled for your session. The is_enabled parameter will be true if any 34 | /// session has your provider enabled. The source_id parameter will be a valid id if the callback 35 | /// was triggered due to a session enabling and it will be NULL if it was triggered due to a session 36 | /// disabling. 37 | /// 38 | /// Example: 39 | /// Session A enables your provider: callback with is_enabled == true, session_id == A, and keywords == Session A 40 | /// Session B enables your provider: callback with is_enabled == true, session_id == B, and keywords == Session A | Session B 41 | /// Session B disables your provider: callback with is_enabled == true, session_id == NULL, and keywords == Session A 42 | /// Session A disables your provider: callback with is_enabled == false, session_id == NULL, and keywords == 0 /// 43 | public HResult EventPipeCreateProvider2(string providerName, IntPtr pCallback) 44 | { 45 | fixed (char* pProviderName = providerName) 46 | { 47 | var result = _impl.EventPipeCreateProvider2(pProviderName, pCallback, out var provider); 48 | return new(result, provider); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/ManagedDotnetProfiler/PInvoke.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace ManagedDotnetProfiler; 6 | 7 | internal static unsafe class PInvoke 8 | { 9 | [UnmanagedCallersOnly(EntryPoint = nameof(GetCurrentThreadInfo))] 10 | public static bool GetCurrentThreadInfo(ulong* pThreadId, uint* pOsId) 11 | { 12 | var info = CorProfiler.Instance.GetCurrentThreadInfo(); 13 | *pThreadId = info.threadId; 14 | *pOsId = info.osId; 15 | return info.result; 16 | } 17 | 18 | [UnmanagedCallersOnly(EntryPoint = nameof(GetThreads))] 19 | public static bool GetThreads(uint* array, int length, int* actualLength) 20 | { 21 | return Task.Run(() => CorProfiler.Instance.GetThreads(array, length, actualLength)).Result; 22 | } 23 | 24 | [UnmanagedCallersOnly(EntryPoint = nameof(GetModuleNames))] 25 | public static int GetModuleNames(char* buffer, int length) 26 | { 27 | return Task.Run(() => CorProfiler.Instance.GetModuleNames(buffer, length)).Result; 28 | } 29 | 30 | [UnmanagedCallersOnly(EntryPoint = nameof(FetchLastLog))] 31 | public static int FetchLastLog(char* buffer, int size) 32 | { 33 | if (!CorProfiler.Logs.TryDequeue(out var log)) 34 | { 35 | return -1; 36 | } 37 | 38 | if (size >= log.Length) 39 | { 40 | log.CopyTo(new Span(buffer, size)); 41 | } 42 | else 43 | { 44 | log.AsSpan(0, size).CopyTo(new Span(buffer, size)); 45 | } 46 | 47 | return log.Length; 48 | } 49 | 50 | [UnmanagedCallersOnly(EntryPoint = nameof(EnumJittedFunctions))] 51 | public static bool EnumJittedFunctions(int version) 52 | { 53 | return Task.Run(() => CorProfiler.Instance.EnumJittedFunctions(version)).Result; 54 | } 55 | 56 | [UnmanagedCallersOnly(EntryPoint = nameof(CountFrozenObjects))] 57 | public static int CountFrozenObjects() 58 | { 59 | return Task.Run(CorProfiler.Instance.CountFrozenObjects).Result; 60 | } 61 | 62 | [UnmanagedCallersOnly(EntryPoint = nameof(GetGenericArguments))] 63 | public static int GetGenericArguments(nint typeHandle, int methodToken, char* buffer, int size) 64 | { 65 | return Task.Run(() => CorProfiler.Instance.GetGenericArguments(typeHandle, methodToken, buffer, size)).Result; 66 | } 67 | 68 | [UnmanagedCallersOnly(EntryPoint = nameof(RequestReJit))] 69 | public static bool RequestReJit(IntPtr module, int methodDef) 70 | { 71 | return Task.Run(() => CorProfiler.Instance.RequestReJit(module, methodDef)).Result; 72 | } 73 | 74 | [UnmanagedCallersOnly(EntryPoint = nameof(RequestRevert))] 75 | public static bool RequestRevert(IntPtr module, int methodDef) 76 | { 77 | return Task.Run(() => CorProfiler.Instance.RequestRevert(module, methodDef)).Result; 78 | } 79 | } -------------------------------------------------------------------------------- /src/Silhouette.IL/IlRewriter.cs: -------------------------------------------------------------------------------- 1 | using dnlib.DotNet.Emit; 2 | using dnlib.DotNet; 3 | using dnlib.IO; 4 | using dnlib.DotNet.Writer; 5 | 6 | namespace Silhouette.IL; 7 | 8 | public sealed class IlRewriter : IDisposable 9 | { 10 | private readonly ICorProfilerInfo3 _corProfilerInfo; 11 | private readonly ICorProfilerFunctionControl _functionControl; 12 | private ModuleId _moduleId; 13 | private MdMethodDef _methodDefToken; 14 | private InstructionOperandResolver _instructionOperandResolver; 15 | 16 | private IlRewriter(ICorProfilerInfo3 corProfilerInfo, ICorProfilerFunctionControl functionControl = null) 17 | { 18 | _corProfilerInfo = corProfilerInfo; 19 | _functionControl = functionControl; 20 | } 21 | 22 | public CilBody Body { get; private set; } 23 | 24 | public static IlRewriter Create(ICorProfilerInfo3 corProfilerInfo) 25 | { 26 | return new IlRewriter(corProfilerInfo); 27 | } 28 | 29 | public static IlRewriter CreateForReJit(ICorProfilerInfo3 corProfilerInfo3, ICorProfilerFunctionControl functionControl) 30 | { 31 | return new IlRewriter(corProfilerInfo3, functionControl); 32 | } 33 | 34 | public void Import(FunctionId functionId) 35 | { 36 | var functionInfo = _corProfilerInfo.GetFunctionInfo(functionId).ThrowIfFailed(); 37 | Import(functionInfo.ModuleId, new(functionInfo.Token)); 38 | } 39 | 40 | public unsafe void Import(ModuleId moduleId, MdMethodDef methodDef) 41 | { 42 | var functionBody = _corProfilerInfo.GetILFunctionBody(moduleId, methodDef).ThrowIfFailed(); 43 | 44 | _moduleId = moduleId; 45 | _methodDefToken = methodDef; 46 | 47 | var dataStream = DataStreamFactory.Create((byte*)functionBody.MethodHeader); 48 | var dataReader = new DataReader(dataStream, 0, uint.MaxValue); 49 | 50 | _instructionOperandResolver = new InstructionOperandResolver(moduleId, _corProfilerInfo); 51 | 52 | var parameters = new List(); 53 | 54 | var bodyReader = new MethodBodyReader(_instructionOperandResolver, dataReader, parameters); 55 | 56 | if (!bodyReader.Read()) 57 | { 58 | throw new InvalidOperationException("Failed to read method body."); 59 | } 60 | 61 | Body = bodyReader.CreateCilBody(); 62 | } 63 | 64 | public unsafe void Export() 65 | { 66 | var writer = new MethodBodyWriter(_instructionOperandResolver, Body); 67 | writer.Write(); 68 | 69 | var bodyBytes = writer.Code; 70 | 71 | if (_functionControl != null) 72 | { 73 | _functionControl.SetILFunctionBody(bodyBytes).ThrowIfFailed(); 74 | return; 75 | } 76 | 77 | var malloc = _corProfilerInfo.GetILFunctionBodyAllocator(_moduleId).ThrowIfFailed(); 78 | 79 | var bodyPtr = malloc.Alloc((uint)bodyBytes.Length); 80 | bodyBytes.AsSpan().CopyTo(new Span((void*)bodyPtr, bodyBytes.Length)); 81 | 82 | _corProfilerInfo.SetILFunctionBody(_moduleId, _methodDefToken, bodyPtr).ThrowIfFailed(); 83 | } 84 | 85 | public void Dispose() 86 | { 87 | _instructionOperandResolver?.Dispose(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TestApp/ThreadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace TestApp; 4 | 5 | internal class ThreadTests : ITest 6 | { 7 | public void Run() 8 | { 9 | var threadId = CreateAndDestroyThread(); 10 | 11 | // Make sure the thread is finalized 12 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); 13 | GC.WaitForPendingFinalizers(); 14 | 15 | var logs = Logs.Fetch().ToList(); 16 | 17 | Logs.AssertContains(logs, $"ThreadCreated - {threadId}"); 18 | Logs.AssertContains(logs, $"ThreadDestroyed - {threadId}"); 19 | Logs.AssertContains(logs, $"ThreadAssignedToOSThread - {threadId}"); 20 | Logs.AssertContains(logs, "ThreadNameChanged - Test"); 21 | 22 | ChildThreadsTest(); 23 | 24 | var currentThreadId = (IntPtr)typeof(Thread).GetField("_DONT_USE_InternalThread", BindingFlags.Instance | BindingFlags.NonPublic)! 25 | .GetValue(Thread.CurrentThread)!; 26 | 27 | var osId = NativeMethods.GetCurrentThreadId(); 28 | 29 | Logs.Assert(ProfilerPInvokes.GetCurrentThreadInfo(out var actualThreadId, out var actualOsId)); 30 | Logs.Assert((ulong)currentThreadId == actualThreadId); 31 | Logs.Assert(osId == actualOsId); 32 | } 33 | 34 | private static unsafe void ChildThreadsTest() 35 | { 36 | const int nbThreads = 8; 37 | 38 | var threads = new Thread[nbThreads]; 39 | var expectedOsIds = new uint[nbThreads]; 40 | using var barrier = new Barrier(nbThreads + 1); 41 | 42 | for (int i = 0; i < nbThreads; i++) 43 | { 44 | int index = i; 45 | threads[index] = new Thread(() => 46 | { 47 | expectedOsIds[index] = NativeMethods.GetCurrentThreadId(); 48 | // ReSharper disable twice AccessToDisposedClosure 49 | barrier.SignalAndWait(); 50 | barrier.SignalAndWait(); 51 | }); 52 | 53 | threads[index].Start(); 54 | } 55 | 56 | barrier.SignalAndWait(); 57 | 58 | Span actualOsIds = stackalloc uint[50]; 59 | int actualLength = 0; 60 | 61 | fixed (uint* pActualOsIds = actualOsIds) 62 | { 63 | ProfilerPInvokes.GetThreads(pActualOsIds, actualOsIds.Length, &actualLength); 64 | } 65 | 66 | if (actualLength >= actualOsIds.Length) 67 | { 68 | throw new InvalidOperationException("The buffer was too small"); 69 | } 70 | 71 | for (int i = 0; i < nbThreads; i++) 72 | { 73 | if (!actualOsIds.Contains(expectedOsIds[i])) 74 | { 75 | throw new InvalidOperationException($"Thread {expectedOsIds[i]} was not found"); 76 | } 77 | } 78 | 79 | barrier.SignalAndWait(); 80 | Logs.Clear(); 81 | } 82 | 83 | private static uint CreateAndDestroyThread() 84 | { 85 | uint id = 0; 86 | 87 | var thread = new Thread(() => 88 | { 89 | id = NativeMethods.GetCurrentThreadId(); 90 | Thread.CurrentThread.Name = "Test"; 91 | }); 92 | 93 | thread.Start(); 94 | thread.Join(); 95 | 96 | return id; 97 | } 98 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/IMetaDataImport2.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | public unsafe interface IMetaDataImport2 : IMetaDataImport 5 | { 6 | public new static readonly Guid Guid = new("FCE5EFA0-8BBA-4f8e-A036-8F2022B08466"); 7 | 8 | HResult EnumGenericParams( 9 | HCORENUM* phEnum, // [IN|OUT] Pointer to the enum. 10 | MdToken tk, // [IN] TypeDef or MethodDef whose generic parameters are requested 11 | MdGenericParam* rGenericParams, // [OUT] Put GenericParams here. 12 | uint cMax, // [IN] Max GenericParams to put. 13 | out uint pcGenericParams); // [OUT] Put # put here. 14 | 15 | HResult GetGenericParamProps( // S_OK or error. 16 | MdGenericParam gp, // [IN] GenericParam 17 | out uint pulParamSeq, // [OUT] Index of the type parameter 18 | out uint pdwParamFlags, // [OUT] Flags, for future use (e.g. variance) 19 | out MdToken ptOwner, // [OUT] Owner (TypeDef or MethodDef) 20 | out uint reserved, // [OUT] For future use (e.g. non-type parameters) 21 | char* wzname, // [OUT] Put name here 22 | uint cchName, // [IN] Size of buffer 23 | out uint pchName); // [OUT] Put size of name (wide chars) here. 24 | 25 | HResult GetMethodSpecProps( 26 | MdMethodSpec mi, // [IN] The method instantiation 27 | out MdToken tkParent, // [OUT] MethodDef or MemberRef 28 | out IntPtr ppvSigBlob, // [OUT] point to the blob value of meta data 29 | out uint pcbSigBlob); // [OUT] actual size of signature blob 30 | 31 | HResult EnumGenericParamConstraints( 32 | HCORENUM* phEnum, // [IN|OUT] Pointer to the enum. 33 | MdGenericParam tk, // [IN] GenericParam whose constraints are requested 34 | MdGenericParamConstraint* rGenericParamConstraints, // [OUT] Put GenericParamConstraints here. 35 | uint cMax, // [IN] Max GenericParamConstraints to put. 36 | out uint pcGenericParamConstraints); // [OUT] Put # put here. 37 | 38 | HResult GetGenericParamConstraintProps( // S_OK or error. 39 | MdGenericParamConstraint gpc, // [IN] GenericParamConstraint 40 | out MdGenericParam ptGenericParam, // [OUT] GenericParam that is constrained 41 | out MdToken ptkConstraintType); // [OUT] TypeDef/Ref/Spec constraint 42 | 43 | HResult GetPEKind( // S_OK or error. 44 | out uint pdwPEKind, // [OUT] The kind of PE (0 - not a PE) 45 | out uint pdwMachine); // [OUT] Machine as defined in NT header 46 | 47 | HResult GetVersionString( // S_OK or error. 48 | char* pwzBuf, // [OUT] Put version string here. 49 | uint ccBufSize, // [IN] size of the buffer, in wide chars 50 | out uint pccBufSize); // [OUT] Size of the version string, wide chars, including terminating nul. 51 | 52 | HResult EnumMethodSpecs( 53 | HCORENUM* phEnum, // [IN|OUT] Pointer to the enum. 54 | MdToken tk, // [IN] MethodDef or MemberRef whose MethodSpecs are requested 55 | MdMethodSpec* rMethodSpecs, // [OUT] Put MethodSpecs here. 56 | uint cMax, // [IN] Max tokens to put. 57 | out uint pcMethodSpecs); // [OUT] Put actual count here. 58 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback4Base.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public abstract class CorProfilerCallback4Base : CorProfilerCallback3Base, Interfaces.ICorProfilerCallback4 4 | { 5 | private readonly NativeObjects.ICorProfilerCallback4 _corProfilerCallback4; 6 | 7 | protected CorProfilerCallback4Base() 8 | { 9 | _corProfilerCallback4 = NativeObjects.ICorProfilerCallback4.Wrap(this); 10 | } 11 | 12 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 13 | { 14 | if (guid == Interfaces.ICorProfilerCallback4.Guid) 15 | { 16 | ptr = _corProfilerCallback4; 17 | return HResult.S_OK; 18 | } 19 | 20 | return base.QueryInterface(guid, out ptr); 21 | } 22 | 23 | #region ICorProfilerCallback4 24 | 25 | HResult Interfaces.ICorProfilerCallback4.GetReJITParameters(ModuleId moduleId, MdMethodDef methodId, nint pFunctionControl) 26 | { 27 | return GetReJITParameters(moduleId, methodId, new(pFunctionControl)); 28 | } 29 | 30 | HResult Interfaces.ICorProfilerCallback4.ReJITCompilationFinished(FunctionId functionId, ReJITId rejitId, HResult hrStatus, int fIsSafeToBlock) 31 | { 32 | return ReJITCompilationFinished(functionId, rejitId, hrStatus, fIsSafeToBlock != 0); 33 | } 34 | 35 | HResult Interfaces.ICorProfilerCallback4.ReJITError(ModuleId moduleId, MdMethodDef methodId, FunctionId functionId, HResult hrStatus) 36 | { 37 | return ReJITError(moduleId, methodId, functionId, hrStatus); 38 | } 39 | 40 | unsafe HResult Interfaces.ICorProfilerCallback4.MovedReferences2(uint cMovedObjectIDRanges, ObjectId* oldObjectIDRangeStart, ObjectId* newObjectIDRangeStart, nint* cObjectIDRangeLength) 41 | { 42 | return MovedReferences2(cMovedObjectIDRanges, oldObjectIDRangeStart, newObjectIDRangeStart, cObjectIDRangeLength); 43 | } 44 | 45 | unsafe HResult Interfaces.ICorProfilerCallback4.SurvivingReferences2(uint cSurvivingObjectIDRanges, ObjectId* objectIDRangeStart, nint* cObjectIDRangeLength) 46 | { 47 | return SurvivingReferences2(cSurvivingObjectIDRanges, objectIDRangeStart, cObjectIDRangeLength); 48 | } 49 | 50 | HResult Interfaces.ICorProfilerCallback4.ReJITCompilationStarted(FunctionId functionId, ReJITId rejitId, int fIsSafeToBlock) 51 | { 52 | return ReJITCompilationStarted(functionId, rejitId, fIsSafeToBlock != 0); 53 | } 54 | 55 | #endregion 56 | 57 | protected virtual HResult ReJITCompilationStarted(FunctionId functionId, ReJITId rejitId, bool fIsSafeToBlock) 58 | { 59 | return HResult.E_NOTIMPL; 60 | } 61 | 62 | protected virtual HResult GetReJITParameters(ModuleId moduleId, MdMethodDef methodId, ICorProfilerFunctionControl functionControl) 63 | { 64 | return HResult.E_NOTIMPL; 65 | } 66 | 67 | protected virtual HResult ReJITCompilationFinished(FunctionId functionId, ReJITId rejitId, HResult hrStatus, bool fIsSafeToBlock) 68 | { 69 | return HResult.E_NOTIMPL; 70 | } 71 | 72 | protected virtual HResult ReJITError(ModuleId moduleId, MdMethodDef methodId, FunctionId functionId, HResult hrStatus) 73 | { 74 | return HResult.E_NOTIMPL; 75 | } 76 | 77 | protected virtual unsafe HResult MovedReferences2(uint cMovedObjectIDRanges, ObjectId* oldObjectIDRangeStart, ObjectId* newObjectIDRangeStart, nint* cObjectIDRangeLength) 78 | { 79 | return HResult.E_NOTIMPL; 80 | } 81 | 82 | protected virtual unsafe HResult SurvivingReferences2(uint cSurvivingObjectIDRanges, ObjectId* objectIDRangeStart, nint* cObjectIDRangeLength) 83 | { 84 | return HResult.E_NOTIMPL; 85 | } 86 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo12.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public unsafe class ICorProfilerInfo12 : ICorProfilerInfo11, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo12Invoker _impl; 6 | 7 | public ICorProfilerInfo12(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo12 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo12.Guid; 14 | 15 | public HResult EventPipeStartSession(Span providerConfigs, bool requestRundown) 16 | { 17 | fixed (COR_PRF_EVENTPIPE_PROVIDER_CONFIG* pProviderConfigs = providerConfigs) 18 | { 19 | var result = _impl.EventPipeStartSession((uint)providerConfigs.Length, pProviderConfigs, requestRundown ? 1 : 0, out var session); 20 | return new(result, session); 21 | } 22 | } 23 | 24 | public HResult EventPipeAddProviderToSession(EVENTPIPE_SESSION session, COR_PRF_EVENTPIPE_PROVIDER_CONFIG providerConfig) 25 | { 26 | return _impl.EventPipeAddProviderToSession(session, providerConfig); 27 | } 28 | 29 | public HResult EventPipeStopSession(EVENTPIPE_SESSION session) 30 | { 31 | return _impl.EventPipeStopSession(session); 32 | } 33 | 34 | public HResult EventPipeCreateProvider(string providerName) 35 | { 36 | fixed (char* pProviderName = providerName) 37 | { 38 | var result = _impl.EventPipeCreateProvider(pProviderName, out var provider); 39 | return new(result, provider); 40 | } 41 | } 42 | 43 | public HResult EventPipeGetProviderInfo(EVENTPIPE_PROVIDER provider, Span name, out uint nameLength) 44 | { 45 | fixed (char* pName = name) 46 | { 47 | return _impl.EventPipeGetProviderInfo(provider, (uint)name.Length, out nameLength, pName); 48 | } 49 | } 50 | 51 | public HResult EventPipeGetProviderInfo(EVENTPIPE_PROVIDER provider) 52 | { 53 | var result = EventPipeGetProviderInfo(provider, [], out var length); 54 | 55 | if (!result) 56 | { 57 | return result; 58 | } 59 | 60 | Span buffer = stackalloc char[(int)length]; 61 | 62 | result = EventPipeGetProviderInfo(provider, buffer, out _); 63 | 64 | if (!result) 65 | { 66 | return result; 67 | } 68 | 69 | return new(result, buffer.WithoutNullTerminator()); 70 | } 71 | 72 | public HResult EventPipeDefineEvent(EVENTPIPE_PROVIDER provider, string eventName, uint eventID, ulong keywords, uint eventVersion, 73 | uint level, byte opcode, bool needStack, ReadOnlySpan paramDescs) 74 | { 75 | fixed (COR_PRF_EVENTPIPE_PARAM_DESC* pParamDescs = paramDescs) 76 | fixed (char* pEventName = eventName) 77 | { 78 | var result = _impl.EventPipeDefineEvent(provider, pEventName, eventID, keywords, eventVersion, level, opcode, needStack ? 1 : 0, (uint)paramDescs.Length, pParamDescs, out var @event); 79 | return new(result, @event); 80 | } 81 | } 82 | 83 | 84 | public HResult EventPipeWriteEvent(EVENTPIPE_EVENT @event, ReadOnlySpan data, in Guid pActivityId, in Guid pRelatedActivityId) 85 | { 86 | fixed (COR_PRF_EVENT_DATA* pData = data) 87 | { 88 | return _impl.EventPipeWriteEvent(@event, (uint)data.Length, pData, pActivityId, pRelatedActivityId); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo4.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo4 : ICorProfilerInfo3, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo4Invoker _impl; 6 | 7 | public ICorProfilerInfo4(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo4 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo4.Guid; 14 | 15 | public HResult> EnumThreads() 16 | { 17 | var result = _impl.EnumThreads(out var pEnum); 18 | return new(result, new(pEnum)); 19 | } 20 | 21 | public HResult InitializeCurrentThread() 22 | { 23 | return _impl.InitializeCurrentThread(); 24 | } 25 | 26 | public unsafe HResult RequestReJIT(ReadOnlySpan moduleIds, ReadOnlySpan methodIds) 27 | { 28 | if (moduleIds.Length != methodIds.Length) 29 | { 30 | throw new ArgumentException("moduleIds and methodIds must have the same length."); 31 | } 32 | 33 | fixed (ModuleId* pModuleIds = moduleIds) 34 | fixed (MdMethodDef* pMethodIds = methodIds) 35 | { 36 | return _impl.RequestReJIT((uint)moduleIds.Length, pModuleIds, pMethodIds); 37 | } 38 | } 39 | 40 | public unsafe HResult RequestRevert(ReadOnlySpan moduleIds, ReadOnlySpan methodIds, Span status) 41 | { 42 | if (moduleIds.Length != methodIds.Length || moduleIds.Length != status.Length) 43 | { 44 | throw new ArgumentException("moduleIds, methodIds, and status must have the same length."); 45 | } 46 | 47 | fixed (ModuleId* moduleIdsPtr = moduleIds) 48 | fixed (MdMethodDef* methodIdsPtr = methodIds) 49 | fixed (HResult* statusPtr = status) 50 | { 51 | return _impl.RequestRevert((uint)moduleIds.Length, moduleIdsPtr, methodIdsPtr, statusPtr); 52 | } 53 | } 54 | 55 | public unsafe HResult GetCodeInfo3(FunctionId functionID, ReJITId reJitId, Span codeInfos, out uint nbCodeInfos) 56 | { 57 | fixed (COR_PRF_CODE_INFO* pCodeInfos = codeInfos) 58 | { 59 | return _impl.GetCodeInfo3(functionID, reJitId, (uint)codeInfos.Length, out nbCodeInfos, pCodeInfos); 60 | } 61 | } 62 | 63 | public HResult GetFunctionFromIP2(nint ip) 64 | { 65 | var result = _impl.GetFunctionFromIP2(ip, out var functionId, out var reJitId); 66 | return new(result, new(functionId, reJitId)); 67 | } 68 | 69 | public unsafe HResult GetReJITIDs(FunctionId functionId, Span reJitIds, out uint nbReJitIds) 70 | { 71 | fixed (ReJITId* pReJitIds = reJitIds) 72 | { 73 | return _impl.GetReJITIDs(functionId, (uint)reJitIds.Length, out nbReJitIds, pReJitIds); 74 | } 75 | } 76 | 77 | public unsafe HResult GetILToNativeMapping2(FunctionId functionId, ReJITId reJitId, Span map, out uint mapLength) 78 | { 79 | fixed (COR_DEBUG_IL_TO_NATIVE_MAP* pMap = map) 80 | { 81 | return _impl.GetILToNativeMapping2(functionId, reJitId, (uint)map.Length, out mapLength, pMap); 82 | } 83 | } 84 | 85 | public HResult> EnumJITedFunctions2() 86 | { 87 | var result = _impl.EnumJITedFunctions2(out var pEnum); 88 | return new(result, new(pEnum)); 89 | } 90 | 91 | public HResult GetObjectSize2(ObjectId objectId) 92 | { 93 | var result = _impl.GetObjectSize2(objectId, out var pcSize); 94 | return new(result, pcSize); 95 | } 96 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Silhouette - A library to build .NET profilers in .NET 2 | ======================= 3 | 4 | # Quick start 5 | 6 | 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. 7 | 8 | ```csharp 9 | 10 | using Silhouette; 11 | 12 | [Profiler("0A96F866-D763-4099-8E4E-ED1801BE9FBC")] // Use your own profiler GUID here 13 | internal partial class CorProfilerCallback : CorProfilerCallback11Base 14 | { 15 | protected override HResult Initialize(int iCorProfilerInfoVersion) 16 | { 17 | if (iCorProfilerInfoVersion < 11) 18 | { 19 | return HResult.E_FAIL; 20 | } 21 | 22 | var result = ICorProfilerInfo11.SetEventMask(COR_PRF_MONITOR.COR_PRF_ENABLE_STACK_SNAPSHOT | COR_PRF_MONITOR.COR_PRF_MONITOR_THREADS); 23 | 24 | return result; 25 | } 26 | } 27 | ``` 28 | 29 | 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. 30 | 31 | ```csharp 32 | using Silhouette; 33 | using System.Runtime.InteropServices; 34 | 35 | internal class DllMain 36 | { 37 | // This code is automatically generated when using the `[Profiler]` attribute on `CorProfilerCallback` 38 | [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")] 39 | public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv) 40 | { 41 | // Use your own profiler GUID here 42 | if (*rclsid != new Guid("0A96F866-D763-4099-8E4E-ED1801BE9FBC")) 43 | { 44 | return HResult.CORPROF_E_PROFILER_CANCEL_ACTIVATION; 45 | } 46 | 47 | *ppv = ClassFactory.For(new CorProfilerBase()); 48 | 49 | return HResult.S_OK; 50 | } 51 | } 52 | ``` 53 | 54 | `CorProfilerXxBase` offers base virtual methods for all `ICorProfilerCallback` methods, so override the ones you're interested in: 55 | 56 | ```csharp 57 | protected override HResult ThreadCreated(ThreadId threadId) 58 | { 59 | Console.WriteLine($"Thread created: {threadId.Value}"); 60 | return HResult.S_OK; 61 | } 62 | ``` 63 | 64 | Use the `ICorProfilerInfoXx` fields to access the `ICorProfilerInfo` APIs: 65 | 66 | ```csharp 67 | private unsafe string ResolveMethodName(nint ip) 68 | { 69 | try 70 | { 71 | var functionId = ICorProfilerInfo11.GetFunctionFromIP(ip).ThrowIfFailed(); 72 | var functionInfo = ICorProfilerInfo2.GetFunctionInfo(functionId).ThrowIfFailed(); 73 | using var metaDataImport = ICorProfilerInfo2.GetModuleMetaDataImport(functionInfo.ModuleId, CorOpenFlags.ofRead).ThrowIfFailed().Wrap(); 74 | var methodProperties = metaDataImport.Value.GetMethodProps(new MdMethodDef(functionInfo.Token)).ThrowIfFailed(); 75 | var typeDefProps = metaDataImport.Value.GetTypeDefProps(methodProperties.Class).ThrowIfFailed(); 76 | 77 | return $"{typeDefProps.TypeName}.{methodProperties.Name}"; 78 | } 79 | catch (Win32Exception) 80 | { 81 | return ""; 82 | } 83 | } 84 | ``` 85 | 86 | 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`. 87 | -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo4.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo4 : ICorProfilerInfo3 5 | { 6 | public new static readonly Guid Guid = new("0d8fdcaa-6257-47bf-b1bf-94dac88466ee"); 7 | 8 | HResult EnumThreads(out nint ppEnum); 9 | HResult InitializeCurrentThread(); 10 | 11 | /* 12 | * Call RequestReJIT to have the runtime re-JIT a particular set of methods. 13 | * A code profiler can then adjust the code generated when the method is 14 | * re-JITed through the ICorProfilerFunctionControl interface. This does 15 | * not impact currently executing methods, only future invocations. 16 | * 17 | * A return code of S_OK indicates that all of the requested methods were 18 | * attempted to be rejitted. However, the profiler must implement 19 | * ICorProfilerCallback4::ReJITError to determine which of the methods were 20 | * successfully re-JITed. 21 | * 22 | * A failure return value (E_*) indicates some failure that prevents any 23 | * re-JITs. 24 | */ 25 | HResult RequestReJIT( 26 | uint cFunctions, 27 | ModuleId* moduleIds, 28 | MdMethodDef* methodIds); 29 | 30 | /* 31 | * RequestRevert will instruct the runtime to revert to using/calling the 32 | * original method (original IL and flags) rather than whatever was 33 | * ReJITed. This does not change any currently active methods, only future 34 | * invocations. 35 | * 36 | */ 37 | HResult RequestRevert( 38 | uint cFunctions, 39 | ModuleId* moduleIds, 40 | MdMethodDef* methodIds, 41 | HResult* status); 42 | 43 | /* 44 | * Same as GetCodeInfo2, except instead of always returning the code info 45 | * associated with the original IL/function, you can request the code info 46 | * for a particular re-JITed version of a function. 47 | */ 48 | HResult GetCodeInfo3( 49 | FunctionId functionID, 50 | ReJITId reJitId, 51 | uint cCodeInfos, 52 | out uint pcCodeInfos, 53 | COR_PRF_CODE_INFO* codeInfos); 54 | 55 | /* 56 | * Same as GetFunctionFromIP, but also returns which re-JITed version is 57 | * associated with the IP address. 58 | */ 59 | HResult GetFunctionFromIP2( 60 | nint ip, 61 | out FunctionId functionId, 62 | out ReJITId reJitId); 63 | 64 | /* 65 | * GetReJITIDs can be used to find all of the re-JITed versions of the 66 | * given function. 67 | */ 68 | HResult GetReJITIDs( 69 | FunctionId functionId, 70 | uint cReJitIds, 71 | out uint pcReJitIds, 72 | ReJITId* reJitIds); 73 | 74 | /* 75 | * Same as GetILToNativeMapping, but allows the code profiler to specify 76 | * which re-JITed version it applies to. 77 | */ 78 | HResult GetILToNativeMapping2( 79 | FunctionId functionId, 80 | ReJITId reJitId, 81 | uint cMap, 82 | out uint pcMap, 83 | COR_DEBUG_IL_TO_NATIVE_MAP* map); 84 | 85 | /* 86 | * Returns an enumerator for all previously jitted functions. May overlap with 87 | * functions previously reported via CompilationStarted callbacks. The returned 88 | * enumeration will include values for the COR_PRF_FUNCTION::reJitId field 89 | */ 90 | HResult EnumJITedFunctions2(out nint ppEnum); 91 | 92 | /* 93 | * The code profiler calls GetObjectSize to obtain the size of an object. 94 | * Note that types like arrays and strings may have a different size for each object. 95 | */ 96 | HResult GetObjectSize2( 97 | ObjectId objectId, 98 | out nint pcSize); 99 | 100 | } -------------------------------------------------------------------------------- /src/Silhouette/CorProfilerCallback2Base.cs: -------------------------------------------------------------------------------- 1 | using Silhouette.Interfaces; 2 | 3 | namespace Silhouette; 4 | 5 | public abstract class CorProfilerCallback2Base : CorProfilerCallbackBase, ICorProfilerCallback2 6 | { 7 | private readonly NativeObjects.ICorProfilerCallback2 _corProfilerCallback2; 8 | 9 | protected CorProfilerCallback2Base() 10 | { 11 | _corProfilerCallback2 = NativeObjects.ICorProfilerCallback2.Wrap(this); 12 | } 13 | 14 | protected override HResult QueryInterface(in Guid guid, out nint ptr) 15 | { 16 | if (guid == ICorProfilerCallback2.Guid) 17 | { 18 | ptr = _corProfilerCallback2; 19 | return HResult.S_OK; 20 | } 21 | 22 | return base.QueryInterface(guid, out ptr); 23 | } 24 | 25 | #region ICorProfilerCallback2 26 | 27 | unsafe HResult ICorProfilerCallback2.GarbageCollectionStarted(int cGenerations, int* generationCollected, COR_PRF_GC_REASON reason) 28 | { 29 | Span generationCollectedAsBool = stackalloc bool[cGenerations]; 30 | 31 | for (int i = 0; i < cGenerations; i++) 32 | { 33 | generationCollectedAsBool[i] = generationCollected[i] != 0; 34 | } 35 | 36 | return GarbageCollectionStarted(generationCollectedAsBool, reason); 37 | } 38 | 39 | unsafe HResult ICorProfilerCallback2.SurvivingReferences(uint cSurvivingObjectIDRanges, ObjectId* objectIDRangeStart, uint* cObjectIDRangeLength) 40 | { 41 | return SurvivingReferences(cSurvivingObjectIDRanges, objectIDRangeStart, cObjectIDRangeLength); 42 | } 43 | 44 | HResult ICorProfilerCallback2.GarbageCollectionFinished() 45 | { 46 | return GarbageCollectionFinished(); 47 | } 48 | 49 | HResult ICorProfilerCallback2.FinalizeableObjectQueued(COR_PRF_FINALIZER_FLAGS finalizerFlags, ObjectId objectID) 50 | { 51 | return FinalizeableObjectQueued(finalizerFlags, objectID); 52 | } 53 | 54 | unsafe HResult ICorProfilerCallback2.RootReferences2(uint cRootRefs, ObjectId* rootRefIds, COR_PRF_GC_ROOT_KIND* rootKinds, COR_PRF_GC_ROOT_FLAGS* rootFlags, uint* rootIds) 55 | { 56 | return RootReferences2(cRootRefs, rootRefIds, rootKinds, rootFlags, rootIds); 57 | } 58 | 59 | HResult ICorProfilerCallback2.HandleCreated(GCHandleId handleId, ObjectId initialObjectId) 60 | { 61 | return HandleCreated(handleId, initialObjectId); 62 | } 63 | 64 | HResult ICorProfilerCallback2.HandleDestroyed(GCHandleId handleId) 65 | { 66 | return HandleDestroyed(handleId); 67 | } 68 | 69 | unsafe HResult ICorProfilerCallback2.ThreadNameChanged(ThreadId threadId, uint cchName, char* name) 70 | { 71 | return ThreadNameChanged(threadId, cchName, name); 72 | } 73 | 74 | #endregion 75 | 76 | protected virtual unsafe HResult ThreadNameChanged(ThreadId threadId, uint cchName, char* name) 77 | { 78 | return HResult.E_NOTIMPL; 79 | } 80 | 81 | protected virtual HResult GarbageCollectionStarted(Span generationCollected, COR_PRF_GC_REASON reason) 82 | { 83 | return HResult.E_NOTIMPL; 84 | } 85 | 86 | protected virtual unsafe HResult SurvivingReferences(uint cSurvivingObjectIDRanges, ObjectId* objectIDRangeStart, uint* cObjectIDRangeLength) 87 | { 88 | return HResult.E_NOTIMPL; 89 | } 90 | 91 | protected virtual HResult GarbageCollectionFinished() 92 | { 93 | return HResult.E_NOTIMPL; 94 | } 95 | 96 | protected virtual HResult FinalizeableObjectQueued(COR_PRF_FINALIZER_FLAGS finalizerFlags, ObjectId objectID) 97 | { 98 | return HResult.E_NOTIMPL; 99 | } 100 | 101 | protected virtual unsafe HResult RootReferences2(uint cRootRefs, ObjectId* rootRefIds, COR_PRF_GC_ROOT_KIND* rootKinds, COR_PRF_GC_ROOT_FLAGS* rootFlags, uint* rootIds) 102 | { 103 | return HResult.E_NOTIMPL; 104 | } 105 | 106 | protected virtual HResult HandleCreated(GCHandleId handleId, ObjectId initialObjectId) 107 | { 108 | return HResult.E_NOTIMPL; 109 | } 110 | 111 | protected virtual HResult HandleDestroyed(GCHandleId handleId) 112 | { 113 | return HResult.E_NOTIMPL; 114 | } 115 | } -------------------------------------------------------------------------------- /src/Silhouette/IMetaDataImport2.cs: -------------------------------------------------------------------------------- 1 | using NativeObjects; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Silhouette; 5 | 6 | public unsafe class IMetaDataImport2 : IMetaDataImport 7 | { 8 | private readonly IMetaDataImport2Invoker _impl; 9 | 10 | public IMetaDataImport2(nint ptr) 11 | : base(ptr) 12 | { 13 | _impl = new(ptr); 14 | } 15 | 16 | public HResult EnumGenericParams( 17 | ref HCORENUM phEnum, 18 | MdToken tk, 19 | Span genericParams, 20 | out uint nbGenericParams) 21 | { 22 | fixed (MdGenericParam* pGenericParams = genericParams) 23 | { 24 | return _impl.EnumGenericParams((HCORENUM*)Unsafe.AsPointer(ref phEnum), tk, pGenericParams, (uint)genericParams.Length, out nbGenericParams); 25 | } 26 | } 27 | 28 | public HResult GetGenericParamProps( 29 | MdGenericParam gp, 30 | Span name, 31 | out uint nameLength) 32 | { 33 | fixed (char* pName = name) 34 | { 35 | var result = _impl.GetGenericParamProps(gp, out var paramSeq, out var paramFlags, out var owner, out var reserved, pName, (uint)name.Length, out nameLength); 36 | return new(result, new(paramSeq, (CorGenericParamAttr)paramFlags, owner, reserved)); 37 | } 38 | } 39 | 40 | public HResult GetGenericParamProps(MdGenericParam gp) 41 | { 42 | var (result, _) = GetGenericParamProps(gp, [], out var length); 43 | 44 | if (!result) 45 | { 46 | return result; 47 | } 48 | 49 | Span buffer = stackalloc char[(int)length]; 50 | (result, var props) = GetGenericParamProps(gp, buffer, out _); 51 | 52 | if (!result) 53 | { 54 | return result; 55 | } 56 | 57 | return new(result, new(props.ParamSeq, props.ParamFlags, props.Owner, props.Reserved, buffer.WithoutNullTerminator())); 58 | } 59 | 60 | public HResult GetMethodSpecProps(MdMethodSpec mi) 61 | { 62 | var result = _impl.GetMethodSpecProps(mi, out var parent, out var sigBlob, out var sigBlobSize); 63 | return new(result, new(parent, new(sigBlob, (int)sigBlobSize))); 64 | } 65 | 66 | public HResult EnumGenericParamConstraints( 67 | ref HCORENUM hEnum, 68 | MdGenericParam tk, 69 | Span genericParamConstraints, 70 | out uint nbGenericParamConstraints) 71 | { 72 | fixed (MdGenericParamConstraint* pGenericParamConstraints = genericParamConstraints) 73 | { 74 | return _impl.EnumGenericParamConstraints((HCORENUM*)Unsafe.AsPointer(ref hEnum), tk, pGenericParamConstraints, (uint)genericParamConstraints.Length, out nbGenericParamConstraints); 75 | } 76 | } 77 | 78 | public HResult GetGenericParamConstraintProps(MdGenericParamConstraint gpc) 79 | { 80 | var result = _impl.GetGenericParamConstraintProps(gpc, out var genericParam, out var constraintType); 81 | return new(result, new(genericParam, constraintType)); 82 | } 83 | 84 | public HResult GetPEKind() 85 | { 86 | var result = _impl.GetPEKind(out var peKind, out var machine); 87 | return new(result, new((CorPEKind)peKind, machine)); 88 | } 89 | 90 | public HResult GetVersionString( 91 | Span buffer, 92 | out uint length) 93 | { 94 | fixed (char* pBuffer = buffer) 95 | { 96 | var result = _impl.GetVersionString(pBuffer, (uint)buffer.Length, out length); 97 | return new(result); 98 | } 99 | } 100 | 101 | public HResult GetVersionString() 102 | { 103 | var result = GetVersionString([], out var length); 104 | 105 | if (!result) 106 | { 107 | return result; 108 | } 109 | 110 | Span buffer = stackalloc char[(int)length]; 111 | result = GetVersionString(buffer, out _); 112 | 113 | if (!result) 114 | { 115 | return result; 116 | } 117 | 118 | return new(result, buffer.WithoutNullTerminator()); 119 | } 120 | 121 | public HResult EnumMethodSpecs( 122 | ref HCORENUM phEnum, 123 | MdToken tk, 124 | Span methodSpecs, 125 | out uint nbMethodSpecs) 126 | { 127 | fixed (MdMethodSpec* pMethodSpecs = methodSpecs) 128 | { 129 | return _impl.EnumMethodSpecs((HCORENUM*)Unsafe.AsPointer(ref phEnum), tk, pMethodSpecs, (uint)methodSpecs.Length, out nbMethodSpecs); 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/Silhouette.IL/CorLibTypes.cs: -------------------------------------------------------------------------------- 1 | using dnlib.DotNet; 2 | 3 | namespace Silhouette.IL; 4 | 5 | public class CorLibTypes : ICorLibTypes, IDisposable 6 | { 7 | private readonly ComPtr _metadataImport; 8 | private readonly ComPtr _corLibMetadataImport; 9 | 10 | public static HResult Create(ComPtr metadataImport, ICorProfilerInfo3 corProfilerInfo) 11 | { 12 | var (result, corLib) = FindCorLib(corProfilerInfo); 13 | 14 | if (!result) 15 | { 16 | return result; 17 | } 18 | 19 | using var corLibPtr = corLib.Wrap(); 20 | 21 | return new CorLibTypes(metadataImport, corLibPtr); 22 | } 23 | 24 | private CorLibTypes(ComPtr metadataImport, ComPtr corLibMetadataImport) 25 | { 26 | _metadataImport = metadataImport.Copy(); 27 | _corLibMetadataImport = corLibMetadataImport.Copy(); 28 | } 29 | 30 | private static HResult FindCorLib(ICorProfilerInfo3 corProfilerInfo) 31 | { 32 | var (result, moduleEnumerator) = corProfilerInfo.EnumModules(); 33 | 34 | if (!result) 35 | { 36 | Console.WriteLine($"Failed to enumerate modules: {result}"); 37 | return result; 38 | } 39 | 40 | using var _ = moduleEnumerator; 41 | 42 | foreach (var module in moduleEnumerator.AsEnumerable()) 43 | { 44 | (result, var props) = corProfilerInfo.GetModuleInfo(module); 45 | 46 | if (!result) 47 | { 48 | continue; 49 | } 50 | 51 | var moduleName = Path.GetFileNameWithoutExtension(props.ModuleName); 52 | 53 | if ("mscorlib".Equals(moduleName, StringComparison.OrdinalIgnoreCase) 54 | || "System.Private.CoreLib".Equals(moduleName, StringComparison.OrdinalIgnoreCase)) 55 | { 56 | // TODO: use ofWrite only if needed 57 | // TODO: double check if this is the correct module 58 | return corProfilerInfo.GetModuleMetaDataImport2(module, CorOpenFlags.ofRead | CorOpenFlags.ofWrite); 59 | } 60 | } 61 | 62 | return new(HResult.E_FAIL, default); 63 | } 64 | 65 | public void Dispose() 66 | { 67 | _metadataImport.Dispose(); 68 | } 69 | 70 | public TypeRef GetTypeRef(string @namespace, string name) 71 | { 72 | Console.WriteLine($"GetTypeRef({@namespace}, {name})"); 73 | throw new NotImplementedException(); 74 | } 75 | 76 | public CorLibTypeSig Void => new(new TypeDefUser("void"), ElementType.Void); 77 | public CorLibTypeSig Boolean => ResolveTypeSig("System.Boolean", ElementType.Boolean); 78 | public CorLibTypeSig Char => ResolveTypeSig("System.Char", ElementType.Char); 79 | public CorLibTypeSig SByte => ResolveTypeSig("System.SByte", ElementType.I1); 80 | public CorLibTypeSig Byte => ResolveTypeSig("System.Byte", ElementType.U1); 81 | public CorLibTypeSig Int16 => ResolveTypeSig("System.Int16", ElementType.I2); 82 | public CorLibTypeSig UInt16 => ResolveTypeSig("System.UInt16", ElementType.U2); 83 | public CorLibTypeSig Int32 => ResolveTypeSig("System.Int32", ElementType.I4); 84 | public CorLibTypeSig UInt32 => ResolveTypeSig("System.UInt32", ElementType.U4); 85 | public CorLibTypeSig Int64 => ResolveTypeSig("System.Int64", ElementType.I8); 86 | public CorLibTypeSig UInt64 => ResolveTypeSig("System.UInt64", ElementType.U8); 87 | public CorLibTypeSig Single => ResolveTypeSig("System.Single", ElementType.R4); 88 | public CorLibTypeSig Double => ResolveTypeSig("System.Double", ElementType.R8); 89 | public CorLibTypeSig String => ResolveTypeSig("System.String", ElementType.String); 90 | public CorLibTypeSig TypedReference => ResolveTypeSig("System.TypedReference", ElementType.TypedByRef); 91 | public CorLibTypeSig IntPtr => ResolveTypeSig("System.IntPtr", ElementType.I); 92 | public CorLibTypeSig UIntPtr => ResolveTypeSig("System.UIntPtr", ElementType.U); 93 | public CorLibTypeSig Object => ResolveTypeSig("System.Object", ElementType.Object); 94 | public AssemblyRef AssemblyRef 95 | { 96 | get 97 | { 98 | Console.WriteLine("AssemblyRef requested, returning null."); 99 | return null; 100 | } 101 | } 102 | 103 | private CorLibTypeSig ResolveTypeSig(string name, ElementType elementType) 104 | { 105 | var (result, _) = _corLibMetadataImport.Value.FindTypeDefByName(name, default); 106 | if (!result) 107 | { 108 | Console.WriteLine($"Failed to find type definition for {name}: {result}"); 109 | return default; 110 | } 111 | return new(new TypeDefUser(name), elementType); 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /src/TestApp/ExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace TestApp; 4 | 5 | internal class ExceptionTests : ITest 6 | { 7 | public void Run() 8 | { 9 | var threadId = (IntPtr)typeof(Thread).GetField("_DONT_USE_InternalThread", BindingFlags.Instance | BindingFlags.NonPublic)! 10 | .GetValue(Thread.CurrentThread)!; 11 | 12 | try 13 | { 14 | throw new InvalidOperationException("Expected"); 15 | } 16 | catch (Exception) 17 | { 18 | try 19 | { 20 | throw new TaskCanceledException("Expected"); 21 | } 22 | catch (OperationCanceledException) when (ExceptionFilter1()) 23 | { 24 | } 25 | } 26 | 27 | try 28 | { 29 | Finally1(); 30 | } 31 | catch 32 | { 33 | // Expected 34 | } 35 | 36 | var logs = Logs.Fetch().ToList(); 37 | 38 | Logs.AssertContains(logs, "ExceptionCatcherEnter - catch System.InvalidOperationException in TestApp.ExceptionTests.Run"); 39 | Logs.AssertContains(logs, "ExceptionCatcherEnter - catch System.Threading.Tasks.TaskCanceledException in TestApp.ExceptionTests.Run"); 40 | Logs.AssertContains(logs, $"ExceptionCatcherLeave - Thread {threadId:x2} - Nested level 1"); 41 | Logs.AssertContains(logs, $"ExceptionCatcherLeave - Thread {threadId:x2} - Nested level 0"); 42 | Logs.AssertContains(logs, "ExceptionSearchFilterEnter - TestApp.ExceptionTests.Run"); 43 | Logs.AssertContains(logs, "ExceptionSearchFilterEnter - TestApp.ExceptionTests.ExceptionFilter1"); 44 | Logs.AssertContains(logs, $"ExceptionSearchFilterLeave - Thread {threadId:x2} - Nested level 1"); 45 | Logs.AssertContains(logs, $"ExceptionSearchFilterLeave - Thread {threadId:x2} - Nested level 0"); 46 | Logs.AssertContains(logs, "ExceptionSearchFunctionEnter - TestApp.ExceptionTests.Run"); 47 | Logs.AssertContains(logs, "ExceptionSearchFunctionEnter - TestApp.ExceptionTests.ExceptionFilter1"); 48 | Logs.AssertContains(logs, $"ExceptionSearchFunctionLeave - Thread {threadId:x2} - Nested level 1"); 49 | Logs.AssertContains(logs, $"ExceptionSearchFunctionLeave - Thread {threadId:x2} - Nested level 0"); 50 | Logs.AssertContains(logs, "ExceptionUnwindFinallyEnter - TestApp.ExceptionTests.Finally1"); 51 | Logs.AssertContains(logs, "ExceptionUnwindFinallyEnter - TestApp.ExceptionTests.Finally2"); 52 | Logs.AssertContains(logs, $"ExceptionUnwindFinallyLeave - Thread {threadId:x2} - Nested level 1"); 53 | Logs.AssertContains(logs, $"ExceptionUnwindFinallyLeave - Thread {threadId:x2} - Nested level 0"); 54 | Logs.AssertContains(logs, "ExceptionUnwindFunctionEnter - TestApp.ExceptionTests.Run"); 55 | Logs.AssertContains(logs, "ExceptionUnwindFunctionEnter - TestApp.ExceptionTests.ExceptionFilter1"); 56 | Logs.AssertContains(logs, "ExceptionUnwindFunctionEnter - TestApp.ExceptionTests.Finally1"); 57 | Logs.AssertContains(logs, "ExceptionUnwindFunctionEnter - TestApp.ExceptionTests.Finally2"); 58 | Logs.AssertContains(logs, $"ExceptionUnwindFunctionLeave - Thread {threadId:x2} - Nested level 1"); 59 | Logs.AssertContains(logs, $"ExceptionUnwindFunctionLeave - Thread {threadId:x2} - Nested level 0"); 60 | Logs.AssertContains(logs, "ExceptionSearchCatcherFound - TestApp.ExceptionTests.Run"); 61 | Logs.AssertContains(logs, "ExceptionSearchCatcherFound - TestApp.ExceptionTests.ExceptionFilter1"); 62 | Logs.AssertContains(logs, "ExceptionSearchCatcherFound - TestApp.ExceptionTests.Finally1"); 63 | Logs.AssertContains(logs, "ExceptionThrown - System.Exception"); 64 | Logs.AssertContains(logs, "ExceptionThrown - System.NotSupportedException"); 65 | Logs.AssertContains(logs, "ExceptionThrown - System.InvalidCastException"); 66 | Logs.AssertContains(logs, "ExceptionThrown - System.InvalidOperationException"); 67 | Logs.AssertContains(logs, "ExceptionThrown - System.Threading.Tasks.TaskCanceledException"); 68 | } 69 | 70 | private static bool ExceptionFilter1() 71 | { 72 | try 73 | { 74 | throw new Exception("Expected"); 75 | } 76 | catch (Exception) when (ExceptionFilter2()) 77 | { 78 | } 79 | 80 | return true; 81 | } 82 | 83 | private static bool ExceptionFilter2() 84 | { 85 | return true; 86 | } 87 | 88 | private static void Finally1() 89 | { 90 | try 91 | { 92 | throw new NotSupportedException("Finally"); 93 | } 94 | finally 95 | { 96 | try 97 | { 98 | Finally2(); 99 | } 100 | catch 101 | { 102 | // Expected 103 | } 104 | } 105 | } 106 | 107 | private static void Finally2() 108 | { 109 | try 110 | { 111 | throw new InvalidCastException("Finally"); 112 | } 113 | finally 114 | { 115 | GC.KeepAlive(null); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback : IUnknown 5 | { 6 | public static readonly Guid Guid = Guid.Parse("8a8cc829-ccf2-49fe-bbae-0f022228071a"); 7 | 8 | HResult Initialize(nint pICorProfilerInfoUnk); 9 | 10 | HResult Shutdown(); 11 | 12 | HResult AppDomainCreationStarted(AppDomainId appDomainId); 13 | HResult AppDomainCreationFinished(AppDomainId appDomainId, HResult hrStatus); 14 | 15 | HResult AppDomainShutdownStarted(AppDomainId appDomainId); 16 | HResult AppDomainShutdownFinished(AppDomainId appDomainId, HResult hrStatus); 17 | 18 | HResult AssemblyLoadStarted(AssemblyId assemblyId); 19 | HResult AssemblyLoadFinished(AssemblyId assemblyId, HResult hrStatus); 20 | 21 | HResult AssemblyUnloadStarted(AssemblyId assemblyId); 22 | HResult AssemblyUnloadFinished(AssemblyId assemblyId, HResult hrStatus); 23 | 24 | HResult ModuleLoadStarted(ModuleId moduleId); 25 | HResult ModuleLoadFinished(ModuleId moduleId, HResult hrStatus); 26 | 27 | HResult ModuleUnloadStarted(ModuleId moduleId); 28 | HResult ModuleUnloadFinished(ModuleId moduleId, HResult hrStatus); 29 | 30 | HResult ModuleAttachedToAssembly(ModuleId moduleId, AssemblyId assemblyId); 31 | 32 | HResult ClassLoadStarted(ClassId classId); 33 | HResult ClassLoadFinished(ClassId classId, HResult hrStatus); 34 | 35 | HResult ClassUnloadStarted(ClassId classId); 36 | HResult ClassUnloadFinished(ClassId classId, HResult hrStatus); 37 | 38 | HResult FunctionUnloadStarted(FunctionId functionId); 39 | 40 | HResult JITCompilationStarted(FunctionId functionId, int fIsSafeToBlock); 41 | HResult JITCompilationFinished(FunctionId functionId, HResult hrStatus, int fIsSafeToBlock); 42 | 43 | HResult JITCachedFunctionSearchStarted(FunctionId functionId, out int pbUseCachedFunction); 44 | HResult JITCachedFunctionSearchFinished(FunctionId functionId, COR_PRF_JIT_CACHE result); 45 | 46 | HResult JITFunctionPitched(FunctionId functionId); 47 | 48 | HResult JITInlining(FunctionId callerId, FunctionId calleeId, out int pfShouldInline); 49 | 50 | HResult ThreadCreated(ThreadId threadId); 51 | HResult ThreadDestroyed(ThreadId threadId); 52 | HResult ThreadAssignedToOSThread(ThreadId managedThreadId, int osThreadId); 53 | 54 | HResult RemotingClientInvocationStarted(); 55 | HResult RemotingClientSendingMessage(in Guid pCookie, int fIsAsync); 56 | HResult RemotingClientReceivingReply(in Guid pCookie, int fIsAsync); 57 | HResult RemotingClientInvocationFinished(); 58 | 59 | HResult RemotingServerReceivingMessage(in Guid pCookie, int fIsAsync); 60 | HResult RemotingServerInvocationStarted(); 61 | HResult RemotingServerInvocationReturned(); 62 | HResult RemotingServerSendingReply(in Guid pCookie, int fIsAsync); 63 | 64 | HResult UnmanagedToManagedTransition(FunctionId functionId, COR_PRF_TRANSITION_REASON reason); 65 | HResult ManagedToUnmanagedTransition(FunctionId functionId, COR_PRF_TRANSITION_REASON reason); 66 | 67 | HResult RuntimeSuspendStarted(COR_PRF_SUSPEND_REASON suspendReason); 68 | HResult RuntimeSuspendFinished(); 69 | HResult RuntimeSuspendAborted(); 70 | 71 | HResult RuntimeResumeStarted(); 72 | HResult RuntimeResumeFinished(); 73 | 74 | HResult RuntimeThreadSuspended(ThreadId threadId); 75 | HResult RuntimeThreadResumed(ThreadId threadId); 76 | 77 | HResult MovedReferences( 78 | uint cMovedObjectIDRanges, 79 | ObjectId* oldObjectIDRangeStart, 80 | ObjectId* newObjectIDRangeStart, 81 | uint* cObjectIDRangeLength); 82 | 83 | HResult ObjectAllocated(ObjectId objectId, ClassId classId); 84 | 85 | HResult ObjectsAllocatedByClass(uint cClassCount, ClassId* classIds, uint* cObjects); 86 | 87 | HResult ObjectReferences( 88 | ObjectId objectId, 89 | ClassId classId, 90 | uint cObjectRefs, 91 | ObjectId* objectRefIds); 92 | 93 | HResult RootReferences(uint cRootRefs, ObjectId* rootRefIds); 94 | 95 | HResult ExceptionThrown(ObjectId thrownObjectId); 96 | 97 | HResult ExceptionSearchFunctionEnter(FunctionId functionId); 98 | HResult ExceptionSearchFunctionLeave(); 99 | 100 | HResult ExceptionSearchFilterEnter(FunctionId functionId); 101 | HResult ExceptionSearchFilterLeave(); 102 | 103 | HResult ExceptionSearchCatcherFound(FunctionId functionId); 104 | 105 | HResult ExceptionOSHandlerEnter(nint* __unused); 106 | HResult ExceptionOSHandlerLeave(nint* __unused); 107 | 108 | HResult ExceptionUnwindFunctionEnter(FunctionId functionId); 109 | HResult ExceptionUnwindFunctionLeave(); 110 | 111 | HResult ExceptionUnwindFinallyEnter(FunctionId functionId); 112 | HResult ExceptionUnwindFinallyLeave(); 113 | 114 | HResult ExceptionCatcherEnter(FunctionId functionId, ObjectId objectId); 115 | HResult ExceptionCatcherLeave(); 116 | 117 | HResult COMClassicVTableCreated( 118 | ClassId wrappedClassId, 119 | in Guid implementedIID, 120 | void* pVTable, 121 | uint cSlots); 122 | 123 | HResult COMClassicVTableDestroyed( 124 | ClassId wrappedClassId, 125 | in Guid implementedIID, 126 | void* pVTable); 127 | 128 | HResult ExceptionCLRCatcherFound(); 129 | HResult ExceptionCLRCatcherExecute(); 130 | } -------------------------------------------------------------------------------- /src/Silhouette.SourceGenerator/ProfilerAttributeSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | namespace Silhouette.SourceGenerator; 8 | 9 | [Generator] 10 | public class ProfilerAttributeSourceGenerator : IIncrementalGenerator 11 | { 12 | public void Initialize(IncrementalGeneratorInitializationContext context) 13 | { 14 | var profilerProvider = context.SyntaxProvider.ForAttributeWithMetadataName("Silhouette.ProfilerAttribute", static (_, _) => true, Transform); 15 | 16 | context.RegisterSourceOutput(profilerProvider, static (ctx, state) => 17 | { 18 | var (profilerGuid, className, diagnostic) = state; 19 | 20 | if (diagnostic != null) 21 | { 22 | ctx.ReportDiagnostic(diagnostic); 23 | return; 24 | } 25 | 26 | string source = $$""" 27 | namespace Silhouette._Generated 28 | { 29 | using System; 30 | using System.Runtime.InteropServices; 31 | 32 | file static class DllMain 33 | { 34 | [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")] 35 | public static unsafe HResult DllGetClassObject(Guid* rclsid, Guid* riid, nint* ppv) 36 | { 37 | if (*rclsid != new Guid("{{profilerGuid}}")) 38 | { 39 | return HResult.CORPROF_E_PROFILER_CANCEL_ACTIVATION; 40 | } 41 | 42 | *ppv = ClassFactory.For(new {{className}}()); 43 | return HResult.S_OK; 44 | } 45 | } 46 | } 47 | """; 48 | 49 | ctx.AddSource("silhouette.dllmain.g.cs", source); 50 | }); 51 | } 52 | 53 | private static readonly DiagnosticDescriptor MissingGuidArgumentDescriptor = new( 54 | id: "SILH002", 55 | title: "MissingGuidArgumentDescriptor", 56 | messageFormat: "ProfilerAttribute must have a valid GUID argument.", 57 | category: "Silhouette.SourceGenerator", 58 | DiagnosticSeverity.Error, 59 | isEnabledByDefault: true); 60 | 61 | private static readonly DiagnosticDescriptor InvalidGuidDescriptor = new( 62 | id: "SILH001", 63 | title: "Invalid Profiler GUID", 64 | messageFormat: "The ProfilerAttribute argument '{0}' is not a valid GUID.", 65 | category: "Silhouette.SourceGenerator", 66 | DiagnosticSeverity.Error, 67 | isEnabledByDefault: true); 68 | 69 | private static (Guid ProfilerGuid, string? ClassName, Diagnostic? Error) Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) 70 | { 71 | // context.TargetNode is the ClassDeclarationSyntax 72 | var classDecl = (ClassDeclarationSyntax)context.TargetNode; 73 | var attributeSyntax = classDecl.AttributeLists 74 | .SelectMany(a => a.Attributes) 75 | .FirstOrDefault(a => context.SemanticModel.GetTypeInfo(a, cancellationToken).Type?.ToDisplayString() == "Silhouette.ProfilerAttribute"); 76 | 77 | if (attributeSyntax == null || attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList.Arguments.Count == 0) 78 | { 79 | var diagnostic = Diagnostic.Create( 80 | MissingGuidArgumentDescriptor, 81 | classDecl.Identifier.GetLocation()); 82 | return (Guid.Empty, null, diagnostic); 83 | } 84 | 85 | var guidExpr = attributeSyntax.ArgumentList.Arguments[0].Expression; 86 | string guidString; 87 | 88 | // Try to get constant value using semantic model 89 | var constantValue = context.SemanticModel.GetConstantValue(guidExpr, cancellationToken); 90 | if (constantValue.HasValue && constantValue.Value is string s) 91 | { 92 | guidString = s; 93 | } 94 | else if (guidExpr is LiteralExpressionSyntax literal && literal.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.StringLiteralExpression)) 95 | { 96 | guidString = literal.Token.ValueText; 97 | } 98 | else 99 | { 100 | var diagnostic = Diagnostic.Create( 101 | InvalidGuidDescriptor, 102 | guidExpr.GetLocation(), 103 | guidExpr); 104 | return (Guid.Empty, null, diagnostic); 105 | } 106 | 107 | if (!Guid.TryParse(guidString, out var guid)) 108 | { 109 | var diagnostic = Diagnostic.Create( 110 | InvalidGuidDescriptor, 111 | guidExpr.GetLocation(), 112 | guidString); 113 | return (Guid.Empty, null, diagnostic); 114 | } 115 | 116 | var fullClassName = context.TargetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 117 | return (guid, fullClassName, null); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | /nugets/** 254 | -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo3.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo3 : ICorProfilerInfo2, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo3Invoker _impl; 6 | 7 | public ICorProfilerInfo3(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo3 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo3.Guid; 14 | 15 | public HResult> EnumJITedFunctions() 16 | { 17 | var result = _impl.EnumJITedFunctions(out var pEnum); 18 | return new(result, new(pEnum)); 19 | } 20 | 21 | public HResult RequestProfilerDetach(int expectedCompletionMilliseconds) 22 | { 23 | return _impl.RequestProfilerDetach(expectedCompletionMilliseconds); 24 | } 25 | 26 | public unsafe HResult SetFunctionIDMapper2(delegate* unmanaged[Stdcall] pFunc, void* clientData) 27 | { 28 | return _impl.SetFunctionIDMapper2(pFunc, clientData); 29 | } 30 | 31 | public HResult GetStringLayout2() 32 | { 33 | var result = _impl.GetStringLayout2(out var stringLengthOffset, out var bufferOffset); 34 | return new(result, new(stringLengthOffset, bufferOffset)); 35 | } 36 | 37 | public HResult SetEnterLeaveFunctionHooks3(IntPtr funcEnter3, IntPtr funcLeave3, IntPtr funcTailcall3) 38 | { 39 | return _impl.SetEnterLeaveFunctionHooks3(funcEnter3, funcLeave3, funcTailcall3); 40 | } 41 | 42 | public HResult SetEnterLeaveFunctionHooks3WithInfo(IntPtr funcEnter3WithInfo, IntPtr funcLeave3WithInfo, IntPtr funcTailcall3WithInfo) 43 | { 44 | return _impl.SetEnterLeaveFunctionHooks3WithInfo(funcEnter3WithInfo, funcLeave3WithInfo, funcTailcall3WithInfo); 45 | } 46 | 47 | public unsafe HResult GetFunctionEnter3Info(FunctionId functionId, COR_PRF_ELT_INFO eltInfo, Span argumentInfo, out uint nbArgumentInfo) 48 | { 49 | fixed (COR_PRF_FUNCTION_ARGUMENT_INFO* pArgumentInfo = argumentInfo) 50 | { 51 | uint pcbArgumentInfo = (uint)argumentInfo.Length; 52 | var result = _impl.GetFunctionEnter3Info(functionId, eltInfo, out var frameInfo, &pcbArgumentInfo, pArgumentInfo); 53 | nbArgumentInfo = pcbArgumentInfo; 54 | return new(result, frameInfo); 55 | } 56 | } 57 | 58 | public HResult GetFunctionLeave3Info(FunctionId functionId, COR_PRF_ELT_INFO eltInfo) 59 | { 60 | var result = _impl.GetFunctionLeave3Info(functionId, eltInfo, out var frameInfo, out var retvalRange); 61 | return new(result, new(frameInfo, retvalRange)); 62 | } 63 | 64 | public HResult GetFunctionTailcall3Info(FunctionId functionId, COR_PRF_ELT_INFO eltInfo) 65 | { 66 | var result = _impl.GetFunctionTailcall3Info(functionId, eltInfo, out var pFrameInfo); 67 | return new(result, pFrameInfo); 68 | } 69 | 70 | public HResult> EnumModules() 71 | { 72 | var result = _impl.EnumModules(out var pEnum); 73 | return new(result, new(pEnum)); 74 | } 75 | 76 | public unsafe HResult GetRuntimeInformation(Span versionString, out uint versionStringLength) 77 | { 78 | fixed (char* szVersionString = versionString) 79 | { 80 | var result = _impl.GetRuntimeInformation(out var clrInstanceId, out var runtimeType, out var majorVersion, out var minorVersion, out var buildNumber, out var qfeVersion, (uint)versionString.Length, out versionStringLength, szVersionString); 81 | return new(result, new(clrInstanceId, runtimeType, majorVersion, minorVersion, buildNumber, qfeVersion)); 82 | } 83 | } 84 | 85 | public unsafe HResult GetThreadStaticAddress2(ClassId classId, MdFieldDef fieldToken, AppDomainId appDomainId, ThreadId threadId) 86 | { 87 | var result = _impl.GetThreadStaticAddress2(classId, fieldToken, appDomainId, threadId, out var address); 88 | return new(result, (nint)address); 89 | } 90 | 91 | public unsafe HResult GetAppDomainsContainingModule(ModuleId moduleId, Span appDomainIds, out uint nbAppDomainIds) 92 | { 93 | fixed (AppDomainId* cAppDomainIds = appDomainIds) 94 | { 95 | return _impl.GetAppDomainsContainingModule(moduleId, (uint)appDomainIds.Length, out nbAppDomainIds, cAppDomainIds); 96 | } 97 | } 98 | 99 | public unsafe HResult GetModuleInfo2(ModuleId moduleId, Span moduleName, out uint moduleNameLength) 100 | { 101 | fixed (char* szName = moduleName) 102 | { 103 | var result = _impl.GetModuleInfo2(moduleId, out var baseLoadAddress, (uint)moduleName.Length, out moduleNameLength, szName, out var assemblyId, out var moduleFlags); 104 | return new(result, new((nint)baseLoadAddress, assemblyId, moduleFlags)); 105 | } 106 | } 107 | 108 | public unsafe HResult GetModuleInfo2(ModuleId moduleId) 109 | { 110 | var (result, _) = GetModuleInfo2(moduleId, [], out var length); 111 | 112 | if (!result) 113 | { 114 | return result; 115 | } 116 | 117 | Span buffer = stackalloc char[(int)length]; 118 | 119 | (result, var moduleInfo) = GetModuleInfo2(moduleId, buffer, out _); 120 | 121 | if (!result) 122 | { 123 | return result; 124 | } 125 | 126 | return new(result, new(buffer.WithoutNullTerminator(), moduleInfo.BaseLoadAddress, moduleInfo.AssemblyId, moduleInfo.ModuleFlags)); 127 | } 128 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback4.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback4 : ICorProfilerCallback3 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("7B63B2E3-107D-4d48-B2F6-F61E229470D2"); 7 | /* 8 | * Similar to JITCompilationStarted, except called when rejitting a method 9 | */ 10 | HResult ReJITCompilationStarted( 11 | FunctionId functionId, 12 | ReJITId rejitId, 13 | int fIsSafeToBlock); 14 | 15 | /* 16 | * This is called exactly once per method (which may represent more than 17 | * one function id), to allow the code profiler to set alternate code 18 | * generation flags or a new method body. 19 | */ 20 | HResult GetReJITParameters( 21 | ModuleId moduleId, 22 | MdMethodDef methodId, 23 | IntPtr functionControl); 24 | 25 | /* 26 | * Similar to JITCompilationFinished, except called when rejitting a method 27 | */ 28 | HResult ReJITCompilationFinished( 29 | FunctionId functionId, 30 | ReJITId rejitId, 31 | HResult hrStatus, 32 | int fIsSafeToBlock); 33 | 34 | /* 35 | * This is called to report an error encountered while processing a ReJIT request. 36 | * This may either be called from within the RequestReJIT call itself, or called after 37 | * RequestReJIT returns, if the error was encountered later on. 38 | */ 39 | HResult ReJITError( 40 | ModuleId moduleId, 41 | MdMethodDef methodId, 42 | FunctionId functionId, 43 | HResult hrStatus); 44 | 45 | /* 46 | * The CLR calls MovedReferences with information about 47 | * object references that moved as a result of garbage collection. 48 | * 49 | * cMovedObjectIDRanges is a count of the number of ObjectID ranges that 50 | * were moved. 51 | * oldObjectIDRangeStart is an array of elements, each of which is the start 52 | * value of a range of ObjectID values before being moved. 53 | * newObjectIDRangeStart is an array of elements, each of which is the start 54 | * value of a range of ObjectID values after being moved. 55 | * cObjectIDRangeLength is an array of elements, each of which states the 56 | * size of the moved ObjectID value range. 57 | * 58 | * The last three arguments of this function are parallel arrays. 59 | * 60 | * In other words, if an ObjectID value lies within the range 61 | * oldObjectIDRangeStart[i] <= ObjectID < oldObjectIDRangeStart[i] + cObjectIDRangeLength[i] 62 | * for 0 <= i < cMovedObjectIDRanges, then the ObjectID value has changed to 63 | * ObjectID - oldObjectIDRangeStart[i] + newObjectIDRangeStart[i] 64 | * 65 | * NOTE: None of the objectIDs returned by MovedReferences are valid during the callback 66 | * itself, as the GC may be in the middle of moving objects from old to new. Thus profilers 67 | * should not attempt to inspect objects during a MovedReferences call. At 68 | * GarbageCollectionFinished, all objects have been moved to their new locations, and 69 | * inspection may be done. 70 | * 71 | * If the profiler implements ICorProfilerCallback4, ICorProfilerCallback4::MovedReferences2 72 | * is called first and ICorProfilerCallback::MovedReferences is called second but only if 73 | * ICorProfilerCallback4::MovedReferences2 returned success. Profilers can return failure 74 | * from ICorProfilerCallback4::MovedReferences2 to save some chattiness. 75 | */ 76 | HResult MovedReferences2( 77 | uint cMovedObjectIDRanges, 78 | ObjectId* oldObjectIDRangeStart, 79 | ObjectId* newObjectIDRangeStart, 80 | nint* cObjectIDRangeLength); 81 | 82 | /* 83 | * The CLR calls SurvivingReferences with information about 84 | * object references that survived a garbage collection. 85 | * 86 | * Generally, the CLR calls SurvivingReferences for non-compacting garbage collections. 87 | * For compacting garbage collections, MovedReferences is called instead. 88 | * 89 | * The exception to this rule is that the CLR always calls SurvivingReferences for objects 90 | * in the large object heap, which is not compacted. 91 | * 92 | * Multiple calls to SurvivingReferences may be received during a particular 93 | * garbage collection, due to limited internal buffering, multiple threads reporting 94 | * in the case of server gc, and other reasons. 95 | * In the case of multiple calls, the information is cumulative - all of the references 96 | * reported in any SurvivingReferences call survive this collection. 97 | * 98 | * cSurvivingObjectIDRanges is a count of the number of ObjectID ranges that 99 | * survived. 100 | * objectIDRangeStart is an array of elements, each of which is the start 101 | * value of a range of ObjectID values that survived the collection. 102 | * cObjectIDRangeLength is an array of elements, each of which states the 103 | * size of the surviving ObjectID value range. 104 | * 105 | * The last two arguments of this function are parallel arrays. 106 | * 107 | * In other words, if an ObjectID value lies within the range 108 | * objectIDRangeStart[i] <= ObjectID < objectIDRangeStart[i] + cObjectIDRangeLength[i] 109 | * for 0 <= i < cMovedObjectIDRanges, then the ObjectID has survived the collection 110 | * 111 | * If the profiler implements ICorProfilerCallback4, ICorProfilerCallback4::SurvivingReferences2 112 | * is called first and ICorProfilerCallback2::SurvivingReferences is called second but only if 113 | * ICorProfilerCallback4::SurvivingReferences2 returned success. Profilers can return failure 114 | * from ICorProfilerCallback4::SurvivingReferences2 to save some chattiness. 115 | */ 116 | HResult SurvivingReferences2( 117 | uint cSurvivingObjectIDRanges, 118 | ObjectId* objectIDRangeStart, 119 | nint* cObjectIDRangeLength); 120 | 121 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerInfo3.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerInfo3 : ICorProfilerInfo2 5 | { 6 | public new static readonly Guid Guid = new("B555ED4F-452A-4E54-8B39-B5360BAD32A0"); 7 | 8 | /* 9 | * Returns an enumerator for all previously jitted functions. May overlap with 10 | * functions previously reported via CompilationStarted callbacks. 11 | * NOTE: The returned enumeration will only include '0' for the value of the 12 | * COR_PRF_FUNCTION::reJitId field. If you require valid COR_PRF_FUNCTION::reJitId values, use 13 | * ICorProfilerInfo4::EnumJITedFunctions2. 14 | */ 15 | HResult EnumJITedFunctions(out IntPtr pEnum); 16 | 17 | HResult RequestProfilerDetach(int dwExpectedCompletionMilliseconds); 18 | 19 | HResult SetFunctionIDMapper2( 20 | delegate* unmanaged[Stdcall] pFunc, 21 | void* clientData); 22 | 23 | /* 24 | * GetStringLayout2 returns detailed information about how string objects are stored. 25 | * 26 | * *pStringLengthOffset is the offset (from the ObjectID pointer) to a int that 27 | * stores the length of the string itself 28 | * 29 | * *pBufferOffset is the offset (from the ObjectID pointer) to the actual buffer 30 | * of wide characters 31 | * 32 | * Strings may or may not be null-terminated. 33 | */ 34 | HResult GetStringLayout2( 35 | out uint pStringLengthOffset, 36 | out uint pBufferOffset); 37 | 38 | /* 39 | * The code profiler calls SetFunctionHooks3 to specify handlers 40 | * for FunctionEnter3, FunctionLeave3, and FunctionTailcall3, and calls 41 | * SetFunctionHooks3WithInfo to specify handlers for FunctionEnter3WithInfo, 42 | * FunctionLeave3WithInfo, and FunctionTailcall3WithInfo. 43 | * 44 | * Note that only one set of callbacks may be active at a time. Thus, 45 | * if a profiler calls SetEnterLeaveFunctionHooks, SetEnterLeaveFunctionHooks2 46 | * and SetEnterLeaveFunctionHooks3(WithInfo), then SetEnterLeaveFunctionHooks3(WithInfo) 47 | * wins. SetEnterLeaveFunctionHooks2 takes precedence over SetEnterLeaveFunctionHooks 48 | * when both are set. 49 | * 50 | * Each function pointer may be null to disable that callback. 51 | * 52 | * SetEnterLeaveFunctionHooks3(WithInfo) may only be called from the 53 | * profiler's Initialize() callback. 54 | */ 55 | HResult SetEnterLeaveFunctionHooks3( 56 | IntPtr pFuncEnter3, 57 | IntPtr pFuncLeave3, 58 | IntPtr pFuncTailcall3); 59 | 60 | 61 | HResult SetEnterLeaveFunctionHooks3WithInfo( 62 | IntPtr pFuncEnter3WithInfo, 63 | IntPtr pFuncLeave3WithInfo, 64 | IntPtr pFuncTailcall3WithInfo); 65 | 66 | /* 67 | * The profiler can call GetFunctionEnter3Info to gather frame info and argument info 68 | * in FunctionEnter3WithInfo callback. The profiler needs to allocate sufficient space 69 | * for COR_PRF_FUNCTION_ARGUMENT_INFO of the function it's inspecting and indicate the 70 | * size in a ULONG pointed by pcbArgumentInfo. 71 | */ 72 | HResult GetFunctionEnter3Info( 73 | FunctionId functionId, 74 | COR_PRF_ELT_INFO eltInfo, 75 | out COR_PRF_FRAME_INFO pFrameInfo, 76 | uint* pcbArgumentInfo, 77 | COR_PRF_FUNCTION_ARGUMENT_INFO* pArgumentInfo); 78 | 79 | /* 80 | * The profiler can call GetFunctionLeave3Info to gather frame info and return value 81 | * in FunctionLeave3WithInfo callback. 82 | */ 83 | HResult GetFunctionLeave3Info( 84 | FunctionId functionId, 85 | COR_PRF_ELT_INFO eltInfo, 86 | out COR_PRF_FRAME_INFO pFrameInfo, 87 | out COR_PRF_FUNCTION_ARGUMENT_RANGE pRetvalRange); 88 | 89 | /* 90 | * The profiler can call GetFunctionTailcall3Info to gather frame info in 91 | * FunctionTailcall3WithInfo callback. 92 | */ 93 | HResult GetFunctionTailcall3Info( 94 | FunctionId functionId, 95 | COR_PRF_ELT_INFO eltInfo, 96 | out COR_PRF_FRAME_INFO pFrameInfo); 97 | 98 | HResult EnumModules(out IntPtr pEnum); 99 | 100 | /* 101 | * The profiler can call GetRuntimeInformation to query CLR version information. 102 | * Passing NULL to any parameter is acceptable except pcchVersionString cannot 103 | * be NULL if szVersionString is not NULL. 104 | */ 105 | HResult GetRuntimeInformation(out ushort pClrInstanceId, 106 | out COR_PRF_RUNTIME_TYPE pRuntimeType, 107 | out ushort pMajorVersion, 108 | out ushort pMinorVersion, 109 | out ushort pBuildNumber, 110 | out ushort pQFEVersion, 111 | uint cchVersionString, 112 | out uint pcchVersionString, 113 | char* szVersionString); 114 | 115 | /* 116 | * GetThreadStaticAddress2 gets the address of the home for the given 117 | * Thread static in the given Thread. 118 | * 119 | * This function may return CORPROF_E_DATAINCOMPLETE if the given static 120 | * has not been assigned a home in the given Thread. 121 | */ 122 | HResult GetThreadStaticAddress2( 123 | ClassId classId, 124 | MdFieldDef fieldToken, 125 | AppDomainId appDomainId, 126 | ThreadId threadId, 127 | out void* ppAddress); 128 | 129 | /* 130 | * GetAppDomainsContainingModule returns the AppDomainIDs in which the 131 | * given module has been loaded 132 | */ 133 | HResult GetAppDomainsContainingModule( 134 | ModuleId moduleId, 135 | uint cAppDomainIds, 136 | out uint pcAppDomainIds, 137 | AppDomainId* appDomainIds); 138 | 139 | 140 | /* 141 | * Retrieve information about a given module. 142 | * 143 | * When the module is loaded from disk, the name returned will be the filename; 144 | * otherwise, the name will be the name from the metadata Module table (i.e., 145 | * the same as the managed System.Reflection.Module.ScopeName). 146 | * 147 | * *pdwModuleFlags will be filled in with a bitmask of values from COR_PRF_MODULE_FLAGS 148 | * that specify some properties of the module. 149 | * 150 | * NOTE: While this function may be called as soon as the moduleId is alive, 151 | * the AssemblyID of the containing assembly will not be available until the 152 | * ModuleAttachedToAssembly callback. 153 | * 154 | */ 155 | HResult GetModuleInfo2( 156 | ModuleId moduleId, 157 | out byte* ppBaseLoadAddress, 158 | uint cchName, 159 | out uint pcchName, 160 | char* szName, 161 | out AssemblyId pAssemblyId, 162 | out uint pdwModuleFlags); 163 | } -------------------------------------------------------------------------------- /src/Silhouette/Interfaces/ICorProfilerCallback2.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette.Interfaces; 2 | 3 | [NativeObject] 4 | internal unsafe interface ICorProfilerCallback2 : ICorProfilerCallback 5 | { 6 | public new static readonly Guid Guid = Guid.Parse("8A8CC829-CCF2-49fe-BBAE-0F022228071A"); 7 | 8 | /* 9 | * 10 | * THREAD EVENTS 11 | * 12 | */ 13 | 14 | /* 15 | * The CLR calls ThreadNameChanged to notify the code profiler 16 | * that a thread's name has changed. 17 | * 18 | * name is not NULL terminated. 19 | * 20 | */ 21 | HResult ThreadNameChanged( 22 | ThreadId threadId, 23 | uint cchName, 24 | char* name); 25 | 26 | /* 27 | * 28 | * GARBAGE COLLECTION EVENTS 29 | * 30 | */ 31 | 32 | /* 33 | * The CLR calls GarbageCollectionStarted before beginning a 34 | * garbage collection. All GC callbacks pertaining to this 35 | * collection will occur between the GarbageCollectionStarted 36 | * callback and the corresponding GarbageCollectionFinished 37 | * callback. Corresponding GarbageCollectionStarted and 38 | * GarbageCollectionFinished callbacks need not occur on the same thread. 39 | * 40 | * cGenerations indicates the total number of entries in 41 | * the generationCollected array 42 | * generationCollected is an array of booleans, indexed 43 | * by COR_PRF_GC_GENERATIONS, indicating which 44 | * generations are being collected in this collection 45 | * reason indicates whether this GC was induced 46 | * by the application calling GC.Collect(). 47 | * 48 | * NOTE: It is safe to inspect objects in their original locations 49 | * during this callback. The GC will begin moving objects after 50 | * the profiler returns from this callback. Therefore, after 51 | * returning, the profiler should consider all ObjectIDs to be invalid 52 | * until it receives a GarbageCollectionFinished callback. 53 | */ 54 | HResult GarbageCollectionStarted( 55 | int cGenerations, 56 | int* generationCollected, 57 | COR_PRF_GC_REASON reason); 58 | 59 | /* 60 | * The CLR calls SurvivingReferences with information about 61 | * object references that survived a garbage collection. 62 | * 63 | * Generally, the CLR calls SurvivingReferences for non-compacting garbage collections. 64 | * For compacting garbage collections, MovedReferences is called instead. 65 | * 66 | * The exception to this rule is that the CLR always calls SurvivingReferences for objects 67 | * in the large object heap, which is not compacted. 68 | * 69 | * Multiple calls to SurvivingReferences may be received during a particular 70 | * garbage collection, due to limited internal buffering, multiple threads reporting 71 | * in the case of server gc, and other reasons. 72 | * In the case of multiple calls, the information is cumulative - all of the references 73 | * reported in any SurvivingReferences call survive this collection. 74 | * 75 | * cSurvivingObjectIDRanges is a count of the number of ObjectID ranges that 76 | * survived. 77 | * objectIDRangeStart is an array of elements, each of which is the start 78 | * value of a range of ObjectID values that survived the collection. 79 | * cObjectIDRangeLength is an array of elements, each of which states the 80 | * size of the surviving ObjectID value range. 81 | * 82 | * The last two arguments of this function are parallel arrays. 83 | * 84 | * In other words, if an ObjectID value lies within the range 85 | * objectIDRangeStart[i] <= ObjectID < objectIDRangeStart[i] + cObjectIDRangeLength[i] 86 | * for 0 <= i < cMovedObjectIDRanges, then the ObjectID has survived the collection 87 | * 88 | * THIS CALLBACK IS OBSOLETE. It reports ranges for objects >4GB as UINT32_MAX 89 | * on 64-bit platforms. Use ICorProfilerCallback4::SurvivingReferences2 instead. 90 | */ 91 | HResult SurvivingReferences( 92 | uint cSurvivingObjectIDRanges, 93 | ObjectId* objectIDRangeStart, 94 | uint* cObjectIDRangeLength); 95 | /* 96 | * The CLR calls GarbageCollectionFinished after a garbage 97 | * collection has completed and all GC callbacks have been 98 | * issued for it. 99 | * 100 | * NOTE: It is now safe to inspect objects in their 101 | * final locations. 102 | */ 103 | HResult GarbageCollectionFinished(); 104 | 105 | /* 106 | * The CLR calls FinalizeableObjectQueued to notify the code profiler 107 | * that an object with a finalizer (destructor in C# parlance) has 108 | * just been queued to the finalizer thread for execution of its 109 | * Finalize method. 110 | * 111 | * finalizerFlags describes aspects of the finalizer, and takes its 112 | * value from COR_PRF_FINALIZER_FLAGS. 113 | * 114 | */ 115 | 116 | HResult FinalizeableObjectQueued( 117 | COR_PRF_FINALIZER_FLAGS finalizerFlags, 118 | ObjectId objectID); 119 | 120 | /* 121 | * The CLR calls RootReferences2 with information about root 122 | * references after a garbage collection has occurred. 123 | * For each root reference in rootRefIds, there is information in 124 | * rootClassifications to classify it. Depending on the classification, 125 | * rootsIds may contain additional information. The information in 126 | * rootKinds and rootFlags contains information about the location and 127 | * properties of the reference. 128 | * 129 | * If the profiler implements ICorProfilerCallback2, both 130 | * ICorProfilerCallback::RootReferences and ICorProfilerCallback2::RootReferences2 131 | * are called. As the information passed to RootReferences2 is a superset 132 | * of the one passed to RootReferences, profilers will normally implement 133 | * one or the other, but not both. 134 | * 135 | * If the root kind is STACK, the ID is the FunctionID of the 136 | * function containing the variable. If the FunctionID is 0, the function 137 | * is an unnamed function internal to the CLR. 138 | * 139 | * If the root kind is HANDLE, the ID is the GCHandleID. 140 | * 141 | * For the other root kinds, the ID is an opaque value and should 142 | * be ignored. 143 | * 144 | * It's possible for entries in rootRefIds to be 0 - this just 145 | * implies the corresponding root reference was null and thus did not 146 | * refer to an object on the managed heap. 147 | * 148 | * NOTE: None of the objectIDs returned by RootReferences2 are valid during the callback 149 | * itself, as the GC may be in the middle of moving objects from old to new. Thus profilers 150 | * should not attempt to inspect objects during a RootReferences2 call. At 151 | * GarbageCollectionFinished, all objects have been moved to their new locations, and 152 | * inspection may be done. 153 | */ 154 | 155 | HResult RootReferences2( 156 | uint cRootRefs, 157 | ObjectId* rootRefIds, 158 | COR_PRF_GC_ROOT_KIND* rootKinds, 159 | COR_PRF_GC_ROOT_FLAGS* rootFlags, 160 | uint* rootIds); 161 | 162 | /* 163 | * The CLR calls HandleCreated when a gc handle has been created. 164 | * 165 | */ 166 | 167 | HResult HandleCreated( 168 | GCHandleId handleId, 169 | ObjectId initialObjectId); 170 | 171 | /* 172 | * The CLR calls HandleDestroyed when a gc handle has been destroyed. 173 | * 174 | */ 175 | 176 | HResult HandleDestroyed( 177 | GCHandleId handleId); 178 | } -------------------------------------------------------------------------------- /src/Silhouette/ICorProfilerInfo2.cs: -------------------------------------------------------------------------------- 1 | namespace Silhouette; 2 | 3 | public class ICorProfilerInfo2 : ICorProfilerInfo, ICorProfilerInfoFactory 4 | { 5 | private readonly NativeObjects.ICorProfilerInfo2Invoker _impl; 6 | 7 | public ICorProfilerInfo2(nint ptr) : base(ptr) 8 | { 9 | _impl = new(ptr); 10 | } 11 | 12 | static ICorProfilerInfo2 ICorProfilerInfoFactory.Create(nint ptr) => new(ptr); 13 | static Guid ICorProfilerInfoFactory.Guid => Interfaces.ICorProfilerInfo2.Guid; 14 | 15 | public unsafe HResult DoStackSnapshot(ThreadId thread, delegate* unmanaged[Stdcall] callback, COR_PRF_SNAPSHOT_INFO infoFlags, void* clientData, byte* context, uint contextSize) 16 | { 17 | return _impl.DoStackSnapshot(thread, callback, (uint)infoFlags, clientData, context, contextSize); 18 | } 19 | 20 | public HResult SetEnterLeaveFunctionHooks2(IntPtr funcEnter, IntPtr funcLeave, IntPtr funcTailcall) 21 | { 22 | return _impl.SetEnterLeaveFunctionHooks2(funcEnter, funcLeave, funcTailcall); 23 | } 24 | 25 | public unsafe HResult GetFunctionInfo2(FunctionId funcId, COR_PRF_FRAME_INFO frameInfo, Span typeArgs, out uint nbTypeArgs) 26 | { 27 | fixed (ClassId* pTypeArgs = typeArgs) 28 | { 29 | var result = _impl.GetFunctionInfo2(funcId, frameInfo, out var classId, out var moduleId, out var token, (uint)typeArgs.Length, out nbTypeArgs, pTypeArgs); 30 | return new(result, new(classId, moduleId, token)); 31 | } 32 | } 33 | 34 | public unsafe HResult GetFunctionInfo2(FunctionId funcId, COR_PRF_FRAME_INFO frameInfo) 35 | { 36 | var result = _impl.GetFunctionInfo2(funcId, frameInfo, out var classId, out var moduleId, out var token, 0, out var nbTypeArgs, null); 37 | 38 | if (!result) 39 | { 40 | return new(result, new(classId, moduleId, token, [])); 41 | } 42 | 43 | var typeArgs = new ClassId[nbTypeArgs]; 44 | 45 | fixed (ClassId* pTypeArgs = typeArgs) 46 | { 47 | result = _impl.GetFunctionInfo2(funcId, frameInfo, out classId, out moduleId, out token, (uint)typeArgs.Length, out nbTypeArgs, pTypeArgs); 48 | return new(result, new(classId, moduleId, token, typeArgs)); 49 | } 50 | } 51 | 52 | public unsafe HResult GetFunctionInfo2(FunctionId funcId, COR_PRF_FRAME_INFO frameInfo, out ClassId pClassId, out ModuleId pModuleId, out MdToken pToken, uint cTypeArgs, out uint pcTypeArgs, ClassId* typeArgs) 53 | { 54 | return _impl.GetFunctionInfo2(funcId, frameInfo, out pClassId, out pModuleId, out pToken, cTypeArgs, out pcTypeArgs, typeArgs); 55 | } 56 | 57 | public HResult GetStringLayout() 58 | { 59 | var result = _impl.GetStringLayout(out var pBufferLengthOffset, out var pStringLengthOffset, out var pBufferOffset); 60 | return new(result, new(pBufferLengthOffset, pStringLengthOffset, pBufferOffset)); 61 | } 62 | 63 | public unsafe HResult GetClassLayout(ClassId classID, Span fieldOffsets, out uint nbFieldOffsets) 64 | { 65 | fixed (COR_FIELD_OFFSET* pFieldOffsets = fieldOffsets) 66 | { 67 | var result = _impl.GetClassLayout(classID, pFieldOffsets, (uint)fieldOffsets.Length, out nbFieldOffsets, out var classSize); 68 | return new(result, classSize); 69 | } 70 | } 71 | 72 | public unsafe HResult GetClassIDInfo2(ClassId classId, Span typeArgs, out uint numTypeArgs) 73 | { 74 | fixed (ClassId* pTypeArgs = typeArgs) 75 | { 76 | var result = _impl.GetClassIDInfo2(classId, out var moduleId, out var typeDefToken, out var parentClassId, (uint)typeArgs.Length, out numTypeArgs, pTypeArgs); 77 | return new(result, new(moduleId, typeDefToken, parentClassId)); 78 | } 79 | } 80 | 81 | public unsafe HResult GetClassIDInfo2(ClassId classId) 82 | { 83 | var result = _impl.GetClassIDInfo2(classId, out var moduleId, out var typeDefToken, out var parentClassId, 0, out var numTypeArgs, null); 84 | 85 | if (!result) 86 | { 87 | return new(result, new(moduleId, typeDefToken, parentClassId, [])); 88 | } 89 | 90 | var typeArgs = new ClassId[numTypeArgs]; 91 | 92 | fixed (ClassId* pTypeArgs = typeArgs) 93 | { 94 | result = _impl.GetClassIDInfo2(classId, out moduleId, out typeDefToken, out parentClassId, (uint)typeArgs.Length, out numTypeArgs, pTypeArgs); 95 | return new(result, new(moduleId, typeDefToken, parentClassId, typeArgs)); 96 | } 97 | } 98 | 99 | public unsafe HResult GetCodeInfo2(FunctionId functionID, Span codeInfos, out uint nbCodeInfos) 100 | { 101 | fixed (COR_PRF_CODE_INFO* pCodeInfos = codeInfos) 102 | { 103 | return _impl.GetCodeInfo2(functionID, (uint)codeInfos.Length, out nbCodeInfos, pCodeInfos); 104 | } 105 | } 106 | 107 | public unsafe HResult GetClassFromTokenAndTypeArgs(ModuleId moduleID, MdTypeDef typeDef, ReadOnlySpan typeArgs) 108 | { 109 | fixed (ClassId* pTypeArgs = typeArgs) 110 | { 111 | var result = _impl.GetClassFromTokenAndTypeArgs(moduleID, typeDef, (uint)typeArgs.Length, pTypeArgs, out var classId); 112 | return new(result, classId); 113 | } 114 | } 115 | 116 | public unsafe HResult GetFunctionFromTokenAndTypeArgs(ModuleId moduleID, MdMethodDef funcDef, ClassId classId, ReadOnlySpan typeArgs) 117 | { 118 | fixed (ClassId* pTypeArgs = typeArgs) 119 | { 120 | var result = _impl.GetFunctionFromTokenAndTypeArgs(moduleID, funcDef, classId, (uint)typeArgs.Length, pTypeArgs, out var functionId); 121 | return new(result, functionId); 122 | } 123 | } 124 | 125 | public HResult> EnumModuleFrozenObjects(ModuleId moduleID) 126 | { 127 | var result = _impl.EnumModuleFrozenObjects(moduleID, out var pEnum); 128 | return new(result, new(pEnum)); 129 | } 130 | 131 | public unsafe HResult GetArrayObjectInfo(ObjectId objectId, Span dimensionSizes, Span dimensionLowerBounds) 132 | { 133 | if (dimensionSizes.Length != dimensionLowerBounds.Length) 134 | { 135 | throw new ArgumentException("The length of the dimension sizes and dimension lower bounds must be equal."); 136 | } 137 | 138 | fixed (uint* pDimensionSizes = dimensionSizes) 139 | fixed (int* pDimensionLowerBounds = dimensionLowerBounds) 140 | { 141 | var result = _impl.GetArrayObjectInfo(objectId, (uint)dimensionSizes.Length, pDimensionSizes, pDimensionLowerBounds, out var ppData); 142 | return new(result, (nint)ppData); 143 | } 144 | } 145 | 146 | public HResult GetBoxClassLayout(ClassId classId) 147 | { 148 | var result = _impl.GetBoxClassLayout(classId, out var bufferOffset); 149 | return new(result, bufferOffset); 150 | } 151 | 152 | public HResult GetThreadAppDomain(ThreadId threadId) 153 | { 154 | var result = _impl.GetThreadAppDomain(threadId, out var appDomainId); 155 | return new(result, appDomainId); 156 | } 157 | 158 | public unsafe HResult GetRVAStaticAddress(ClassId classId, MdFieldDef fieldToken) 159 | { 160 | var result = _impl.GetRVAStaticAddress(classId, fieldToken, out var address); 161 | return new(result, (nint)address); 162 | } 163 | 164 | public unsafe HResult GetAppDomainStaticAddress(ClassId classId, MdFieldDef fieldToken, AppDomainId appDomainId) 165 | { 166 | var result = _impl.GetAppDomainStaticAddress(classId, fieldToken, appDomainId, out var address); 167 | return new(result, (nint)address); 168 | } 169 | 170 | public unsafe HResult GetThreadStaticAddress(ClassId classId, MdFieldDef fieldToken, ThreadId threadId) 171 | { 172 | var result = _impl.GetThreadStaticAddress(classId, fieldToken, threadId, out var address); 173 | return new(result, (nint)address); 174 | } 175 | 176 | public unsafe HResult GetContextStaticAddress(ClassId classId, MdFieldDef fieldToken, ContextId contextId) 177 | { 178 | var result = _impl.GetContextStaticAddress(classId, fieldToken, contextId, out var address); 179 | return new(result, (nint)address); 180 | } 181 | 182 | public HResult GetStaticFieldInfo(ClassId classId, MdFieldDef fieldToken) 183 | { 184 | var result = _impl.GetStaticFieldInfo(classId, fieldToken, out var fieldInfo); 185 | return new(result, fieldInfo); 186 | } 187 | 188 | public unsafe HResult GetGenerationBounds(Span ranges, out uint nbObjectRanges) 189 | { 190 | fixed (COR_PRF_GC_GENERATION_RANGE* pObjectRanges = ranges) 191 | { 192 | return _impl.GetGenerationBounds((uint)ranges.Length, out nbObjectRanges, pObjectRanges); 193 | } 194 | } 195 | 196 | public HResult GetObjectGeneration(ObjectId objectId) 197 | { 198 | var result = _impl.GetObjectGeneration(objectId, out var range); 199 | return new(result, range); 200 | } 201 | 202 | public HResult GetNotifiedExceptionClauseInfo() 203 | { 204 | var result = _impl.GetNotifiedExceptionClauseInfo(out var info); 205 | return new(result, info); 206 | } 207 | } --------------------------------------------------------------------------------