使用C#编写.NET分析器(三)

译者注

这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Reshaper等等。之前只能使用C++编写,自从.NET NativeAOT发布以后,使用C#编写变为可能。

笔者最近也在尝试开发一个运行时方法注入的工具,欢迎熟悉MSIL 、PE Metadata 布局、CLR 源码、CLR Profiler API的大佬,或者对这个感兴趣的朋友留联系方式或者在公众号留言,一起交流学习。.

原作者:Kevin Gosse

原文链接:https://minidump.net/writing-a-net-profiler-in-c-part-3-7d2c59fc017f

项目链接:https://github.com/kevingosse/ManagedDotnetProfiler

使用C#编写.NET分析器-第一部分:https://mp.weixin.qq.com/s/faa9CFD2sEyGdiLMFJnyxw

使用C#编写.NET分析器-第二部分:

https://mp.weixin.qq.com/s/uZDtrc1py0wvCcUERZnKIw

正文

在第一部分中,我们了解了如何使用 NativeAOT让我们用C#编写一个分析器,以及如何暴露一个伪造的 COM对象来使用分析API。在第二部分中,我们改进了解决方案,使用实例方法替代静态方法。现在我们知道了如何与分析API进行交互,我们将编写一个源代码生成器,自动生成实现 ICorProfilerCallback接口中声明的70多个方法所需的样板代码。

首先,我们需要手动将 ICorProfilerCallback接口转换为C#。从技术上讲,本可以从C++头文件中自动生成这些代码,但是相同的C++代码在C#中可以用不同的方式翻译,因此了解函数的目的以正确语义进行转换十分重要。

JITInlining函数为实际例子。在C++中的原型是:


 
  1. HRESULT JITInlining(FunctionID callerId, FunctionID calleeId, BOOL *pfShouldInline);

一个简单的C#版本转换可能是:


 
  1. HResult JITInlining(FunctionId callerId, FunctionId calleeId, in bool pfShouldInline);

但是,如果我们查看函数的文档,我们可以了解到pfShouldInline是一个应由函数自身设置的值。所以我们应该使用out关键字:


 
  1. Result JITInlining(FunctionId callerId, FunctionId calleeId, out bool pfShouldInline);

在其他情况下,我们会根据意图使用in或ref关键字。这就是为什么我们无法完全自动化这个过程。

在将接口转换为C#之后,我们可以继续创建源代码生成器。请注意,我并不打算编写一个最先进的源代码生成器,主要原因是API非常复杂(是的,这话来自于一个教你如何用C#编写分析器的人),你可以查看Andrew Lock的精彩文章来了解如何编写高级源代码生成器。

编写源代码生成器

要创建源代码生成器,我们在解决方案中添加一个针对 netstandard2.0的类库项目,并添加对 Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.Analyzers的引用:


 
  1. <Project Sdk="Microsoft.NET.Sdk">

  2. <PropertyGroup>

  3. <TargetFramework>netstandard2.0</TargetFramework>

  4. <ImplicitUsings>enable</ImplicitUsings>

  5. <LangVersion>latest</LangVersion>

  6. <IsRoslynComponent>true</IsRoslynComponent>

  7. </PropertyGroup>

  8. <ItemGroup>

  9. <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />

  10. <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">

  11. <PrivateAssets>all</PrivateAssets>

  12. <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

  13. </PackageReference>

  14. </ItemGroup>

  15. </Project>

接下来,我们添加一个实现 ISourceGenerator接口的类,并用 [Generator]属性进行修饰:


 
  1. [Generator]

  2. public class NativeObjectGenerator : ISourceGenerator

  3. {

  4. public void Initialize(GeneratorInitializationContext context)

  5. {

  6. }

  7. public void Execute(GeneratorExecutionContext context)

  8. {

  9. }

  10. }

我们要做的第一件事是生成一个 [NativeObject]属性。我们将用它来修饰我们想要在源代码生成器上运行的接口。我们使用 RegisterForPostInitialization在管道早期运行这段代码:


 
  1. [Generator]

  2. public class NativeObjectGenerator : ISourceGenerator

  3. {

  4. public void Initialize(GeneratorInitializationContext context)

  5. {

  6. context.RegisterForPostInitialization(EmitAttribute);

  7. }

  8. public void Execute(GeneratorExecutionContext context)

  9. {

  10. }

  11. private void EmitAttribute(GeneratorPostInitializationContext context)

  12. {

  13. context.AddSource("NativeObjectAttribute.g.cs", """

  14. using System;

  15. [AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]

  16. internal class NativeObjectAttribute : Attribute { }

  17. """);

  18. }

  19. }

现在我们需要注册一个 ISyntaxContextReceiver来检查类型并检测哪些类型被我们的 [NativeObject] 属性修饰。


 
  1. public class SyntaxReceiver : ISyntaxContextReceiver

  2. {

  3. public List<INamedTypeSymbol> Interfaces { get; } = new();

  4. public void OnVisitSyntaxNode(GeneratorSyntaxContext context)

  5. {

  6. if (context.Node is InterfaceDeclarationSyntax classDeclarationSyntax

  7. && classDeclarationSyntax.AttributeLists.Count > 0)

  8. {

  9. var symbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);

  10. if (symbol.GetAttributes().Any(a => a.AttributeClass.ToDisplayString() == "NativeObjectAttribute"))

  11. {

  12. Interfaces.Add(symbol);

  13. }

  14. }

  15. }

  16. }

基本上,语法接收器将被用于访问语法树中的每个节点。我们检查该节点是否是一个接口声明,如果是,我们检查属性以查找 NativeObjectAttribute。可能有很多事情都可以改进,特别是确认它是否是我们的 NativeObjectAttribute,但我们认为对于我们的目的来说这已经足够好了。

在源代码生成器初始化期间,需要注册语法接收器:


 
  1. public void Initialize(GeneratorInitializationContext context)

  2. {

  3. context.RegisterForPostInitialization(EmitAttribute);

  4. context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());

  5. }

最后,在 Execute方法中,我们获取存储在语法接收器中的接口列表,并为其生成代码:


 
  1. public void Execute(GeneratorExecutionContext context)

  2. {

  3. if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver))

  4. {

  5. return;

  6. }

  7. foreach (var symbol in receiver.Interfaces)

  8. {

  9. EmitStubForInterface(context, symbol);

  10. }

  11. }

使用C#编写.NET分析器(三)

生成Native包装器

对于EmitStubForInterface方法,我们可以使用模板引擎,但是我们将依赖于一个经典的StringBuilder和Replace调用。

首先,我们创建我们的模板:


 
  1. var sourceBuilder = new StringBuilder("""

  2. using System;

  3. using System.Runtime.InteropServices;

  4. namespace NativeObjects

  5. {

  6. {visibility} unsafe class {typeName} : IDisposable

  7. {

  8. private {typeName}({interfaceName} implementation)

  9. {

  10. const int delegateCount = {delegateCount};

  11. var obj = (IntPtr*)NativeMemory.Alloc((nuint)2 + delegateCount, (nuint)IntPtr.Size);

  12. var vtable = obj + 2;

  13. *obj = (IntPtr)vtable;

  14. var handle = GCHandle.Alloc(implementation);

  15. *(obj + 1) = GCHandle.ToIntPtr(handle);

  16. {functionPointers}

  17. Object = (IntPtr)obj;

  18. }

  19. public IntPtr Object { get; private set; }

  20. public static {typeName} Wrap({interfaceName} implementation) => new(implementation);

  21. public static implicit operator IntPtr({typeName} stub) => stub.Object;

  22. ~{typeName}()

  23. {

  24. Dispose();

  25. }

  26. public void Dispose()

  27. {

  28. if (Object != IntPtr.Zero)

  29. {

  30. NativeMemory.Free((void*)Object);

  31. Object = IntPtr.Zero;

  32. }

  33. GC.SuppressFinalize(this);

  34. }

  35. private static class Exports

  36. {

  37. {exports}

  38. }

  39. }

  40. }

  41. """);

如果你对某些部分不理解,请记得查看前一篇文章。这里唯一的新内容是析构函数和 Dispose方法,我们在其中调用 NativeMemory.Free来释放为该对象分配的内存。接下来,我们需要填充所有的模板部分:{visibility}{typeName}{interfaceName}{delegateCount}{functionPointers}{exports}

首先是简单的部分:


 
  1. var interfaceName = symbol.ToString();

  2. var typeName = $"{symbol.Name}";

  3. var visibility = symbol.DeclaredAccessibility.ToString().ToLower();

  4. // To be filled later

  5. int delegateCount = 0;

  6. var exports = new StringBuilder();

  7. var functionPointers = new StringBuilder();

对于一个接口 MyProfiler.ICorProfilerCallback,我们将生成一个类型为 NativeObjects.ICorProfilerCallback的包装器。这就是为什么我们将完全限定名存储在 interfaceName(= MyProfiler.ICorProfilerCallback)中,而仅将类型名存储在 typeName(= ICorProfilerCallback)中。

接下来我们想要生成导出列表及其函数指针。我希望源代码生成器支持继承,以避免代码重复,因为 ICorProfilerCallback13实现了 ICorProfilerCallback12,而 ICorProfilerCallback12本身又实现了 ICorProfilerCallback11,依此类推。因此我们提取目标接口继承自的接口列表,并为它们中的每一个提取方法:


 
  1. var interfaceList = symbol.AllInterfaces.ToList();

  2. interfaceList.Reverse();

  3. interfaceList.Add(symbol);

  4. foreach (var @interface in interfaceList)

  5. {

  6. foreach (var member in @interface.GetMembers())

  7. {

  8. if (member is not IMethodSymbol method)

  9. {

  10. continue;

  11. }

  12. // TODO: Inspect the method

  13. }

  14. }

对于一个 QueryInterface(inGuidguid,outIntPtrptr)方法,我们将生成的导出看起来像这样:


 
  1. [UnmanagedCallersOnly]

  2. public static int QueryInterface(IntPtr* self, Guid* __arg1, IntPtr* __arg2)

  3. {

  4. var handleAddress = *(self + 1);

  5. var handle = GCHandle.FromIntPtr(handleAddress);

  6. var obj = (IUnknown)handle.Target;

  7. var result = obj.QueryInterface(*__arg1, out var __local2);

  8. *__arg2 = __local2;

  9. return result;

  10. }

由于这些方法是实例方法,我们添加了 IntPtr*self参数。另外,如果托管接口中的函数带有 in/out/ref关键字修饰,我们将参数声明为指针类型,因为 UnmanagedCallersOnly方法不支持 in/out/ref

生成导出所需的代码为:


 
  1. var parameterList = new StringBuilder();

  2. parameterList.Append("IntPtr* self");

  3. foreach (var parameter in method.Parameters)

  4. {

  5. var isPointer = parameter.RefKind == RefKind.None ? "" : "*";

  6. parameterList.Append($", {parameter.Type}{isPointer} __arg{parameter.Ordinal}");

  7. }

  8. exports.AppendLine($" [UnmanagedCallersOnly]");

  9. exports.AppendLine($" public static {method.ReturnType} {method.Name}({parameterList})");

  10. exports.AppendLine($" {{");

  11. exports.AppendLine($" var handle = GCHandle.FromIntPtr(*(self + 1));");

  12. exports.AppendLine($" var obj = ({interfaceName})handle.Target;");

  13. exports.Append($" ");

  14. if (!method.ReturnsVoid)

  15. {

  16. exports.Append("var result = ");

  17. }

  18. exports.Append($"obj.{method.Name}(");

  19. for (int i = 0; i < method.Parameters.Length; i++)

  20. {

  21. if (i > 0)

  22. {

  23. exports.Append(", ");

  24. }

  25. if (method.Parameters[i].RefKind == RefKind.In)

  26. {

  27. exports.Append($"*__arg{i}");

  28. }

  29. else if (method.Parameters[i].RefKind is RefKind.Out)

  30. {

  31. exports.Append($"out var __local{i}");

  32. }

  33. else

  34. {

  35. exports.Append($"__arg{i}");

  36. }

  37. }

  38. exports.AppendLine(");");

  39. for (int i = 0; i < method.Parameters.Length; i++)

  40. {

  41. if (method.Parameters[i].RefKind is RefKind.Out)

  42. {

  43. exports.AppendLine($" *__arg{i} = __local{i};");

  44. }

  45. }

  46. if (!method.ReturnsVoid)

  47. {

  48. exports.AppendLine($" return result;");

  49. }

  50. exports.AppendLine($" }}");

  51. exports.AppendLine();

  52. exports.AppendLine();

对于函数指针,给定与前面相同的方法,我们希望建立:


 
  1. *(vtable + 1) = (IntPtr)(delegate* unmanaged<IntPtr*, Guid*, IntPtr*>)&Exports.QueryInterface;

生成代码如下:


 
  1. var sourceArgsList = new StringBuilder();

  2. sourceArgsList.Append("IntPtr _");

  3. for (int i = 0; i < method.Parameters.Length; i++)

  4. {

  5. sourceArgsList.Append($", {method.Parameters[i].OriginalDefinition} a{i}");

  6. }

  7. functionPointers.Append($" *(vtable + {delegateCount}) = (IntPtr)(delegate* unmanaged<IntPtr*");

  8. for (int i = 0; i < method.Parameters.Length; i++)

  9. {

  10. functionPointers.Append($", {method.Parameters[i].Type}");

  11. if (method.Parameters[i].RefKind != RefKind.None)

  12. {

  13. functionPointers.Append("*");

  14. }

  15. }

  16. if (method.ReturnsVoid)

  17. {

  18. functionPointers.Append(", void");

  19. }

  20. else

  21. {

  22. functionPointers.Append($", {method.ReturnType}");

  23. }

  24. functionPointers.AppendLine($">)&Exports.{method.Name};");

  25. delegateCount++;

我们在接口的每个方法都完成了这个操作后,我们只需替换模板中的值并添加生成的源文件:


 
  1. sourceBuilder.Replace("{typeName}", typeName);

  2. sourceBuilder.Replace("{visibility}", visibility);

  3. sourceBuilder.Replace("{exports}", exports.ToString());

  4. sourceBuilder.Replace("{interfaceName}", interfaceName);

  5. sourceBuilder.Replace("{delegateCount}", delegateCount.ToString());

  6. sourceBuilder.Replace("{functionPointers}", functionPointers.ToString());

  7. context.AddSource($"{symbol.ContainingNamespace?.Name ?? "_"}.{symbol.Name}.g.cs", sourceBuilder.ToString());

就这样,我们的源代码生成器现在准备好了。

使用生成的代码

要使用我们的源代码生成器,我们可以声明 IUnknownIClassFactoryICorProfilerCallback接口,并用 [NativeObject]属性修饰它们:


 
  1. [NativeObject]

  2. public interface IUnknown

  3. {

  4. HResult QueryInterface(in Guid guid, out IntPtr ptr);

  5. int AddRef();

  6. int Release();

  7. }


 
  1. [NativeObject]

  2. internal interface IClassFactory : IUnknown

  3. {

  4. HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance);

  5. HResult LockServer(bool @lock);

  6. }


 
  1. [NativeObject]

  2. public unsafe interface ICorProfilerCallback : IUnknown

  3. {

  4. HResult Initialize(IntPtr pICorProfilerInfoUnk);

  5. // 70+ 多个方法,在这里省略

  6. }

然后我们实现 IClassFactory并调用 NativeObjects.IClassFactory.Wrap来创建本机包装器并暴露我们的 ICorProfilerCallback实例:


 
  1. public unsafe class ClassFactory : IClassFactory

  2. {

  3. private NativeObjects.IClassFactory _classFactory;

  4. private CorProfilerCallback2 _corProfilerCallback;

  5. public ClassFactory()

  6. {

  7. _classFactory = NativeObjects.IClassFactory.Wrap(this);

  8. }

  9. // The native wrapper has an implicit cast operator to IntPtr

  10. public IntPtr Object => _classFactory;

  11. public HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance)

  12. {

  13. Console.WriteLine("[Profiler] ClassFactory - CreateInstance");

  14. _corProfilerCallback = new();

  15. instance = _corProfilerCallback.Object;

  16. return HResult.S_OK;

  17. }

  18. public HResult LockServer(bool @lock)

  19. {

  20. return default;

  21. }

  22. public HResult QueryInterface(in Guid guid, out IntPtr ptr)

  23. {

  24. Console.WriteLine("[Profiler] ClassFactory - QueryInterface - " + guid);

  25. if (guid == KnownGuids.ClassFactoryGuid)

  26. {

  27. ptr = Object;

  28. return HResult.S_OK;

  29. }

  30. ptr = IntPtr.Zero;

  31. return HResult.E_NOTIMPL;

  32. }

  33. public int AddRef()

  34. {

  35. return 1; // TODO: 做实际的引用计数

  36. }

  37. public int Release()

  38. {

  39. return 0; // TODO: 做实际的引用计数

  40. }

  41. }

并在 DllGetClassObject中暴露它:


 
  1. public class DllMain

  2. {

  3. private static ClassFactory Instance;

  4. [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]

  5. public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv)

  6. {

  7. Console.WriteLine("[Profiler] DllGetClassObject");

  8. Instance = new ClassFactory();

  9. *ppv = Instance.Object;

  10. return 0;

  11. }

  12. }

最后,我们可以实现 ICorProfilerCallback的实例:


 
  1. public unsafe class CorProfilerCallback2 : ICorProfilerCallback2

  2. {

  3. private static readonly Guid ICorProfilerCallback2Guid = Guid.Parse("8a8cc829-ccf2-49fe-bbae-0f022228071a");

  4. private readonly NativeObjects.ICorProfilerCallback2 _corProfilerCallback2;

  5. public CorProfilerCallback2()

  6. {

  7. _corProfilerCallback2 = NativeObjects.ICorProfilerCallback2.Wrap(this);

  8. }

  9. public IntPtr Object => _corProfilerCallback2;

  10. public HResult Initialize(IntPtr pICorProfilerInfoUnk)

  11. {

  12. Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize");

  13. // TODO: To be implemented in next article

  14. return HResult.S_OK;

  15. }

  16. public HResult QueryInterface(in Guid guid, out IntPtr ptr)

  17. {

  18. if (guid == ICorProfilerCallback2Guid)

  19. {

  20. Console.WriteLine("[Profiler] ICorProfilerCallback2 - QueryInterface");

  21. ptr = Object;

  22. return HResult.S_OK;

  23. }

  24. ptr = IntPtr.Zero;

  25. return HResult.E_NOTIMPL;

  26. }

  27. // Stripped for brevity: the default implementation of all 70+ methods of the interface

  28. // Automatically generated by the IDE

  29. }

如果我们使用一个测试应用程序运行它,我们会发现这些功能能按预期工作:


 
  1. [Profiler] DllGetClassObject

  2. [Profiler] ClassFactory - CreateInstance

  3. [Profiler] ICorProfilerCallback2 - QueryInterface

  4. [Profiler] ICorProfilerCallback2 - Initialize

  5. Hello, World!

在下一步中,我们将处理拼图的最后一个缺失部分:实现ICorProfilerCallback.Initialize方法并获取ICorProfilerInfo的实例。这样我们就拥有了与性能分析器API实际交互所需的一切。