Loading [MathJax]/jax/output/CommonHTML/config.js
部署DeepSeek模型,进群交流最in玩法!
立即加群
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >轻松掌握C++ AST的处理方法 - CppAst.Net使用介绍

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

作者头像
fangfang
发布于 2023-10-16 07:28:19
发布于 2023-10-16 07:28:19
64000
代码可运行
举报
文章被收录于专栏:方方的杂货铺方方的杂货铺
运行总次数:0
代码可运行

导语

现代的游戏引擎一般都会较重度的依赖代码生成技术, 而代码生成技术一般都是以原始代码为处理信息源, 再结合专用的配置来做进一步的处理. 发展到后来, 就渐渐变成原始代码和配置一体化的形式了. 比如大家熟知的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
代码运行次数:0
运行
AI代码解释
复制
//-------------------------------------
//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
代码运行次数:0
运行
AI代码解释
复制
//-------------------------------------
//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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
<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
代码运行次数:0
运行
AI代码解释
复制
<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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
[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
代码运行次数:0
运行
AI代码解释
复制
//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
代码运行次数:0
运行
AI代码解释
复制
#if !defined(__cppast)
#define __cppast(...)
#endif

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

对于宏的情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#if !defined(__cppast)
#define __cppast(...)
#endif

#define UUID() 12345

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

相关的测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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
代码运行次数:0
运行
AI代码解释
复制
__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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
(译) Understanding Elixir Macros, Part 6 - In-place Code Generation
这是宏系列文章的最后一篇. 在开始之前, 我想提一下 Björn Rochel, 他已经将他的 Apex 库中的 deftraceable 宏改进了. 因为他发现系列文章中 deftraceable 的版本不能正确处理默认参数(arg \ def_value), 于是实现了一个修复 fix.
Cloud-Cloudys
2023/10/21
1990
(译) Understanding Elixir Macros, Part 4 - Diving Deeper
在前一篇文章中, 我向你展示了分析输入 AST 并对其进行处理的一些基本方法. 今天我们将研究一些更复杂的 AST 转换. 这将重提已经解释过的技术. 这样做的目的是为了表明深入研究 AST 并不是很难的, 尽管最终的结果代码很容易变得相当复杂, 而且有点黑科技(hacky).
Cloud-Cloudys
2023/10/21
1240
(译) Understanding Elixir Macros, Part 3 - Getting into the AST
是时候继续探索 Elixir 的宏了. 上次我介绍了一些关于宏的基本原理, 今天, 我将进入一个较少谈及的领域, 并讨论Elixir AST 的一些细节.
Cloud-Cloudys
2023/10/21
1800
(译) Understanding Elixir Macros, Part 2 - Micro Theory
这是 Elixir 中的宏系列的第二篇. 上一次我们讨论了编译过程和 Elixir AST, 最后讲了一个基本的宏的例子 trace. 今天, 我们会更详细地讲解宏的机制.
Cloud-Cloudys
2023/10/21
1720
(译) Understanding Elixir Macros, Part 1 Basics
这是讨论宏 (Macros) 微系列文章的第一篇. 我原本计划在我即将出版的《Elixir in Action》一书中讨论这个主题, 但最终决定不这么做, 因为这个主题不符合这本书的主题, 这本书更关注底层 VM 和 OTP 的关键部分.
Cloud-Cloudys
2023/10/21
2110
(译) Understanding Elixir Macros, Part 1 Basics
Scala Macros - 元编程 Metaprogramming with Def Macros
    Scala Macros对scala函数库编程人员来说是一项不可或缺的编程工具,可以通过它来解决一些用普通编程或者类层次编程(type level programming)都无法解决的问题,这
用户1150956
2018/01/05
3.2K0
深入浅出 Babel 下篇:既生 Plugin 何生 Macros
我想我们对宏并不陌生,因为很多程序员第一门语言就是 C/C++; 一些 Lisp 方言也支持宏(如 Clojure、Scheme), 听说它们的宏写起来很优雅;一些现代的编程语言对宏也有一定的支持,如 Rust、Nim、Julia、Elixir,它们是如何解决技术问题, 实现类Lisp的宏系统的?宏在这些语言中扮演着什么角色...
Nealyang
2019/10/18
1.6K0
the-solution-of-elixir-continuous-runtime-system-code-coverage-collection
Code coverage is an effective means to assist software engineers in verifying code quality. The runtime environment’s ability to collect code coverage fully combines black and white box testing capabilities and greatly increases engineers’ confidence in software quality. This article introduces a solution for code coverage collection in the Elixir runtime environment, and provides an in-depth insight into its internal principles.
Cloud-Cloudys
2023/10/21
1850
the-solution-of-elixir-continuous-runtime-system-code-coverage-collection
命令行中 tree 的多重实现
解题思路 利用递归,将目录转换成 {:name: ".", :children: []} 结构 对于第一层目录名,前缀装饰成 T_branch = "├── "或者 L_branch = "└── " 对于子目录,前缀装饰成 I_branch = "│ "或者SPACER = " " 举例如下: . ├── tree.py # 不是最后一项,所以使用 T_branch 前缀 ├── files.py ├── lists.py ├── tuples.py ├── resources │ └── RE
lambeta
2018/08/17
6490
Elixir 连续运行时代码覆盖率采集方案
作为 SET 和 SWE, 我们经常需要编写单元测试或集成测试用例来验证系统/应用的正确性, 但同时我们也常会质疑我们的测试是否充分了. 这时测试覆盖率是可以辅助用来衡量我们测试充分程度的一种手段, 增强发布成功率与信心, 同时给了我们更多可思考的视角. 值的注意的是代码覆盖率高不能说明代码质量高, 但是反过来看, 代码覆盖率低, 代码质量不会高到哪里去.
Cloud-Cloudys
2023/10/21
3860
Elixir 连续运行时代码覆盖率采集方案
"超级简单!Elixir和ScyllaDB教你创建CRUD CLI,惊人的效率提升!"
如果您了解用于通信的高流量应用程序、需要低延迟和良好容错能力的应用程序,您很可能已经遇到过 Elixir(作为一种编程语言)和 ScyllaDB(一种旨在低延迟的 NoSQL 数据库)的名称。两者的目标非常相似:处理通常需要更加关注稳定性的应用程序。
zayyo
2023/09/21
5430
听GPT 讲Rust源代码--compiler(37)
在Rust编译器的源代码中,rust/compiler/rustc_expand/src/errors.rs文件的作用是定义了各种错误类型和帮助信息,这些错误和帮助信息用于扩展宏时的错误处理和用户提示。
fliter
2024/04/15
1600
听GPT 讲Rust源代码--compiler(37)
Apache Velocity-----基于Java的模板引擎
Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。
wuweixiang
2018/08/14
10.3K0
听GPT 讲Rust源代码--src/tools(39)
在Rust代码中,rust/src/tools/rustfmt/src/config/config_type.rs文件的作用是定义了与配置相关的数据结构和函数。
fliter
2024/02/26
1490
Rust 写脚手架,Clap你应该知道的二三事
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder。
前端柒八九
2024/03/18
3920
Rust 写脚手架,Clap你应该知道的二三事
光剑评注:其实,说了这么多废话,无非就是: 一切皆是映射。不管是嵌套 XML,还是 Lisp 嵌套括号,还是 XXX 的 Map 数据结构,一切都是树形结构——映射。Lisp的本质(The Natur
http://www.defmacro.org/ramblings/lisp.html
一个会写诗的程序员
2018/08/17
1.5K0
听GPT 讲Rust源代码--compiler(15)
在Rust源代码中,rustc_arena/src/lib.rs文件定义了TypedArena,ArenaChunk,DroplessArena和Arena结构体,以及一些与内存分配和容器操作相关的函数。
fliter
2024/03/18
1760
听GPT 讲Rust源代码--compiler(15)
听GPT 讲Rust源代码--src/tools(22)
rust/src/tools/tidy/src/lib.rs是Rust编译器源代码中tidy工具的实现文件之一。tidy工具是Rust项目中的一项静态检查工具,用于确保代码质量和一致性。
fliter
2024/01/09
2440
TVM 学习指南(个人版)
最近粗略的看完了天奇大佬的MLC课程(顺便修了一些语法和拼写错误,也算是做了微弱的贡献hh),对TVM的近期发展有了一些新的认识。之前天奇大佬在《新一代深度学习编译技术变革和展望》一文中(链接:https://zhuanlan.zhihu.com/p/446935289)讲解了TVM Unify也即统一多层抽象的概念。这里的统一多层抽象具体包括AutoTensorization用来解决硬件指令声明和张量程序对接,TVM FFI(PackedFunc)机制使得我们可以灵活地引入任意的算子库和运行库函数并且在各个编译模块和自定义模块里面相互调用。TensorIR负责张量级别程序和硬件张量指令的整合。Relax (Relax Next) 引入relay的进一步迭代,直接引入first class symbolic shape的支持 (摘抄自《新一代深度学习编译技术变革和展望》一文)。然后这些抽象可以相互交互和联合优化来构造深度学习模型对应的最终部署形式。我个人感觉TVM Unify类似于MLIR的Dialect,但是这几个抽象的直接交互能力相比于MLIR的逐级lower我感觉是更直观方便的,毕竟是Python First(这个只是我最近看MLC课程的一个感觉)。对这部分内容感兴趣的读者请查看天奇大佬的TVM Unify介绍原文以及MLC课程。
BBuf
2022/09/28
3.8K0
TVM 学习指南(个人版)
Webpack——从基础使用到手动实现(万字长文)
这个其实不用多说了,如今的前端项目哪里还有用不到打包工具的呢,而webpack又作为打包工具中的王者我们又有什么理由不去搞明白它呢。
coder_koala
2020/07/15
1.1K0
Webpack——从基础使用到手动实现(万字长文)
推荐阅读
相关推荐
(译) Understanding Elixir Macros, Part 6 - In-place Code Generation
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验