LLVM 项目博客

LLVM 项目新闻和来自一线的信息

Cling -- 不仅仅是 C++ 解释器

使用 Cling 进行交互式 C++

在我们之前的博客文章 “数据科学中的交互式 C++” 中,我们描述了 eval 风格的编程、笔记本中的交互式 C++ 以及 CUDA。本文将讨论 Cling 的一些开发应用,这些应用支持互操作性和可扩展性。我们的目标是展示按需模板实例化、将 Cling 嵌入为服务,并展示一个允许进行即时自动微分的扩展。

按需模板实例化

Cling 实现了一个名为 LookupHelper 的功能,它接收 C++ 代码并检查是否已存在具有该限定名称的声明。例如

[cling] #include "cling/Interpreter/Interpreter.h"
[cling] #include "cling/Interpreter/LookupHelper.h"
[cling] #include "clang/AST/Decl.h"
[cling] struct S{};
[cling] cling::LookupHelper& LH = gCling->getLookupHelper()
(cling::LookupHelper &) @0x7fcba3c0bfc0
[cling] auto D = LH.findScope("std::vector<S>",
                 cling::LookupHelper::DiagSetting::NoDiagnostics)
(const clang::Decl *) 0x1216bdcd8
[cling] D->getDeclKindName()
(const char *) "ClassTemplateSpecialization"

在这种特定情况下,findScope 会实例化模板并返回其 clang AST 表示。按需模板实例化解决了模板组合爆炸这一常见的库问题。按需模板实例化以及将文本限定的 C++ 名称转换为实体元信息已被证明是一种非常强大的机制,有助于数据序列化和语言互操作性。

按需语言互操作性

一个例子是 cppyy,它通过 Cling 为 C++ 代码提供运行时自动 Python 绑定。Python 本身是一种由解释器执行的动态语言,因此在由 Cling 中介的情况下,与 C++ 代码的交互更加自然。示例包括运行时模板实例化、函数(指针)回调、跨语言继承、自动向下转换和异常映射。许多高级 C++ 特性,如就地 new、多重虚继承、可变参数模板等,都可以通过 LookupHelper 自然地解决。

cppyy 通过对运行时绑定构建采用全延迟方法以及通过运行时反射对常见情况进行特殊化来实现高性能。因此,它比例如 pybind11 具有更低的调用开销,并且通过 cppyy 循环遍历 std::vector 比循环遍历相同类型的 numpy 数组更快。更进一步,它针对 PyPy 的实现,PyPy 是一种完全兼容的 Python 解释器,具有 跟踪 JIT,在许多情况下可以为 PyPy 中的 JIT 提供对 C++ 代码的本机访问,包括重载解析和 JIT 提示,这些提示允许进行积极的优化。

由于 Cling 的运行时反射,cppyy 使维护大型软件堆栈变得更简单:除了 cppyy 自己的 python 解释器绑定之外,它没有任何依赖 Python 的编译代码。也就是说,基于 cppyy 的扩展模块在切换 Python 版本时(甚至是在切换 CPython 和 PyPy 解释器之间时,例如)不需要重新编译。

下面的示例展示了 C++ 和 python 的紧密集成;展示了模板实例化和跨继承覆盖的紧密来回通信;以及运行时行为(所有操作都在运行时发生,这里没有编译代码)。

import cppyy

cppyy.cppdef(r"""\
template<typename T> class Producer {
private:
  T m_value;

protected:
  virtual T produce_imp() = 0;

public:
  Producer(const T& value) : m_value(value) {}
  virtual ~Producer() {}

  T produce_total() { return m_value + produce_imp(); }
};

class Consumer {
public:
  template<typename T>
  void consume(Producer<T>& p) {
    std::cout << "received: \"" << p.produce_total() << "\"\n";
  }
};""")

def factory(base_v, *derived_v):
  class _F(cppyy.gbl.Producer[type(base_v)]):
    def __init__(self, base_v, *derived_v):
      super().__init__(base_v)
      self._values = derived_v

    def produce_imp(self):
      return type(base_v)(sum(self._values))

    return _F(base_v, *derived_v)

consumer = cppyy.gbl.Consumer()
for producer in [factory(*x) for x in \
                  (("hello ", 42), (3., 0.14, 0.0015))]:
  consumer.consume(producer)

输出

python3 cppyy_demo.py
received: "hello 42"
received: "3.1415"

在代码片段中,我们根据 python 参数创建 python 类,这些类派生自用类型实例化的模板化 C++ 类。python 类为一个受保护的函数提供实现,该函数从一个公共函数调用,从而导致预期的返回值,该返回值被打印出来。我们旨在强调

  • Python 在运行时创建类,就像 Cling 一样,即使它们是在模块中声明的(相关的类是在这里在一个工厂方法中创建的);
  • 模板化 C++ 类可以从 Python 中动态实例化,方法是获取参数的类型(即在 Python 中使用运行时内省)来为 Python 类创建 C++ 基类。
  • 跨语言派生是在运行时进行的,C++ 类不需要任何支持,除了虚析构函数和虚方法之外;
  • C++ 的“受保护”方法可以在 Python 中被重写,即使 Python 没有这样的概念,并且实际上你不能从绑定到 Python 的 C++ 对象中调用受保护的方法;
  • 这一切都开箱即用。

cppyy 被用于物理学、化学、数学和生物学领域的多个大型代码库中。它可以通过 [pip from PyPI] (https://pypi.ac.cn/project/cppyy/) 和 conda 轻松安装。

另一个例子是 Symmetry Integration Language (SIL),它是一种基于 D 的功能风格领域特定语言,由 Symmetry Investments 内部开发和使用。SIL 的主要目标之一是能够轻松地与各种语言和系统互操作,这是通过各种插件实现的。为了调用 C++ 代码,SIL 使用一个名为 sil-cling 的插件,它充当 SIL 和 Cling 之间的中间桥梁。然而,sil-cling 并不直接与 Cling 交互,而是通过 cppyy-backend 交互,cppyy-backend 是 cppyy 围绕 Cling 的 C/C++ 包装器,它提供了一个稳定的 C/C++ 反射 API。

从 sil-cling 公开到 SIL 的核心类型有两个。一个是 CPPNamespace,它公开了一个 C++ 命名空间,并允许自由函数调用、访问命名空间的变量以及为该命名空间中定义的类实例化对象。另一个是 ClingObj,它是 C++ 对象的代理,允许构造、方法调用和操作对象的成员数据。鉴于 cppyy 将 C++ 类、结构体和命名空间表示为“范围”,并且关于这些 C++ 实体的任何反射信息都是通过其关联的“范围”对象获得的,因此公开给 SIL 的两个包装器类型都持有对其关联范围对象的引用,该对象在包装器类型用于调用 C++ 代码时会被查询。

所有从 SIL 通过两个包装器类型进行的调用都有 3 个参数:使用的包装器对象、需要调用的 C++ 函数的名称以及(如果需要)该函数的参数序列。一旦重载解析和参数转换完成,sil-cling 就会调用相应的 cppyy 函数,该函数将包装调用并将调用调度到 Cling 以进行 JIT 编译。目前,sil-cling 可用于调用 Boost.AsiodlibXapian 等 C++ 库。

下面的示例使用 sil-cling 插件创建一个基于 Boost Asio 的客户机-服务器应用程序,该应用程序用 SIL 编写。Server.sil 包含服务器的 SIL 代码。它首先使用 cppCompile 包含相关的头文件。下一步是为所需的命名空间创建包装器对象,这是通过使用要访问的命名空间的名称调用 cppNamespace 来实现的。这些 CPPNamespace 包装器用于实例化在它们包装的 C++ 命名空间中定义的类。使用这些包装器,将创建端点、接受套接字(侦听传入连接)和活动套接字(处理与客户机的通信)。然后服务器等待连接,一旦客户机连接,它就会读取客户机的信息并发送回复。

// Server.sil
import * from silcling
import format from format

cppCompile ("#include <boost/asio.hpp>")
cppCompile ("#include \"helper.hpp\"")

// CPPNamespace wrappers
asio = cppNamespace("boost::asio")
tcp = cppNamespace("boost::asio::ip::tcp")
helpers = cppNamespace("helpme")

// Using namespace wrappers to instantiate classes - creates ClingObj(s)
ioService = asio.obj("io_service")
endpoint = tcp.obj("endpoint", tcp.v4(), 9999)

// Acceptor socket - incoming connections
acceptorSocket = tcp.obj("acceptor", ioService, endpoint)
// Active socket - communication with client
activeSocket = tcp.obj("socket", ioService)

// Waiting for connection and use the activeSocket to connect with the client
helpers.accept(acceptorSocket, activeSocket)

// Waiting for message
message = helpers.getData(activeSocket);
print(format("[Server]: Received \"%s\" from client.", message.getString()))

// Send reply
reply = "Hello \'" ~ message.getString() ~ "\'!"
helpers.sendData(activeSocket, reply)
print(format("[Server]: Sent \"%s\" to client.", reply))

Client.sil 包含客户机的 SIL 代码。作为服务器,它包含相关的头文件,为所需的命名空间创建包装器,并使用它们来创建端点和套接字。然后客户机连接到服务器,发送一条消息,并等待服务器的回复。一旦收到回复,客户机就会将其打印到屏幕上。

// Client.sil
import * from silcling
import format from format

cppCompile ("#include <boost/asio.hpp>")
cppCompile ("#include \"helper.hpp\"")

asio = cppNamespace("boost::asio")
tcp = cppNamespace("boost::asio::ip::tcp")
helpers = cppNamespace("helpme")

// Scope resolution operator <-> address::static_method() or address::static_member
address = classScope("boost::asio::ip::address")

ioService = asio.obj("io_service")
endpoint = tcp.obj("endpoint", address.from_string("127.0.0.1"), 9999)

// Creating socket
client_socket = tcp.obj("socket", ioService)
// Connect
client_socket.connect(endpoint)

message = "demo"
helpers.sendData(client_socket, message)
print(format("[Client]: Sent \"%s\" to server.", message))

message = helpers.getData(client_socket);
print(format("[Client]: Received \"%s\" from server.", message.getString()))

输出

[Client]: Sent "demo" to server.
[Server]: Received "demo" from client.
[Server]: Sent "Hello demo" to client.
[Client]: Received "Hello demo" from server.

解释器/编译器作为服务

Cling 的设计,就像 Clang 一样,允许它用作库。在下一个示例中,我们将展示如何在 C++ 程序中包含 libCling。Cling 可以按需用作服务,来编译、修改或描述 C++ 代码。示例程序展示了编译的 C++ 和解释的 C++ 交互的几种方式

  • callCompiledFn – cling-demo.cpp 定义了一个全局变量 aGlobal;一个静态 float 变量 anotherGlobal;以及它的访问器。interp 参数是之前创建的 Cling 解释器实例。就像在标准 C++ 中一样,将编译的实体转发声明到解释器中就足够了,以便能够使用它们。然后,来自对 process 的不同调用的执行信息存储在一个通用的 Cling Value 对象中,该对象用于在编译代码和解释代码之间交换信息。
  • callInterpretedFn – 作为 callCompiledFn 的补充,编译代码可以通过要求 Cling 从给定的损坏名称中形成一个函数指针来调用解释的函数。然后调用使用标准 C++ 语法。
  • modifyCompiledValue – Cling 完全理解 C++,因此我们可以支持对堆栈分配内存进行复杂底层操作。在示例中,我们要求编译器提供局部变量 loc 的内存地址,并要求解释器在运行时对它的值进行平方。
// cling-demo.cpp
// g++ ... cling-demo.cpp; ./cling-demo
#include <cling/Interpreter/Interpreter.h>
#include <cling/Interpreter/Value.h>
#include <cling/Utils/Casting.h>
#include <iostream>
#include <string>
#include <sstream>

/// Definitions of declarations injected also into cling.
/// NOTE: this could also stay in a header #included here and into cling, but
/// for the sake of simplicity we just redeclare them here.
int aGlobal = 42;
static float anotherGlobal = 3.141;
float getAnotherGlobal() { return anotherGlobal; }
void setAnotherGlobal(float val) { anotherGlobal = val; }

///\brief Call compiled functions from the interpreter.
void callCompiledFn(cling::Interpreter& interp) {
  // We could use a header, too...
  interp.declare("int aGlobal;\n"
                 "float getAnotherGlobal();\n"
                 "void setAnotherGlobal(float val);\n");

  cling::Value res; // Will hold the result of the expression evaluation.
  interp.process("aGlobal;", &res);
  std::cout << "aGlobal is " << res.getAs<long long>() << '\n';
  interp.process("getAnotherGlobal();", &res);
  std::cout << "getAnotherGlobal() returned " << res.getAs<float>() << '\n';

  setAnotherGlobal(1.); // We modify the compiled value,
  interp.process("getAnotherGlobal();", &res); // does the interpreter see it?
  std::cout << "getAnotherGlobal() returned " << res.getAs<float>() << '\n';

  // We modify using the interpreter, now the binary sees the new value.
  interp.process("setAnotherGlobal(7.777); getAnotherGlobal();");
  std::cout << "getAnotherGlobal() returned " << getAnotherGlobal() << '\n';
}

/// Call an interpreted function using its symbol address.
void callInterpretedFn(cling::Interpreter& interp) {
  // Declare a function to the interpreter. Make it extern "C" to remove
  // mangling from the game.
  interp.declare("extern \"C\" int plutification(int siss, int sat) "
                 "{ return siss * sat; }");
  void* addr = interp.getAddressOfGlobal("plutification");
  using func_t = int(int, int);
  func_t* pFunc = cling::utils::VoidToFunctionPtr<func_t*>(addr);
  std::cout << "7 * 8 = " << pFunc(7, 8) << '\n';
}

/// Pass a pointer into cling as a string.
void modifyCompiledValue(cling::Interpreter& interp) {
  int loc = 17; // The value that will be modified

  // Update the value of loc by passing it to the interpreter.
  std::ostringstream sstr;
  // on Windows, to prefix the hexadecimal value of a pointer with '0x',
  // one need to write: std::hex << std::showbase << (size_t)pointer
  sstr << "int& ref = *(int*)" << std::hex << std::showbase << (size_t)&loc << ';';
  sstr << "ref = ref * ref;";
  interp.process(sstr.str());
  std::cout << "The square of 17 is " << loc << '\n';
}

int main(int argc, const char* const* argv) {
  // Create the Interpreter. LLVMDIR is provided as -D during compilation.
  cling::Interpreter interp(argc, argv, LLVMDIR);

  callCompiledFn(interp);
  callInterpretedFn(interp);
  modifyCompiledValue(interp);

  return 0;
}

输出

./cling-demo

aGlobal is 42
getAnotherGlobal() returned 3.141
getAnotherGlobal() returned 1
getAnotherGlobal() returned 7.777
7 * 8 = 56
The square of 17 is 289

跨越编译代码和解释代码的边界依赖于 Clang 对主机应用程序二进制接口 (ABI) 实现的稳定性。多年来,它一直非常可靠,适用于 Unix 和 Windows,但是,Cling 被大量用于与 GCC 编译的代码库交互,并且对 GCC 和 Clang 在 Itanium ABI 规范方面的 ABI 不兼容性很敏感。

扩展

就像 Clang 一样,Cling 可以通过插件进行扩展。下一个示例演示了 Cling 的自动微分扩展 Clad 的嵌入式使用。Clad 将 clang 的 AST 转换为生成数学函数的导数和梯度。在创建 Cling 实例时,我们指定 -fplugin 和插件本身的路径。然后我们定义一个目标函数 pow2,并要求它相对于它的第一个参数的导数。

#include <cling/Interpreter/Interpreter.h>
#include <cling/Interpreter/Value.h>

// Derivatives as a service.

void gimme_pow2dx(cling::Interpreter &interp) {
  // Definitions of declarations injected also into cling.
  interp.declare("double pow2(double x) { return x*x; }");
  interp.declare("#include <clad/Differentiator/Differentiator.h>");
  interp.declare("auto dfdx = clad::differentiate(pow2, 0);");

  cling::Value res; // Will hold the evaluation result.
  interp.process("dfdx.getFunctionPtr();", &res);

  using func_t = double(double);
  func_t* pFunc = res.getAs<func_t*>();
  printf("dfdx at 1 = %f\n", pFunc(1));
}

int main(int argc, const char* const* argv) {
 std::vector<const char*> argvExt(argv, argv+argc);
  argvExt.push_back("-fplugin=etc/cling/plugins/lib/clad.dylib");
  // Create cling. LLVMDIR is provided as -D during compilation.
  cling::Interpreter interp(argvExt.size(), &argvExt[0], LLVMDIR);
  gimme_pow2dx(interp);
  return 0;
}

输出

./clad-demo
dfdx at 1 = 2.000000

结论

我们已经展示了 Cling 在按需模板实例化、将解释器集成到第三方代码以及促进解释器扩展方面的能力。嵌入式解释器中的延迟模板实例化提供了一种非常适合与 C++ 互操作的服务。用自动微分等特定领域的功能扩展此类服务可以成为各种科学案例和其他更广泛社区的关键推动力量。

致谢

作者感谢 Sylvain Corlay、Simeon Ehrig、David Lange、Chris Lattner、Javier Lopez Gomez、Wim Lavrijsen、Axel Naumann、Alexander Penev、Xavier Valls Pla、Richard Smith、Martin Vassilev、Ioana Ifrim 对本文的贡献。

您可以在 https://root.cern/cling/https://compiler-research.org 了解有关我们活动的更多信息。