我可以从 cppyy 获取 AST

Posted

技术标签:

【中文标题】我可以从 cppyy 获取 AST【英文标题】:Can I get the AST from cppyy 【发布时间】:2020-04-17 00:45:02 【问题描述】:

我想在创建 python 绑定之前访问 cppyy 中的 AST。我想用它来生成其他类型的绑定。

我见过cppyy-generator,但它需要在机器上单独安装clang。由于 cppyy 可以在没有单独安装 clang 的情况下进行 JIT 编译,我必须相信 AST 可以从底层的 Cling 解释器中获得。有没有办法从 cppyy 获取这个 AST 信息?

示例:

import cppyy

cppyy.cppdef("""
namespace foo

class Bar

public:
    void DoSomething() 
;


""")

cppyy 可以(惊人地)为我生成 cppyy.gbl.foo.Bar。这意味着它必须使用 Cling 来编译、获取 AST 并生成 python。如何查看 AST 数据?

谢谢!

编辑

我可以看到我需要的大部分信息都在 cppyy-backend capi 和 cpp_cppyy 文件中。但是,我的 CPython foo 不够强大,无法弄清楚这些是如何被调用的,以及我如何从 python 脚本中访问它们。

编辑2

目前我们正在使用 castxml 和 pygccxml 的组合来生成表示 AST 的 python 数据结构。我看到与 cppyy 有很多重叠,并希望仅减少对 cppyy 的依赖,因为我们已经将它用于其他事情并且它是独立的。

我们将 AST 数据用于多种用途。一个重要的问题是代码生成。所以我们想对 AST 进行多次迭代 like you can with pygccxml。

【问题讨论】:

【参考方案1】:

这里有一些歧义,b/c 相同的名称适用于不同的步骤和不同的地方。让我解释一下结构(和历史),它甚至可以回答你的问题。

cppyy-generator 使用 Clang Python 绑定。因此,它访问的 AST 是 C++ 的,并且它完全(丑陋)的荣耀可用。您不需要 cppyy 的任何部分来使用 Clang Python 绑定。 cppyy-generator 服务于一个特定的用例,您希望将所有本地 C++ 实体预加载到 Python 模块中。由于 cppyy 利用惰性一切和自动加载,出于性能原因,“所有 C++ 实体”(本地或其他)的概念没有明确定义的含义。因此使用了 libclang,概念清晰。

cppyy-backend capi(或 C-API)是一个 API,它是在 reductio 中开发的,用于服务于 cppyy 的 PyPy 实现。它是一个用于引导 cppy/C++ 的 C 风格 API。它被简化为编写 Python-C++ 绑定的本质,隐藏了 Clang AST 的许多不相关的细节(例如,模板可以在 Clang AST 中存在的 15 种左右的方式被简化为“IsTemplate”等)。后端 C-API 完全不依赖或使用 Python。

后端 C-API 的实现相当不漂亮。部分是由于历史原因(一件坏事),部分是为了隐藏所有 Cling 和 Clang,以防止与可能正在使用 Clang 或 LLVM 的应用程序的其他部分发生冲突(一件好事;正在使用的 Clang 版本by Cling 是定制的,可能不适用于 Numba)。同样,所有这些都完全独立于 Python。

然后,它在 Python 中的使用。有两种不同的实现:用于 CPython 的 CPyCppyy,用 C 实现,和 PyPy _cppyy 模块,用 RPython 实现。两者都通过 C-API 执行从 Python 跨入 C++ 的咒语。既不生成也不使用 Python AST:都直接生成和操作 Python 实体。这是懒惰地发生的。仔细考虑这些步骤:在上面的示例中,Python 用户将输入类似cppyy.gbl.foo.Bar().DoSomething() 的内容。在cppyy中,使用Python的__getattr__来截取名字,然后简单的通过后端向Cling询问是否知道fooBar等。例如C-APIGetScope("foo")将返回一个有效的标识符,因此 CPyCppyy/_cppyy 知道生成一个 Python 类来表示命名空间。然而,它在任何时候都不会完全扫描 AST 中的全局(甚至foo)命名空间来生成先验绑定。根据您的描述,CPyCppyy/_cppyy 中没有对您有用的内容。

回到您的第一个语句,您想要生成其他类型的绑定。您没有说明什么类型的绑定,但选择 C-API 的主要原因是它位于 Cling 之上,而不是 Clang,因为 Clang AST 直接来自 C++ 或通过其 Python 绑定。 Cling 提供了对 JIT 的轻松访问,但您也可以直接从 Clang(它的库,而不是 AST)对其进行编程。作为这种 easyu 访问的示例,在后端 C-API 中,您只需将要 JITted 的 C++ 字符串转储到 compile 函数中(与您的示例中的 cppdef 完全相同)。 Cling 人员计划直接为来自 Cling 的动态语言提供更好的接口,但这是一项正在进行的工作,并且 (AFAIK) 尚不可用。

最后,请注意 Cling 包含 Clang,所以如果你安装 Cling,你仍然会得到 Clang(和 LLVM),这可能是一个严重的依赖项。

编辑:从根本上说,与其他工具相反,cppyy 不提供起点列表(例如“所有类”),也不提供完整/真实的 AST。您可以从后端复制cpp_cppyy.h 标头(否则它不是安装的一部分),只需包含它并使用它(所有符号都已导出),但您需要先验地知道类列表。示例:

import cppyy

cppyy.cppdef('#define RPY_EXPORTED extern')
cppyy.include('cpp_cppyy.h')

import cppyy

cppyy.cppdef("""
namespace foo 
class Bar 
public:
    void DoSomething() 
;
""")

cpp = cppyy.gbl
capi = cpp.Cppyy

scope_id = capi.GetScope(cpp.foo.Bar.__cpp_name__) # need to know existence
for i in range(capi.GetNumMethods(scope_id)):
     m = capi.GetMethod(scope_id, i)
     print(capi.GetMethodName(m))

但正如您所见,它不提供与原始代码一对一的结果。例如,所有编译器生成的构造函数和析构函数都被列为方法。

后端 API 中也没有任何东西,如您链接的 pygccxml 文档中的 run_functions = unittests.member_functions('run')。原因是这样在 cppyy 的上下文中没有任何意义。例如。如果另一个头文件加载了更多run 函数怎么办?如果它是一个模板函数并且弹出更多实例怎么办?如果using namespace ... 出现在后面的代码中,引入更多run 重载怎么办?

cppyy 确实有 GetAllCppNames C-API 函数,但不能保证它是详尽无遗的。它的存在是为了代码编辑器中的制表符补全(在绑定范围的自定义__dir__ 函数中调用它)。事实上,正是因为不完整,cppyy-generator 使用了 libclang。

您在 cmets 中提到了 gInterpreter,但这是我之前提到的历史的一部分:它是 libclang 提供的完整 AST 和 Python 所需的极简 AST(例如后端 C -API)。是的,您可以直接使用它(事实上,它仍然在后端 C-API 下使用),但它更笨重,没有什么好处。

例如,要处理“获取所有'运行'方法”示例,您可以这样做:

import cppyy

cppyy.cppdef("""
namespace foo 
void run(int) 
void run(double) 
""")

cpp = cppyy.gbl

# using the ROOT/meta interface
cls = cpp.CppyyLegacy.TClass.GetClass(cpp.foo.__cpp_name__)
print('num "run" overloads:"', cls.GetListOfMethodOverloads('run').GetSize())

# directly through gInterpreter
gInterp = cpp.gInterpreter

cls = gInterp.ClassInfo_Factory(cpp.foo.__cpp_name__)
v = cpp.std.vector['const void*']()
gInterp.GetFunctionOverloads(cls, 'run', v)
gInterp.ClassInfo_Delete(cls)

print('num "run" overloads:"', len(v))

但前一个界面(通过 CppyyLegacy.TClass)可能不会保留下来,而且 gInterpreter 真的很难看。

我很确定你不会乐意尝试让 cppyy 取代 pygccxml 的使用,如果我是你,我会改用 Clang Python 绑定。

【讨论】:

感谢您的历史。 cppyy 包含 cling,其中包含 clang。我很惊讶要使用 cppyy-generator 我必须单独导入 clang 包并安装 LLVM/clang 以指向 libclang。我认为您是说 libclang 由于 cppyy-backend 安装而存在于 python 的某个地方,但这使得 cppyy-generator 无法找到自己的 libclang 依赖项更加令人困惑。 参见 Edit2 了解动机。我想使用 capi 通过 cppyy 获取 AST 数据。我可以在cppyy.gbl.gInterpreter 上调用各种函数,但我正在努力弄清楚如何在 python 脚本中调用 capi。你能给我一个例子,说明我如何获得类似于 cppyy-generator 为 cppyy 类声明吐出的信息,就像我的例子一样?谢谢! 对于您的第一条评论:使用 libclang 正是因为 Clang 在 Cling 中“过于隐藏”,因此几乎不可能用于其他目的。请注意,cppyy-generator 是贡献的。我很想将其更改为使用 Cling 的内部 clang,但我还没有时间这样做。我已经用一些代码示例更新了答案,但我不认为使用 cppyy 来替换 pygccxml 会让你的生活变得轻松。 您是指生成(JITed)代码的速度吗?如果从 cppyy 使用,这几乎是相同的(vanilla cling 会进行指针检查以防止 nullptr 取消引用,这既昂贵又会扼杀许多优化)。您还可以设置自定义选项。或者你的意思是编译本身?在那里,Cling 明确删除了某些使用成本高但几乎没有运行时优势的优化通道。 Cling 现在也支持预编译模块,但是 cppyy 还没有使用它,因为它只在 Linux 上受支持。这些将编译速度提高了一个数量级。 现在我不再 100% 确定我们在谈论同一件事。可以肯定的是:JIT 不编译任何 Python 代码,只编译 C++ 代码。是的,C++ 代码以本机速度运行。将 C++ 代码放入 cppyy.cppdef 然后调用它就像手动执行 Numba(如果这种比较有意义的话),速度相同。如果您需要 JIT Python 代码,请使用 PyPy(此时,如果您在 PyPy 中使用 cppyy,则涉及 两个 JIT,尽管如果库函数可用,cppyy 将绕过 clang JIT 和通过 cffi_backend 调用这些函数)。

以上是关于我可以从 cppyy 获取 AST的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 cppyy 从指针索引类数组

cppyy 无法打开共享库

cppyy - 如何调用接受集合的 c++ 函数?

从 c++ 中用 cppyy 读取 char16_t* 字符串

pyhonizing STL 向量等的 cppyy 源代码在哪里

如何通过备注从 Markdown 中获取 AST 树