前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >轻松掌握C++ AST的处理方法 - CppAst.Net使用介绍

轻松掌握C++ AST的处理方法 - CppAst.Net使用介绍

作者头像
fangfang
发布2023-10-16 15:28:19
2930
发布2023-10-16 15:28:19
举报
文章被收录于专栏:方方的杂货铺方方的杂货铺

导语

现代的游戏引擎一般都会较重度的依赖代码生成技术, 而代码生成技术一般都是以原始代码为处理信息源, 再结合专用的配置来做进一步的处理. 发展到后来, 就渐渐变成原始代码和配置一体化的形式了. 比如大家熟知的UE使用的是在原始代码上利用宏来注入额外信息的方式, 然后再用自己专门实现的 UHT - Unreal Header Tool 来完成代码生成的目的. 早期的 UHT 使用 C++ 编写, 它采用的一个 2 Pass 解析相关头文件源码并提取相关信息进行生成的方式, 新版的 UE5 使用处理字符串更友好的 C# 重写了整个 UHT, 整体的实现对比之前的版本也更完整, 对对各类 C++ Token 的处理也更完备了。 笔者所参与的腾讯IEG自研的 3D 引擎同样也大量使用了代码生成技术,与UE相比, 我们并没有选择自己从头开始开发的代码生成工具, 而是综合历史经验和重新选型后,选择了直接在 C++ 抽象语法树(AST)层级来完成原始代码信息的提取, 以此为基础进行代码生成。早期我们直接使用了 libclang 的 Python Wrapper , 来完成相关的工作. 相关的维护成本和执行效率都不尽如人意, 重新调研之后我们选择了底层同样使用 libclang, 但整体设计和实现更合理, 使用更友好的 http://CppAst.Net 来完成这部分工作. 当然, 整个过程也不是一帆风顺的, 在对 http://CppAst.Net 做了几个关键功能的 PR 之后, 我们已经可以基于 http://CppAst.Net 很好的完成我们需要的代码解析和额外信息注入的功能了, 本文将重点介绍 C# 库 - http://CppAst.Net 的方方面面, 希望帮助大家更好的完成 C++ 代码分析或者代码生成相关的工具.


1. 代码生成简介

导语部分也有提到了, 现代的游戏引擎一般都会较重度的依赖代码生成技术, 我们先来看一个IEG自研引擎 CE中的一个实际的例子 - Vector3 类的反射注册代码:

C++ 中的 Vector3 类:

代码语言:javascript
复制
//-------------------------------------
//declaration
//-------------------------------------
class Vector3 {
 public:
  double x;
  double y;
  double z;
 public:
  Vector3() : x(0.0), y(0.0), z(0.0) {}
  Vector3(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {}
  double DotProduct(const Vector3& vec) const;
};

Vector3类的反射注册代码:

代码语言:javascript
复制
//-------------------------------------
//register code
//-------------------------------------
__register_type<Vector3>("Vector3")
        .constructor()
        .constructor<double, double, double>()
        .property("x", &Vector3::x)
        .property("y", &Vector3::y)
        .property("z", &Vector3::z)
        .function("DotProduct", &Vector3::DotProduct);
        );

即可以完成对它的构造函数以及几个属性的反射注册, 然后我们就可以通过反射库来使用它了. 实际的工程应用中, 虽然选择手动为每个类去实现注册代码也是一种方式. 但这种方式明显工作量巨大, 而且容易出现修改原始实现的时候, 可能就漏改注册部分代码的情况, 肯定不是一种特别可取的方式. 这种情况下, 我们就会考虑使用更 "自动化" 的机制来解决注册代码的生成问题, 这也是目前CE所选择的方式, CE中整体的反射代码自动生成流程大致如下图所示:

对比常规的C++编译处理过程[上图中的Normal Mode], 我们合理安排编译管线, 利用 libclang, 形成如右侧所示的二次 Compile 过程: - 第一次编译发生在工具内, 仅处理头文件, 用于提取必须的信息, 如类的定义等. - 第二次是真实的正常编译过程, 将工具额外产生的文件一起加入整个编译生成. 这样, 利用工具自动生成的一部分注册代码, 与原来的代码一起进行编译, 我们就能得到一个运行时信息完备的反射系统了. 其他诸如脚本语言的中间层自动生成的原理与这里的情况也差不多, 利用这种 header only libclang compile 的模式, 我们都能够比较好的去组织离线工具的实现逻辑, 来完成需要代码的生成. 看起来是不是很美好的一个现状?


2. libclang - 带来希望, 也带来迷茫

早期没有llvm库的时候, 我们只能通过正则匹配等字符串模式匹配的方式来完成相关工作, 这种方式比较大的弊端一方面是效率, 另外一方面是业务程序对代码的组织方式可能破坏自动工具的工作, 排查和定位相关问题又不是那么直接. 在llvm库流程后, 越来越多的人开始尝试在AST这一层对源代码信息进行提取, 这样相关的问题就回归到了c++本身来解决了, 这肯定比前面说的基于字符串的机制要稳定可控非常多, 相关的问题也更容易定位排查. 要使用这种方式, 我们先来简单的了解一下libclang.

2.1 libclang 和它带来的改变

libclang是llvm工具链中的一部分, 整个llvm的工作过程简单来说可以看成下图所示:

而libclang主要用于处理c++源码 -> AST 这部分的工作. 我们的代码生成工具主要就是利用这部分的能力, 在获取到AST后, 基于一些配置信息进行代码生成相关的工作.

像上面介绍的 CE 的方案一样, 基于 libclang 和二次编译, 我们可以在设计层面整理出一个很简洁的离线代码生成工具的流程, 但基于libclang离线工具的实际实现过程中依然还是会碰到种种问题: 1. libclang的编译问题 - 跨平台使用的情况下复杂度更高. 可参考文章此处文章了解一些详情 2. 如何选择 libclang 的使用语言, 是C++, Python, C#, 还是其他? 3. 如何支持好C++中大量使用的各种模板类型? 4. 生成信息的标记和额外信息的注入如何解决, 如UE里大量使用的 Property 在Editor中使用到的各种信息的注入? 5. 如何更好的组织生成代码, 避免工具中大量的字符串拼接代码的存在?

除了上面列的这些问题, 还有以下这些 libclang 和 C++ 复杂度本身带来的问题:

2.2 libclang 本身的 Cursor 机制带来的限制

libclang 本身是以 Cursor 的方式来对 AST 进行表达的, 比如对于下面的类:

代码语言:javascript
复制
namespace math {

class Ray {
 protected:
  Vector3 mOrigin;
  Vector3 mDirection;
 public:
  Ray();
  Ray(const Vector3& origin, const Vector3& direction);

  /** Sets the origin of the ray. */
  void setOrigin(const Vector3& origin);
  //... something ignore here
};

} //namespace math

对应的AST如下:

与源码一一对应的看, 还是比较好了解 AST 中对应 Cursor 的作用的. 我们可以看到 libclang 以各种类型的 CXCursor的方式来构建整个AST, 复杂度较高的Cursor主要还是集中在Stmt和Exprs部分. 但因为跟源代码语法基本是一一对应的关系, 熟悉起来其实也比较快. 以下是 CXCursor的一个分类情况:

本身CXCursor的分类和使用并没有什么障碍, 但 libclang 主要访问某个节点下的子节点的方式, 更多是按回调的方式来提供的, 如我们用来Dump一个CXCursor信息的局部代码所示:

代码语言:javascript
复制
private static void PrintASTByCursor(CXCursor cursor, int level, List<string> saveList)
{
    bool needPrintChild = true;
    saveList.Add(GetOneCursorDetails(cursor, level, out needPrintChild));

    unsafe
    {
        PrintCursorInfo cursorInfo = new PrintCursorInfo();
        cursorInfo.Level = level + 1;
        cursorInfo.SaveList = saveList;
        GCHandle cursorInfoHandle = GCHandle.Alloc(cursorInfo);

        cursor.VisitChildren(VisitorForPrint,
            new CXClientData((IntPtr)cursorInfoHandle));
    }
}

这给我们实现代码分析和生成工具造成了极大的不便, 因为很多时候我们的处理工具没有办法对 AST 做单次处理就完成所有的事情, 很多事项是需要依赖重复访问某个 CXCursor以及其子级来完成的. 所以曾经在 G6 的时候, 我们是通过 C# 来访问 libclang 的(使用 ClangSharp ), 当时我们就尝试自己在 C# 层中完整的保存了一份来自 libclang 的数据层, 当然, 这个数据层肯定也是通过 libclang 原生的回调方式一次性获取的, 这样离线工具与 libclang 的原生 AST就解耦了, 也不会有 libclang 回调和多次获取数据不便的问题了. 当时加入 C# 数据层后的的工作流程大致如下: 加入自定义的结构化ClangAST层, 整个处理流程如下所示:

但当时的实现其实也存在一些问题, 同时使用了 ClangSharp 的 LowLevel 接口, 以及 HighLevel 接口, 而ClangSharp的 HighLevel 接口实现质量其实并不高. 另外因为中间数据层的存在, 整体实现代码量并不少, 上手相关工具的复杂度其实并没有降低. 这其实也是后面会考虑转向 http://CppAst.Net 实现的一大原因之一.

2.3 C++ 类型系统的复杂度

除了上面提到的 libclang 本身带来的问题, C++复杂的类型系统也是离线生成工具处理的一大难点, 如下图所示, 我们给出了 C++ 中大概的类型分类:

类型系统的复杂度主要体现在: - C++中众多的 builtin 类型 - 用户可以通过自定义的方法扩展大量的 UDT (如class和enum等) - c++支持如Pointer和Reference, Array这些进阶类型, 这些类型还能相互嵌套作用 - 类型可以加const, volatile等修饰, 形成新的类型 - 我们还能通过using, typedef为类型指定别名 - 再加上c++11开始扩展的关键字, 我们可能还会使用auto, decltype, typeof进行类型表达 - 模板的支持带来了更复杂的类型系统表达(复杂度比较高, 本篇直接先略过了).

所以整个类型系统的复杂度是步步攀升, 基本上离线工具处理的难点就集中在这一部分了. 当从某个Cursor中解析到一个Type, 很多时候我们需要层层递进的分析, 才能最终解析出它实际的类型. 这其实也是我们后面会具体说到的 http://CppAst.Net的一个优势, 它基本在 C# 层相对完整的实现了 C++的这个类型系统, 这样虽然类型系统本身的复杂度还是存在的, 但我们在 C# 层可以以比较接近原生 C++ 的方式对各个类型进行很好的处理, 再加上 C# 本身运行时完备的类型信息, 很多问题都能够得到有效的简化处理了.


3. http://CppAst.Net - 新的天花板

前面的铺垫比较长, 终于迎来我们的正主 CppAst.Net了, 还是按照老规矩, 我们先来看一段 http://CppAst.Net 官网上的示例代码:

代码语言:javascript
复制
// Parse a C++ files
var compilation = CppParser.Parse(@"
enum MyEnum { MyEnum_0, MyEnum_1 };
void function0(int a, int b);
struct MyStruct { int field0; int field1;};
typedef MyStruct* MyStructPtr;
"
);
// Print diagnostic messages
foreach (var message in compilation.Diagnostics.Messages)
    Console.WriteLine(message);

// Print All enums
foreach (var cppEnum in compilation.Enums)
    Console.WriteLine(cppEnum);

// Print All functions
foreach (var cppFunction in compilation.Functions)
    Console.WriteLine(cppFunction);

// Print All classes, structs
foreach (var cppClass in compilation.Classes)
    Console.WriteLine(cppClass);

// Print All typedefs
foreach (var cppTypedef in compilation.Typedefs)
    Console.WriteLine(cppTypedef);

相应的输出:

代码语言:javascript
复制
enum MyEnum {...}
void function0(int a, int b)
struct MyStruct { ... }
typedef MyStruct* MyStructPtr

从上面的示例中我们已经可以看到 http://CppAst.Net 的一些优势: 1. C# 侧提供了各种高级的类型, 如 CppFunction, CppClass, CppEnum 等, 整个 C# 侧重新组织的 AST 也是不依赖回调就能直接按 foreach 的方式进行访问的. 2. 能够支持直接从字符串构建 Compilation, 这样也方便实现单元测试.

3.1 简单配置即可上手使用

http://CppAst.Net底层是依赖ClangSharp的, 有过 ClangSharp 使用经念的同学可能都知道, 整个编译运行的体验可能并不太好, 从 NuGet 添加了 ClangSharp 包之后, 可能我们直接运行相关的示例和测试代码, 还是会提示 libclang.dll/ligclang.so 找不到之类的问题, 体验不会特别好, 这个其实也不能全怪 ClangSharp, 主要还是 NuGet 对包本身依赖的原生二进制的大小做了一些限制, 因为这个问题可能比较多人遇到, 我们先贴出一下相关的 Workaround, 方便大家更好的运行自己的测试代码:

代码语言:javascript
复制
<PropertyGroup>
    <!-- Workaround for issue https://github.com/microsoft/ClangSharp/issues/129 -->
    <RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == '' AND '$(PackAsTool)' != 'true'">$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
</PropertyGroup>

对于前面说到的官方示例代码, 我们可以尝试从零开始建立一个C# .netcore 3.1 的Console App, 一步一步将其运行起来:

3.1.1 新建工程

打开 Visual Studio 建立一个C# Console App (笔者当前使用的环境是 VS 2022):

image.png

配置项目名称:

image.png

选定.net 版本(这里我们直接用.net core3.1):

image.png

3.1.2 通过 NuGet 添加 CppAst.Net

打开NuGet包管理器:

image.png

image.png

添加http://CppAst.Net包:

image.png

操作完成后, 我们在项目的依赖里就可以看到 http://CppAst.Net 包了:

image.png

3.1.3 针对 csproj 的 Workaround

我们还需要一步针对前面提到的找不到 native dll 的 Workaround, 打开工程对应的 csproj文件进行编辑, 添加前面提到的 Workaround, 正确配置 native dll 加载需要的信息:

代码语言:javascript
复制
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>

    <!-- Workaround for issue https://github.com/microsoft/ClangSharp/issues/129 -->
    <RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == '' AND '$(PackAsTool)' != 'true'">$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CppAst" Version="0.13.0" />
  </ItemGroup>

</Project>

也就是上面的 RuntimeIdentifier 项, 这个是必须的, 不然容易出现运行时找不到 libclang 的 native dll的报错.

3.1.4 添加示例代码后测试运行对应的App

在Program.cs的Main()函数中添加测试代码:

代码语言:javascript
复制
static void Main(string[] args)
{
    // Parse a C++ files
    var compilation = CppParser.Parse(@"
enum MyEnum { MyEnum_0, MyEnum_1 };
void function0(int a, int b);
struct MyStruct { int field0; int field1;};
typedef MyStruct* MyStructPtr;
"
    );
    // Print diagnostic messages
    foreach (var message in compilation.Diagnostics.Messages)
        Console.WriteLine(message);

    // Print All enums
    foreach (var cppEnum in compilation.Enums)
        Console.WriteLine(cppEnum);

    // Print All functions
    foreach (var cppFunction in compilation.Functions)
        Console.WriteLine(cppFunction);

    // Print All classes, structs
    foreach (var cppClass in compilation.Classes)
        Console.WriteLine(cppClass);

    // Print All typedefs
    foreach (var cppTypedef in compilation.Typedefs)
        Console.WriteLine(cppTypedef);


    Console.WriteLine("Hello World!");
}

编译运行程序:

image.png

我们的第一个 http://CppAst.Net 应用就跑起来了, 是不是比预期的简单很多? 说完如何运行示例代码, 我们再回过头来探索 http://CppAst.Net 的类型系统.

3.2 C# 侧完整表达的类型系统

前文中其实也提到了, http://CppAst.Net 几个比较有优势的点, 我们这里再总结一下: 1. 配置使用简单, 支持直接从字符串解析 C++代码 2. C#层有完整的数据层 - 代码Parse后会直接将所有信息C#化并存储在我们前面代码中看到的 CppCompilation 中 3. C# 层中对 C++类型系统的重建还原比较好

第三点通过 http://CppAst.Net 的类图其实就能看出来:

image.png

再加上具体C#类型实现上的Property, 如我们前面看到的CppCompilation上的各个属性: - Namespaces - 编译单元中包含的命名空间 - Enums - 编译单元包含的枚举 - Functions - 编译单元中包含的函数(一般就是全局函数了) - Classes - 编译单元中包含的类 - Typedefs - 编译单元中包含的 typedef 类型 - ... 通过C#侧重新组织整个AST的外观, 我们实际体验会发现对整个 C++ AST 的信息读取和多遍操作变简单了.

3.3 从 Test 了解 http://CppAst.Net 的基础功能

我们快速了解 http://CppAst.Net 的基础功能, 直接下载 http://CppAst.Net源代码, 参考运行其中的 CppAst.Tests 工程是比较推荐的方式, 其中的代码都是针对具体功能的测试, 而且绝大部分都是像我们上面例子中一样, 直接用字符串的方式传入 C++ 代码后再 Parse 生成 CppCompilation 后再去测试其中的功能, 我们可以调整其中的实现进行快速测试, 从中了解 CppAst.Net 提供的 Features 是非常方便的.


4. 强化 http://CppAst.Net

实际使用的过程中我们也发现了 http://CppAst.Net 原有版本(0.10版) 实现的一些功能缺失, 我们在 0.11, 0.12, 0.13版逐步追加了一些功能, 主要包括: 1. 模板, 以及模板特化, 部分特化的支持 2. CppType 的 FullName和Name的支持 3. 高性能的 meta attribute实现支持 以更好的支持我们的项目, 本身也是一个对 http://CppAst.Net开源库索取和反哺的过程:

image.png

image.png

image.png

个人感觉这其实也是一种比较不错的协作方式, 一方面我们通过引入 CppAst.Net, 首先是原来自己维护的大量代码变为了一个第三方库, 我们甚至可以不使用源码, 直接以 NuGet 的方式引入对应包就可以了, 对于团队其他小伙伴要了解相关工具的实现, 就变得比原来简单非常多. 另外在项目实际落地的过程中, 我们也会发现一些 http://CppAst.Net 不那么完善或者功能有缺失的地方, 这样我们补齐相关功能后反哺到开源库, 本身也会让 http://CppAst.Net 变得更完备更好用. 下文我们会具体展开一下模板相关的功能和 meta attribute 相关的功能.

4.1 模板的支持与处理

当时提交 Template 相关的 PR 的时候, 因为是多个 commit 一次提交的, 有一个 PR 漏合了, 所以当时社区有人发现有问题并提交了相关的测试代码:

image.png

后面我就直接用这个代码作为单元测试代码, 并且完整添加了模板偏特化和部分特化的支持, 让 http://CppAst.Net 显示的区分了模板参数和模板特化参数, 这样就能更好的获取模板, 以及模板特化实例相关的信息了, 这里我们直接来看一下相关的单元测试代码:

代码语言:javascript
复制
[Test]
        public void TestTemplatePartialSpecialization()
        {
            ParseAssert(@"
template<typename A, typename B>
struct foo {};

template<typename B>
struct foo<int, B> {};

foo<int, int> foobar;
",
                compilation =>
                {
                    Assert.False(compilation.HasErrors);

                    Assert.AreEqual(3, compilation.Classes.Count);
                    Assert.AreEqual(1, compilation.Fields.Count);

                    var baseTemplate = compilation.Classes[0];
                    var fullSpecializedClass = compilation.Classes[1];
                    var partialSpecializedTemplate = compilation.Classes[2];

                    var field = compilation.Fields[0];
                    Assert.AreEqual(field.Name, "foobar");

                    Assert.AreEqual(baseTemplate.TemplateKind, CppAst.CppTemplateKind.TemplateClass);
                    Assert.AreEqual(fullSpecializedClass.TemplateKind, CppAst.CppTemplateKind.TemplateSpecializedClass);
                    Assert.AreEqual(partialSpecializedTemplate.TemplateKind, CppAst.CppTemplateKind.PartialTemplateClass);

                    //Need be a specialized for partial template here
                    Assert.AreEqual(fullSpecializedClass.SpecializedTemplate, partialSpecializedTemplate);

                    //Need be a full specialized class for this field
                    Assert.AreEqual(field.Type, fullSpecializedClass);

                    Assert.AreEqual(partialSpecializedTemplate.TemplateSpecializedArguments.Count, 2);
                    //The first argument is integer now
                    Assert.AreEqual(partialSpecializedTemplate.TemplateSpecializedArguments[0].ArgString, "int");
                    //The second argument is not a specialized argument, we do not specialized a `B` template parameter here(partial specialized template)
                    Assert.AreEqual(partialSpecializedTemplate.TemplateSpecializedArguments[1].IsSpecializedArgument, false);

                    //The field use type is a full specialized type here~, so we can have two `int` template parmerater here
                    //It's a not template or partial template class, so we can instantiate it, see `foo<int, int> foobar;` before.
                    Assert.AreEqual(fullSpecializedClass.TemplateSpecializedArguments.Count, 2);
                    //The first argument is integer now
                    Assert.AreEqual(fullSpecializedClass.TemplateSpecializedArguments[0].ArgString, "int");
                    //The second argument is not a specialized argument
                    Assert.AreEqual(fullSpecializedClass.TemplateSpecializedArguments[1].ArgString, "int");
                }
            );
        }

4.2 meta attribute 支持

这一部分我们在http://CppAst.Net的代码仓库里添加了一个具体的文档 attributes.md, 感兴趣的读者可以自行查阅, 主要是用来解决上面提到的需要对某些导出项进行配置, 但又不希望代码项和配置信息分离的问题的, 原来 CppAst.Net 也有一版自己的基于再次 token parse 的 token attributes实现, 不过实际用于项目存在一些社区中比较多反馈的问题: 1. ParseAttributes() 耗时巨大, 所以导致了后来的版本中加入了ParseAttributes 参数来控制是否解析 attributes, 但某些场合, 我们需要依赖 attributes 才能完成相关的功能实现. 这显然带来了不便.

  1. meta attribute - [[]] 的解析存在缺陷, 像 FunctionField 上方定义的 meta attribute, 在语义层面, 显然是合法的, 但 cppast.net 并不能很好的支持这种在对象上方定义的meta attribute (这里存在一些例外情况, 像 namespace, class, enum 这些的 attribute 声明, attribute定义本身就不能位于上方, 相关的用法编译器会直接报错, 只能在相关的关键字后面, 如 class [[deprecated]] Abc{}; 这种 ).
  2. meta attribute 个别参数使用宏的情况. 因为我们原有的实现是基于 token 解析来实现的, 编译期的宏显然不能很好的在这种情况下被正确处理.

所以这一部分我们沿用了G6时的思路, 重新对 http://CppAst.Net的相关实现做了重构和扩展, 我们重新将 attribute 分为了三类: 1. AttributeKind.CxxSystemAttribute - 对应的是 libclang 本身就能很好的解析的各类系统 attribute, 如上面提到的 visibility, 以及 [[deprecated]], [[noreturn]] 等. 借助ClangSharp 就能够高效的完成对它们的解析和处理, 也就不需要考虑开关的问题了. 2. AttributeKind.TokenAttribute - 从名字上我们能猜出, 这对应的是cppast.net原来版本中的 attribute, 已经标记为 deprecated 了, 但token 解析始终是一种保底实现机制, 我们会保留相关的 Tokenizer 的代码, 在一些 ClangSharp 没有办法实现相关功能的情况谨慎的使用它们来实现一些复杂功能. 3. AttributeKind.AnnotateAttribute - 用来取代原先基于 token 解析实现的 meta attribute, 以高性能低限制的实现前面介绍的为类和成员注入的方式.

而我们用来解决配置数据注入的, 就是第三种实现 AttributeKind.AnnotateAttribute 了. 下面我们来简单看一下它的实现和使用:

4.2.1 AttributeKind.AnnotateAttribute

我们需要一种绕开token 解析的机制来实现 meta attribute, 这里我们巧妙的使用了 annotate 属性来完成这一项操作, 从新增的几个内置宏我们可以看出它是如何起作用的:

代码语言:javascript
复制
//Add a default macro here for CppAst.Net
            Defines = new List<string>() { 
                "__cppast_run__",                                     //Help us for identify the CppAst.Net handler
                @"__cppast_impl(...)=__attribute__((annotate(#__VA_ARGS__)))",          //Help us for use annotate attribute convenience
                @"__cppast(...)=__cppast_impl(__VA_ARGS__)",                         //Add a macro wrapper here, so the argument with macro can be handle right for compiler.
            };

[!note] 此处的三个系统宏不会被解析为 CppMacro 加入最终的解析结果中, 避免污染输出结果.

最终我们其实只是将 __VA_ARGS__ 这个可变参数转为字符串并利用 __attribute__((annotate(???))) 来完成信息的注入, 这样如果我们像测试代码中一样, 在合适的地方加入:

代码语言:javascript
复制
#if !defined(__cppast)
#define __cppast(...)
#endif

当代码被 cppast.net 解析时, 相关的输入会被当成annotate attribute 被正确的识别并读取, 而在非 cppast.net 的情况下, 代码也能正确的忽略__cppast() 中注入的数据, 避免干扰实际代码的编译执行, 这样我们就间接的完成了 meta attribute 注入和读取的目的了.

对于宏的情况:

代码语言:javascript
复制
#if !defined(__cppast)
#define __cppast(...)
#endif

#define UUID() 12345

__cppast(id=UUID(), desc=""a function with macro"")
void TestFunc()
{
}

相关的测试代码:

代码语言:javascript
复制
//annotate attribute support on namespace
var func = compilation.Functions[0];
                    Assert.AreEqual(1, func.Attributes.Count);
                    Assert.AreEqual(func.Attributes[0].Kind, AttributeKind.AnnotateAttribute);
                    Assert.AreEqual(func.Attributes[0].Arguments, "id=12345, desc=\"a function with macro\"");

因为我们定义__cppast 的时候, 做了一次 wrapper 包装, 我们发现宏也能很好的在meta attribute 状态下工作了.

而对于outline attribute 的情况, 像Function, Field, 本身就能很好的支持, 甚至你可以在一个对象上定义多个attribute, 同样也是合法的:

代码语言:javascript
复制
__cppast(id = 1)
__cppast(name = "x")
__cppast(desc = "???")
float x;

5. 小结

本文我们从离线代码生成工具出发, 逐步介绍了:

1. 代码生成相关的内容

2. libclang带来的改变和限制

3. http://CppAst.Net的基础使用

4. http://CppAst.Net的两个扩展实现, 模板的支持和 meta attribute的注入和使用

希望大家通过阅读本文能够对如何处理 C++ AST以及如何使用 http://CppAst.Net 有一个初步的认知.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • 1. 代码生成简介
  • 2. libclang - 带来希望, 也带来迷茫
  • 2.1 libclang 和它带来的改变
  • 2.2 libclang 本身的 Cursor 机制带来的限制
  • 2.3 C++ 类型系统的复杂度
  • 3. http://CppAst.Net - 新的天花板
  • 3.1 简单配置即可上手使用
  • 3.1.1 新建工程
  • 3.1.2 通过 NuGet 添加 CppAst.Net 包
  • 3.1.3 针对 csproj 的 Workaround
  • 3.1.4 添加示例代码后测试运行对应的App
  • 3.2 C# 侧完整表达的类型系统
  • 3.3 从 Test 了解 http://CppAst.Net 的基础功能
  • 4. 强化 http://CppAst.Net
  • 4.1 模板的支持与处理
  • 4.2 meta attribute 支持
  • 4.2.1 AttributeKind.AnnotateAttribute
  • 5. 小结
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档