C++ ABI 提供了什么

前面关于 type_info 的讨论我们认识到一个语言的实现可以有多种可能,这个语言的标准往往只把一些公有的 interface 展现给这个语言的程序员。但实际上,很多时候我们可能需要更加底层的信息来帮助我们完成或者理解这个语言的实现,它往往在这个 ABI 层次上提供了相关的文档。我们这里参看这个文档。这里选择了一些有意思的东西

数据布局

成员指针(member pointer)实际的表示是 ptrdiff_t(指针偏移,我们在计算 any_ptr 的转换的时候也使用了这个类型),因此它的空一般使用 -1。而成员函数指针(pointer to member function)是两部分组成的,ptr 部分是函数指针(对于 virtual function 来说是 1+ vtab offset,也是使用 ptrdiff_t 表示的),使用 0 表示空,adj 表示相对于 this 的偏移(ptrdiff_t)。之所以需要 adj 这个部分也是为了支持多继承。

使用 new 产生一个 array 的时候需要存放这个 array 的大小(除非 T 具有 trivial 的 destructor 或者使用的是 ::operator new[](size_t, void*)),这称为 cookie,这一般存放在申请的内存最开始。

每个 function scope 的 static variable 存在所谓的 initialization guard variable,这就是为什么可以通过 lambda 初始化 static variable 同时能够处理多线程的情况的原因。这占用 64 bits,其中第一个字节表示初始化的状态。

在 vtab 里面会存放 type_info 对象的指针,通常是嵌入在对象的 virtual pointer 的值 -1;而在 -2 位置存放的是 virtual pointer 相对于这个类型的对象的偏移量。这主要是应对 virtual base 的情形,这个时候对象可能存在多个 virtual pointer,而这个偏移量对应的自然是 primary vtab。

函数调用

pass-by-value 对于 non-trivial copy constructor 对象来说实际的实现其实是通过 copy constructor 生成一个临时对象,然后 pass by reference。而 pass-by-reference 实际上是 pass-by-pointer 而在语言层面上翻译成为 reference。返回对象的过程一般是 caller 分配存放该对象的空间,然后将 this 指针传递给 callee(作为第一个隐式传递的参数),而 callee 创建这个对象。实际在实现这个函数的时候,我们其实无法看见这个指针,因此在函数里会有另一个变量 hold 这个返回值,在返回这个对象的时候发生 copy constructor/move constructor。

这个地方有意思的大概是 Google 的 style guide 要求把返回值尽量使用 non-const pointer 传递到函数里,其实是显式的要求程序员做和编译器一样的事。

构造函数其实返回值永远是 void 所以不需要写出来。

编译单元可以使用 #pragma priority 将目标文件中注入一个 struct 描述其构造顺序,通过 __cxa_priority_init 在运行时访问。这些初始化后的静态变量将在 __cxa_atexit 和 __cxa_finalize 时(后者调用前者)释放,用户通过 atexit 实际上也是注册 __cxa_atexit。

异常处理

这个分为两种:

  • throw/catch 类型,抛出点并不知道哪里会处理,这对应 __Unwind_RaiseException
  • longjmp 类型,跳出时其实是知道跳到哪里,使用 __Unwind_ForcedUnwind

在 unwind 过程里面,通常会有两个 phase,每个 phase 都会调用对应的 personality routine,但是传递的参数对应的是 phase,其行为也不一样

  • search phase,从栈顶开始一个 frame 一个 frame 的寻找能够接受 exception 的 frame,如果一直都没有,最终会调用 terminate
  • cleanup phase,一样的顺序,这个过程中调用 cleanup code 释放每个 frame 里面需要处理的对象

异常本身是 language 与 implementation dependent 的,但是通常会存放一个 header,这通过 abi layer 的函数使用。

Advertisements
C++ ABI 提供了什么

编译器插件(1)

一个语言的编译器往往都支持所谓的 plugin,它们存在的理由大概是编译器本身是一个非常复杂的东西,如果只能借助它完成语言的翻译(一种语言到另一种语言),有点大材没有尽用。因此通过 plugin 可以为高端用户提供一次搭顺风车的机会。我们这里简单的看三个例子,算是初来练手了。

这里我们先看 protobuf 的 plugin。作为一个定义数据类型的语言,它可以为多种语言生成数据在该语言中的类型、容器,并提供对应 serialize/deserialize 的代码。那么在处理 protobuf 的定义过程中,我们怎么建立一个 plugin 呢?

一个简单的 python plugin 可以参看这里,最核心的部分就是将 CodeGenerationRequest 转换到 CodeGenerationRequest 对象的过程。这最终是一个 binary,通过 –plugin=protoc-gen-NAME 传递给 protoc,同时可以指定 –NAME_out 作为 plugin 的输出。比较有意思的是我们可以看到,在 traverse proto 文件的过程中我们实际是通过 proto 的 descriptor API 来判断处理的是什么类型的信息。这跟我们使用 reflection API 来操纵 proto 是非常类似的。

比较复杂的 plugin 我们可能还是希望使用 C/C++ 来实现,这部分一般通过 plugin.h 提供的 API 来完成。

试想如果我们希望为每个被标注过的 message 类型生成一些额外的 C++ 代码,比如将某个 proto message 关联到某个类,这样可以提供一些额外的 accessor 方便我们使用,那么我们大致如何处理这样一个事情呢?

  • 引入我们自己的 options,在 message 里面通过诸如 binding_class 指定一个需要生成的 C++ 类
  • 为我们生成的 binding class 提供一个公用的基类,这样可以减少一些重复性的代码,很显然这个会使用 CRTP
  • plugin 的实现其实挺直接,拿到 message 看有没有我们的 options,有的话就写 CodeGenerationResponse

又比如说,如果我们需要为 proto 定义的数据提供一个 query engine,某些数据其实可以通过 options 标注其表达(比如 uint64 可能是一个 timestamp 而通过 query 显示出来的时候可能就当个日期显示比较合理)。我们是否也可以通过 plugin 来 customize 这些 field 对应的 rendering 的代码呢?

通过这个例子我们可以清楚的看到一个 compiler 的 plugin 本身就是在 inspection 过程中嵌入的一些 user hook。对于更加复杂的 compiler/language,这个 plugin 获得的功能会更加广泛。

编译器插件(1)

从 type_info 判断子类(下)

那么在有多继承的情况下,我们需要更为复杂的 is_base_of,这时需要计算偏移量:更为复杂的就是如何更改指针的值。如果你不清楚为什么我们需要这个,那说明你对 C++ 的 object model 不是十分了解。这个版本不支持 virtual base

bool is_base_of(const std::type_info* a, const std::type_info* b, ptrdiff_t* offset) {
 if (*a == *b) return true;
 const auto* bb = dynamic_cast<const __cxxabiv1::__class_type_info*>(b);
 if (bb == nullptr) return false;
 const auto* si_bb = dynamic_cast<const __cxxabiv1::__si_class_type_info*>(bb);
 if (si_bb) {
   const auto* b_base = si_bb->__base_type;
   return is_base_of(a, b_base, offset);
 }
 const auto* vmi_bb = dynamic_cast<const __cxxabiv1::__vmi_class_type_info*>(bb);
 if (vmi_bb) {
   for (int i = 0; i < vmi_bb->__base_count; ++i) {
     const auto& b_base = vmi_class_tinfo->__base_info[i];
     *offset += b_base.__offset();
     auto result = is_base_of(a, b_base.__base_type, offset);
     if (!result) {
       *offset -= b_base.__offset();
       continue;
     }
     return result;
   }
 }
 return false;
}

有了这个我们可以修改 any_ptr,这里有几个地方值得斟酌:

  • 这里的 is_base_of 同时计算了指针的偏移量,我们可以把 void* reinterpret 成为 char* 偏移之后再 reinterpret 成为 void*,最后 static_cast 成为需要的类型
  • make_any_ptr 传递类型信息究竟应该选择 typeid(T) 还是 typeid(t),这其实是一个很 delicate 的事情
  • 对于之前的 boost.any 版本我们应该选择 typeid(T),即传递编译时类型(比如使用 parent ref 到一个 child 对象,对这个 parent ref 使用 make_any_ptr 对应的 T 为 parent 类型)。这是因为这个版本只能转换回编译时类型,这意味着如果不这样做 any_cast 到 parent 类型反而会 fail,这就让人觉得不能接受
  • 但是我们现在的 any_ptr 支持任意一个 base class pointer 的 casting,这意味着如果我们传递 typeid(t),即运行时类型,对于编译时类型以及所有其父类型都应该是可以成功的转换的。所以不存在之前的问题
  • 存在 virtual base 的时候 __offset() 返回的并不是 base 指针的偏移,而是这个 virtual base 对应的 virtual base offset 在 vtable 中的 offset,这个听起来很诡异:一个类可以存在多个 virtual base,每一个 virtual base 的访问都需要根据对应的 virtual base offset(加在对象的指针上);而 __offset() 返回的 offset 就是用来获得这个 virtual base offset 的,然而我们需要将这个 offset 与 vtable 指针相加,然后 deref 获得实际的偏移量。

从这个意义上来说,这个 type erasure 的实现完全借助了 std::type_info 本身。通过 fake_type_id 我们能实现一般性的 boost.any 但是如果需要这种程度的类型转换,我们需要依赖更底层的 cxxabi.h,这已经不是 C++ 标准的范畴,而是每个实现标准的 vendor 自己定义的一套东西了。这个东西里面其实也涉及了诸如异常处理、mangling 等实现的细节。

从 type_info 判断子类(下)