深入 Swift Runtime 之 TrailingObjects
Swift Runtime 底层结构和行为大量使用 C++ 开发,因此如果想要深入分析 Runtime 的原理或者 ABI 的内存布局等就需要对这部分 C++ 代码有所了解。Swift 的 C++ 源码中使用了很多“黑魔法”,尤其是模板,可能对 C++ 新手来说会比较劝退。本文就带大家深入浅出地分析其源码中的一个模板结构 TrailingObjects
,揭开 Swift Runtime 的冰山一角。
简单聊聊 Swift ABI
据 Apple 称,自 Swift 5.0 起,其 ABI 在 Apple 平台上就稳定了,这意味着 Swift 5.0 的编译产物在内存布局、runtime 调用上就不会发生变化了,也意味着你可以更换任意一个 ABI 兼容的标准库(libSwiftCore.dylib
)。因此在新的 iOS 版本中,app 就可以不将额外的 Swift Runtime 内嵌到 bundle 中了,从而节省了一部分包体积。
ABI 的稳定也意味着我们可以直接更换编译好的可执行二进制产物的 Swift Runtime,并通过自己编译的 runtime 来调试其内部数据结构和行为。亦或者直接将 Swift Runtime 的头文件引入自己的工程中来调用(毕竟内存布局是一样的,所有的结构可以完全匹配上)。
如何调试 Runtime
自己编译好 Swift 工具链后可以得到可调试的 libSwiftCore.dylib
, 但直接 link 它的话在运行时还是会加在系统中的动态库,因为产物默认的 LC_ID_DYLIB
是系统路径,这里为了方便我们可以直接通过 install_name_tool -change
更换可执行的 LC_LOAD_DYLIB
command,让它加载我们的 runtime。
更改好后我们在 runtime 代码里打一个断点,发现调试器可以成功断在里面并且可以随意访问变量和步进执行。
认识第一个 Runtime 结构
Swift Runtime 中的数据结构很多,有运行时产生的,也有编译链接时产生的。我们这里来看一个编译时产生的重要结构:Metadata
。它表示一个类型的元数据,runtime 的方方面面都依赖这个信息。在 Swift 中的表示为 T.Type
,即 T.self
的返回值类型。我们可以通过反编译下面这个简单的代码片段来看一下它在运行时是怎么获取的:
func takeType<T>(_ t: T.Type) { }
takeType(Foo.self) // Line to decompile
反编译结果如下:
可以看到 takeType 的第一个参数为一个指针,指向了 _$s4test3FooVMf
这个符号加偏移量 8 的地址。这个符号 demangle 之后的符号名为 full type metadata for test.Foo
,内容如下:
其 + 8 后的地址为另一个符号,但内容 hopper 无法解析。
之前我们说过,Metadata
结构对应的 Swift 类型为 T.Type
,那么这个 _$s4test3FooVMf + 8
的地址必然存放了与 Metadata
布局相同的内容。
我们来看一下 Metadata
在 C++ 代码中的定义(已简化,完整代码链接也贴在了文章最后):
template <typename Runtime> struct TargetMetadata;
using Metadata = TargetMetadata<InProcess>;
template <typename Runtime>
struct TargetMetadata {
using StoredPointer = typename Runtime::StoredPointer;
/// The basic header type.
typedef TargetTypeMetadataHeader<Runtime> HeaderType;
constexpr TargetMetadata()
: Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}
constexpr TargetMetadata(MetadataKind Kind)
: Kind(static_cast<StoredPointer>(Kind)) {}
private:
/// The kind. Only valid for non-class metadata; getKind() must be used to get
/// the kind value.
StoredPointer Kind;
public:
/// Get the metadata kind.
MetadataKind getKind() const {
return getEnumeratedMetadataKind(Kind);
}
/// Set the metadata kind.
void setKind(MetadataKind kind) {
Kind = static_cast<StoredPointer>(kind);
}
};
通过类型的定义可以看到,这个 Metadata
是 TargetMetadata<InProcess>
的 type-alias,为了方便我们后续都用 Metadata
代指后者。其存储字段只有一个 StoredPointer
类型的 Kind
变量。StoredPointer
定义在 Runtime 泛型中,代入 InProcess
可以看到定义(已简化):
struct InProcess {
static constexpr size_t PointerSize = sizeof(uintptr_t);
using StoredPointer = uintptr_t;
using StoredSignedPointer = uintptr_t;
using StoredSize = size_t;
using StoredPointerDifference = ptrdiff_t;
};
我们再来看下 MetadataKind 的定义:
const unsigned MetadataKindIsNonHeap = 0x200;
enum class MetadataKind : uint32_t {
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end) \
name##_Start = start, name##_End = end,
// content of: #include "MetadataKind.def"
#ifndef NOMINALTYPEMETADATAKIND
#define NOMINALTYPEMETADATAKIND(Name, Value) METADATAKIND(Name, Value)
#endif
/// A class type.
NOMINALTYPEMETADATAKIND(Class, 0)
/// A struct type.
NOMINALTYPEMETADATAKIND(Struct, 0 | MetadataKindIsNonHeap)
/// An enum type.
/// If we add reference enums, that needs to go here.
NOMINALTYPEMETADATAKIND(Enum, 1 | MetadataKindIsNonHeap)
// ...
};
刚才反编译看到的那段未知数据在内存布局上就与这个 Kind
对应,按值匹配即为 Struct
。到这里大家可能就会好奇了,为什么 _$s4test3FooVMf + 8
才是 Metadata
呢,那 _$s4test3FooVMf
对应的是什么呢?这里先不展开,相信大家按照本文的思路也能自己找到它的定义。
Metadata 子类?
到这里我们已经正式将一个类型横跨 Swift - 二进制 - C++ 进行了对接,接下来我们就来继续分析其他的结构。通过 Metadata
的定义我们应该能猜到,它不可能承载所有的信息,那么不同类型的详细信息都在哪里呢?这就要引出它的“子类”们了。以本文分析的 struct 为例,其真实 Metadata
类型是 StructMetadata
(aka. TargetStructMetadata<InProcess>
),结构定义及类型层次如下:
template <typename Runtime>
struct TargetValueMetadata : public TargetMetadata<Runtime> {
using StoredPointer = typename Runtime::StoredPointer;
TargetValueMetadata(MetadataKind Kind,
const TargetTypeContextDescriptor<Runtime> *description)
: TargetMetadata<Runtime>(Kind), Description(description) {}
/// An out-of-line description of the type.
TargetSignedPointer<Runtime, const TargetValueTypeDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
};
template <typename Runtime>
struct TargetStructMetadata : public TargetValueMetadata<Runtime> {
using StoredPointer = typename Runtime::StoredPointer;
using TargetValueMetadata<Runtime>::TargetValueMetadata;
const TargetStructDescriptor<Runtime> *getDescription() const {
return llvm::cast<TargetStructDescriptor<Runtime>>(this->Description);
}
/// Get a pointer to the field offset vector, if present, or null.
const uint32_t *getFieldOffsets() const {
auto offset = getDescription()->FieldOffsetVectorOffset;
if (offset == 0)
return nullptr;
auto asWords = reinterpret_cast<const void * const*>(this);
return reinterpret_cast<const uint32_t *>(asWords + offset);
}
};
可以看到 StructMetadata
本质上与 ValueMetadata
内存布局一致,仅当类型不同时有不同的运行时行为。ValueMetadata
在公共的 Kind
字段下又增加了 Description
字段,通过类型分析也是一个指针。这个字段指向了一个外联的描述结构,这个结构也是本文要重点分析的。
TargetStructDescriptor
先看定义:
template <typename Runtime>
class TargetStructDescriptor final
: public TargetValueTypeDescriptor<Runtime>,
public TrailingGenericContextObjects<TargetStructDescriptor<Runtime>,
TargetTypeGenericContextDescriptorHeader,
/*additional trailing objects*/
TargetForeignMetadataInitialization<Runtime>,
TargetSingletonMetadataInitialization<Runtime>,
TargetCanonicalSpecializedMetadatasListCount<Runtime>,
TargetCanonicalSpecializedMetadatasListEntry<Runtime>,
TargetCanonicalSpecializedMetadatasCachingOnceToken<Runtime>> {
// ...
};
说实话,刚开始看到这个类型我也懵了一下,但实际并没有看上去那么复杂。我们来一点一点分析。
首先第一个父类 TargetValueTypeDescriptor
,它其实是一个很简单的类型,提供了一些公共字段和行为,继承关系如下:
TargetValueTypeDescriptor
`-- TargetTypeContextDescriptor
`-- TargetContextDescriptor
比较麻烦的是它的第二个父类 TrailingGenericContextObjects<&@#?%^@*>
,这个类型的模板参数很多且雷同,一看就是个 variadic template,我们还是直接看定义:
template<class Self,
template <typename> class TargetGenericContextHeaderType =
TargetGenericContextDescriptorHeader,
typename... FollowingTrailingObjects>
class TrailingGenericContextObjects;
template<class Runtime,
template <typename> class TargetSelf,
template <typename> class TargetGenericContextHeaderType,
typename... FollowingTrailingObjects>
class TrailingGenericContextObjects<TargetSelf<Runtime>,
TargetGenericContextHeaderType,
FollowingTrailingObjects...> :
protected swift::ABI::TrailingObjects<TargetSelf<Runtime>,
TargetGenericContextHeaderType<Runtime>,
GenericParamDescriptor,
TargetGenericRequirementDescriptor<Runtime>,
FollowingTrailingObjects...>
{ /* ... */ };
通过定义可知,TrailingGenericContextObjects
在使用时有两个定参(Self
、TargetGenericContextHeaderType
)和一个不定参模板参数(FollowingTrailingObjects...
),下面有一个偏特化定义,主要作用是萃取 Runtime
参数以便后用。这个偏特化的定义直接继承了 TrailingObjects
,即我们接下来要重点分析的类型,实现如下:
template <typename BaseTy, typename... TrailingTys>
class TrailingObjects : private trailing_objects_internal::TrailingObjectsImpl<
trailing_objects_internal::AlignmentCalcHelper<
TrailingTys...>::Alignment,
BaseTy, TrailingObjects<BaseTy, TrailingTys...>,
BaseTy, TrailingTys...> {
// ...
private:
// These two methods are the base of the recursion for this method.
static const BaseTy *
getTrailingObjectsImpl(const BaseTy *Obj,
TrailingObjectsBase::OverloadToken<BaseTy>) {
return Obj;
}
static BaseTy *
getTrailingObjectsImpl(BaseTy *Obj,
TrailingObjectsBase::OverloadToken<BaseTy>) {
return Obj;
}
// ...
public:
/// Returns a pointer to the trailing object array of the given type
/// (which must be one of those specified in the class template). The
/// array may have zero or more elements in it.
template <typename T> const T *getTrailingObjects() const {
// Forwards to an impl function with overloads, since member
// function templates can't be specialized.
return this->getTrailingObjectsImpl(
static_cast<const BaseTy *>(this),
TrailingObjectsBase::OverloadToken<T>());
}
// ...
};
通过模板参数的声明我们可以大致猜想到,这个类型除了第一个模板参数(BaseTy
),其他不定参模板参数的角色都是相同的,对应到 TrailingGenericContextObjects
类型上,除了第一个 TargetSelf<Runtime>
参数,其他参数 TrailingObjects
并不实际感知。那么就可以得出结论,TrailingGenericContextObjects
实际上就是为它的参数追加上一部分额外参数(与 Swift 泛型相关),传递给 TrailingObjects
使用,充当了一层装饰器。这里我们可以先忽略它的作用。
TrailingObjects
接下来我们继续分析 TrailingObjects
。
通过前面的代码,我们可以看到这个类型定义了一些方法,但没有任何存储字段,并且可以看到它对接受的模板参数进行了一些类型计算,又传给了父类作为模板参数。其父类是 TrailingObjects
的核心实现,我们也一块来看一下定义:
template <int Align, typename BaseTy, typename TopTrailingObj, typename PrevTy,
typename... MoreTys>
class TrailingObjectsImpl {
// The main template definition is never used -- the two
// specializations cover all possibilities.
};
template <int Align, typename BaseTy, typename TopTrailingObj, typename PrevTy,
typename NextTy, typename... MoreTys>
class TrailingObjectsImpl<Align, BaseTy, TopTrailingObj, PrevTy, NextTy,
MoreTys...>
: public TrailingObjectsImpl<Align, BaseTy, TopTrailingObj, NextTy,
MoreTys...> {
// ...
};
/*
trailing_objects_internal::TrailingObjectsImpl<
trailing_objects_internal::AlignmentCalcHelper<TrailingTys...>::Alignment,
BaseTy,
TrailingObjects<BaseTy, TrailingTys...>,
BaseTy,
TrailingTys...
>
*/
这个类型继承了它自己,因此这是一个递归模板,是处理不定参模板的一种手段。在这种模式下,每一层类型都可以获取到参数列表中的一项,然后把剩下的参数传给父类,那么它的父类就可以处理下一项,然后继续把剩下的参数传给父类...
很像 Lisp 和 Haskell 等 FP 语言处理列表的方式,有兴趣的同学可以看看这篇文章:Learn You a Haskell for Great Good! - Recursion
但是自己继承自己如果没有终止条件就会死循环,因此一定存在一个(偏)特化用于终止递归:
// The base case of the TrailingObjectsImpl inheritance recursion,
// when there's no more trailing types.
template <int Align, typename BaseTy, typename TopTrailingObj, typename PrevTy>
class TrailingObjectsImpl<Align, BaseTy, TopTrailingObj, PrevTy>
: public TrailingObjectsAligner<Align> {
// ...
};
/*
matching TrailingObjectsImpl<Align, BaseTy, TopTrailingObj, NextTy, MoreTys...>
with MoreTys = []
*/
整个模板递归展开的过程大家可以使用 cppinsights 这个工具来查看,我这里已经做好了一个可编译的版本可以直接打开:https://cppinsights.io/s/8de0f488
接下来我们来看递归的每层类型为最终的类型添加了什么内容:
class TrailingObjectsImpl<Align, BaseTy, TopTrailingObj, /* 顶层完整类型 */
PrevTy, /* 上一层处理的模板参数 */
NextTy, /* 当前层处理的模板参数 */
MoreTys... /* 下一层处理的模板参数列表 */> /* : ... */ {
protected:
static const NextTy *
getTrailingObjectsImpl(const BaseTy *Obj,
TrailingObjectsBase::OverloadToken<NextTy>) {
auto *Ptr = TopTrailingObj::getTrailingObjectsImpl(
Obj, TrailingObjectsBase::OverloadToken<PrevTy>()) +
TopTrailingObj::callNumTrailingObjects(
Obj, TrailingObjectsBase::OverloadToken<PrevTy>());
if (requiresRealignment())
return reinterpret_cast<const NextTy *>(
llvm::alignAddr(Ptr, llvm::Align(alignof(NextTy))));
else
return reinterpret_cast<const NextTy *>(Ptr);
}
};
可以看到每一层都会有一个 getTrailingObjectsImpl
方法的重载,这个就是 TrailingObjectsImpl
最核心的方法,可以看到它能够返回当前层所处理的类型。同时他接受一个比较特殊的参数 OverloadToken
,之所以存在这个参数是因为 C++ 不支持仅返回值不同的函数重载,也不支持模板函数的(偏)特化,因此这个参数是用来帮助编译器确定我们需要的重载版本的,函数体内并不需要关心其内容。其他语言中也有类似的技术,比如 Rust 的 PhantomData。
继续来看函数的实现,它调用了 TopTrailingObj
的 getTrailingObjectsImpl
方法并传入了 PrevTy
构造的 OverloadToken
。这里 TopTrailingObj
表示一个完整类型,可以像外界一样获取到整个类型层次中的所有成员。很明显这里其实也是一个递归调用,PrevTy
向前追溯是 BaseTy
,所以我们看看返回 BaseTy
的重载版本,它位于 TrailingObjects
这个类型:
static const BaseTy *
getTrailingObjectsImpl(const BaseTy *Obj,
TrailingObjectsBase::OverloadToken<BaseTy>) {
return Obj;
}
callNumTrailingObjects
则是直接调用到 TopTrailingObj
中的实现了,因为每层类型中并没有增加重载,它的实现如下:
template <typename T>
static size_t callNumTrailingObjects(const BaseTy *Obj,
TrailingObjectsBase::OverloadToken<T>) {
return Obj->numTrailingObjects(TrailingObjectsBase::OverloadToken<T>());
}
这里是用到了 BaseTy
这个参数,在 TargetStructDescriptor
的使用场景中 BaseTy
为 TargetStructDescriptor<Runtime>
,而 TargetStructDescriptor
继承自 TrailingObjects
,这种手法叫 CRTP,即父类通过模板参数感知子类,从而实现编译期多态。在这个例子中,callNumTrailingObjects
调用到 TargetStructDescriptor
中的 numTrailingObjects
实现,实现不同类型的自定义行为:
class TargetStructDescriptor {
// ...
size_t numTrailingObjects(OverloadToken<ForeignMetadataInitialization>) const{
return this->hasForeignMetadataInitialization() ? 1 : 0;
}
size_t numTrailingObjects(OverloadToken<SingletonMetadataInitialization>) const{
return this->hasSingletonMetadataInitialization() ? 1 : 0;
}
size_t numTrailingObjects(OverloadToken<MetadataListCount>) const {
return this->hasCanonicicalMetadataPrespecializations() ?
1
: 0;
}
size_t numTrailingObjects(OverloadToken<MetadataListEntry>) const {
return this->hasCanonicicalMetadataPrespecializations() ?
this->template getTrailingObjects<MetadataListCount>()->count
: 0;
}
// ...
};
用法分析 & 小结
到这里我们就可以分析一下 TrailingObjects
的作用了。首先整个 TrailingObjects
继承链中没有任何存储字段,因此当一个类型继承它是不会更改内存布局。然后来看一下 getTrailingObjects
方法的逻辑,假设对于 TrailingObjects<Base, A, B>
调用 getTrailingObjects<B>()
,其调用过程为:
getTrailingObjects<B>()
= getTrailingObjectsImpl<B>()
= getTrailingObjectsImpl(A) + callNumTrailingObjects(A)
= (A*)(getTrailingObjectsImpl(Base) + callNumTrailingObjects(Base)) + callNumTrailingObjects(A)
可以得出 TrailingObjects
作用的结构内存布局为:
+------+------------+------------+
| Base | A * num(A) | B * num(B) |
+------+------------+------------+
这与 Swift ABI 约定的 Metadata 在二进制中的布局一致,通过 TrailingObjects
我们可以自由地声明类型的内存映射,在顺序和数量上的调整更加灵活。相比于简单的指针偏移计算,它能够提供更强的类型安全性,并且能够复用大部分计算逻辑,减少代码错误。
当然这种结构也有一些不大不小的缺点,比如会让代码看起来更晦涩,不太容易看出类型的真实结构;另一方面,受制于 C++ 编译器的优化能力,模板 + 非尾递归的代码不能被优化为简单的表达式计算(尽管它在运行时做的确实就是一个简单的表达式计算),在运行效率上可能有一定开销,至少方法调用是真实存在的:
总结
本文通过对 TrailingObjects
的分析,用具体例子介绍了自己在 Swift Runtime 的调试和源码阅读上的思路。相比 Objective-C Runtime,Swift Runtime 在复杂度和抽象程度上会更上一层,不过好在调试相对更容易。
希望本文对大家可以起到抛砖引玉的作用。
相关链接
* 如果文章有任何问题,欢迎提交 Issues,也可以通过 Twitter 或邮箱联系我。