Cinder(Github) 是一个无需逐模块启动注册、支持字符串、类型和函数调用的 Swift 编译期注册框架。
Cinder 是什么
在 iOS app 里借助 Mach-O 注册能力做一些二进制的插桩操作并不罕见。以往这些操作大都使用 objc runtime ,借助 __attribute__((used)) 和 __attribute__((section("name"))) 实现。在 Swift 上实现有一些难度,因为 Swift runtime 能力弱,以及早期的版本没有类似 objc 的 __attribute__ 能力。
- 在 Swift 5.10 版本,Apple 提供了
@_used和@_section的实验性版本,需要在 project 设置或者 Package.swift 里面加上SymbolLinkageMarkers开关才能使用。 - 从 Swift 6.3 开始,Apple 正式提供
@used和@section,embedded-swift-improvements-swift-6.3,弥补了一部分二进制插桩能力。
@section 要求编译器把变量放进指定的 Mach-O segment 和 section;@used 则告诉编译器与 linker,即使这个变量在普通代码里没有被引用,也必须生成并保留。
Cinder 利用 Swift 6.3 提供的 @used和 @section,结合 Swift Macro,提供一套 Mach-O 注册和读取能力。
借助 Cinder,你可以在某处代码向 Mach-O 中注册一个或多个字符串、方法、函数等,然后在别的地方把它们读出来进行使用或执行。「别的地方」可以是没有依赖关系的另一个组件。整体实现没有依赖 objc 的 runtime 能力,因此纯 Swift 类也可以正常注册和调用。
假设现在有 A、B 两个组件库,互不依赖。使用 Cinder 在 B 里面注册一个方法,然后在 A 里面就可以直接调用。
swift
// in ModuleB
import Cinder
final class ModuleB {
@CinderMethod("callModuleB")
class func callModuleB() {
print("ModuleB called!")
}
}
swift
// in ModuleA
import Cinder
public final class ModuleA {
func moduleACallModuleB() {
print("Module A will call Module B")
do {
try Cinder.shared.executeMethod(for: "callModuleB")
} catch {}
}
}
// 调用 moduleACallModuleB()
// 输出:
// Module A will call Module B
// ModuleB called!
Cinder 适合的场景
- 模块化 App 解耦。模块 A 可以通过 key 调用模块 B 暴露的能力,而不直接依赖或导入模块 B。
- App 生命周期事件分发。多个业务模块分别注册
AppLaunch、登录完成、进入后台等处理器,由宿主统一执行。 - 插件式架构。各组件自行注册服务类型、初始化动作或扩展点,宿主只负责发现和执行。新增模块时不需要修改一个中央注册表。
- 路由与配置映射。使用
#CinderString保存路由名、页面类名或模块标识。适合静态、编译期确定的映射关系。 - 减少启动注册开销。注册记录直接进入 Mach-O,不需要每个模块在启动时执行
register(),可以减少启动阶段的集中初始化问题。 - 减少解耦代码,降低包体积等
Cinder 能做的事情
注册字符串
swift
import Cinder
#CinderString("root.name", "HomeViewController")
调用方可以在其他任何地方读取:
swift
let rootName = try Cinder.shared.string(for: "root.name")
注册 Swift Type
swift
final class HomeService {}
final class ProfileService {}
#CinderType("services", HomeService.self, multiple: true)
#CinderType("services", ProfileService.self, multiple: true)
multiple 表示要注册的数据有很多个,共用一个 key。调用方可以读取所有注册的 service type:
swift
let serviceTypes = try Cinder.shared.types(for: "services")
注册全局 Action
swift
@CinderAction("application.initialize")
func initializeManager() {
Manager.shared.initialize()
}
上面的方法是 top level 的 global function,独立函数,不与某个类绑定。调用方只需要知道 key,就能调用对应的函数。
swift
try Cinder.shared.executeAction(for: "application.initialize")
注册实例方法和类型方法
swift
final class Dog {
@CinderMethod("dog.eat")
func eat() {
print("Dog is eating")
}
@CinderMethod("dog.prepare")
static func prepare() {
print("Preparing food")
}
}
运行时可以在任意地方调用实例方法或 static/class 方法:
swift
let dog = Dog()
try Cinder.shared.executeMethod(for: "dog.eat", on: dog)
try Cinder.shared.executeMethod(for: "dog.prepare")
Cinder 的实现原理
构成
Cinder 核心功能由以下三部分组成:
- Swift 宏:在编译期生成统一格式的注册记录和函数适配器。
- Mach-O 自定义 section:保存所有注册记录。
- 运行时扫描器:遍历已加载的 Mach-O image,解析、缓存并查询记录。
Cinder 不会在运行时根据字符串寻找 Swift 方法。所有最终执行的函数都在编译期生成,并随 App 一起链接和签名。
核心数据结构:固定字节的数据模型
不管是注册字符串、函数还是方法,Cinder 最终都会把它们统一编码成一个固定 512 字节的结构:
c
// Sources/CinderRuntime/include/CinderRuntime.h
typedef struct CinderSectionRecord {
uint32_t magic;
uint16_t version;
uint8_t kind;
uint8_t flags;
uint16_t keyLength;
uint16_t valueLength;
uint32_t reserved;
char key[128];
union {
char value[368];
struct {
uint8_t padding[368 - sizeof(uintptr_t)];
uintptr_t function;
} direct;
};
} CinderSectionRecord;
固定大小有几个好处:
- C 和 Swift 可以使用同一套 ABI 读取;
- 不需要在 section 内保存额外索引;
- 运行时可以用
sectionSize / 512直接计算记录数; - 任意一条记录都可以通过固定偏移访问 key、value 和函数指针。
Swift 宏把数据注册进 Mach-O
Cinder 宏展开后的核心代码都类似:
swift
@used
@section("__DATA,__cinder")
nonisolated(unsafe)
private static var record = /* 固定格式数据 */
这些变量会在编译时进入 __DATA,__cinder。Linker 随后把同一个 Mach-O image 中的记录合并到一起。
Cinder 暴露了四类宏:
swift
@freestanding(declaration)
public macro CinderString(
_ key: StaticString,
_ value: StaticString,
multiple: Bool = false
)
@freestanding(declaration)
public macro CinderType<T>(
_ key: StaticString,
_ type: T.Type,
multiple: Bool = false
)
@attached(peer)
public macro CinderAction(
_ key: StaticString,
multiple: Bool = false
)
@attached(peer)
public macro CinderMethod(
_ key: StaticString,
multiple: Bool = false
)
含义分别是:
#CinderString:注册一个字符串。#CinderType:注册一个 Swift 类型。@CinderAction:注册一个全局无参函数。@CinderMethod:注册一个 class 中的无参实例方法或 class/static 方法。
运行时查询接口与它们对应:
swift
try Cinder.shared.string(for: "key")
try Cinder.shared.strings(for: "key")
try Cinder.shared.type(for: "key")
try Cinder.shared.types(for: "key")
try Cinder.shared.executeAction(for: "key")
try Cinder.shared.executeMultipleActions(for: "key")
try Cinder.shared.executeMethod(for: "key", on: object)
try Cinder.shared.executeMethod(for: "key")
try Cinder.shared.executeMultipleMethods(for: "key", on: object)
try Cinder.shared.executeMultipleMethods(for: "key")
默认注册是 multiple = false,也就是同一个 key 只能有一条记录。如果你希望多个模块都能往同一个 key 下挂载能力,需要写 multiple: true,然后使用对应的 multiple API 来读取或执行。
宏展开后是什么样
以字符串注册为例:
swift
#CinderString("root.name", "HomeViewController")
宏会生成类似这样的代码:
swift
private func __cinder_registration(...) {
struct Storage {
@used
@section("__DATA,__cinder")
nonisolated(unsafe) private static var record: (UInt8, UInt8, ...) = (
// 512 bytes
)
}
}
逻辑上,这 512 字节表示:
text
magic = 0x444E4943
version = 1
kind = 0
flags = 0
keyLength = 9
valueLength = 18
key = "root.name"
value = "HomeViewController"
@used 用来告诉编译器不要把这个变量优化掉;@section("__DATA,__cinder") 则指定它进入 Mach-O 的自定义 section。
对于 action:
swift
@CinderAction("application.initialize")
func initializeManager() {
Manager.shared.initialize()
}
宏会生成一个 peer declaration:
swift
@used
@section("__DATA,__cinder")
nonisolated(unsafe) private var __cinder_record_initializeManager:
(..., @convention(c) () -> Void) = (
// header bytes
{ initializeManager() }
)
这里的重点是最后那个闭包。它是一个 @convention(c) () -> Void 的 adapter,Cinder 不需要通过字符串再查 Swift 函数名,而是可以直接把 section 里的尾部内容解释成函数指针并调用。
实例方法稍微复杂一点:
swift
final class Dog {
@CinderMethod("dog.eat")
func eat() {
print("Dog eating")
}
}
宏生成的 adapter 大致如下:
swift
@used
@section("__DATA,__cinder")
nonisolated(unsafe) private static var __cinder_record_eat_xxx:
(..., @convention(c) (UnsafeMutableRawPointer?) -> Int32) = (
// header bytes
{ opaqueTarget in
guard let opaqueTarget else { return 0 }
let object = Unmanaged<AnyObject>
.fromOpaque(opaqueTarget)
.takeUnretainedValue()
guard let target = object as? Dog else { return 0 }
target.eat()
return 1
}
)
宏不直接存"方法/函数名",而是为方法生成一个可直接调用的 C ABI adapter,再把 adapter 指针塞进 Mach-O section。 这样不用依赖 objc runtime 能力,就能支持纯 Swift 的函数、方法。
运行时执行:
swift
try Cinder.shared.executeMethod(for: "dog.eat", on: dog)
dog 会被转成 opaque pointer 传给 adapter。adapter 内部再检查它是不是 Dog,是就调用 eat(),不是就返回 0。外层看到返回 0 会抛 invalidMethodTarget。
class/static 方法则不需要传入对象:
swift
final class ServiceA {
@CinderMethod("AppLaunch", multiple: true)
class func doLaunchThings() {
print("ServiceA launch")
}
}
宏生成的 adapter 等价于:
swift
{ ServiceA.doLaunchThings() }
record 的 flags 会包含:
text
multiple | directFunction | classMethod
所以调用:
swift
try Cinder.shared.executeMultipleMethods(for: "AppLaunch")
就可以依次执行所有注册到 "AppLaunch" 下面的 class/static 方法。
运行时扫描 Mach-O image,获取注册数据
枚举 Mach-O image
编译和链接完成后,注册记录已经分散在各个 Mach-O image 的 __DATA,__cinder 中。运行时第一次查询时,Cinder 会调用 C Runtime 扫描这些 image。
c
uint32_t imageCount = _dyld_image_count();
for (uint32_t imageIndex = 0;
imageIndex < imageCount;
imageIndex++) {
const struct mach_header *header =
_dyld_get_image_header(imageIndex);
}
查找 __cinder section
对于每个 image,Cinder 使用:
c
uint8_t *bytes = getsectiondata(
header,
"__DATA",
"__cinder",
&byteCount
);
如果 section 存在,记录数量就是:
c
size_t recordCount = byteCount / 512;
随后按固定步长解析:
c
const CinderSectionRecord *record =
(const CinderSectionRecord *)(
bytes + recordIndex * 512
);
通过校验的记录再交给 Swift visitor。
解码数据
Swift 根据固定偏移读取 key 和 value:
swift
let keyBytes = rawRecord
.advanced(by: 16)
.assumingMemoryBound(to: UInt8.self)
let valueBytes = rawRecord
.advanced(by: 144)
.assumingMemoryBound(to: UInt8.self)
然后按长度进行 UTF-8 解码,并解析 kind、flags、函数地址和 image 路径,最终转换成内部模型:
swift
private struct CinderRecord {
let key: String
let value: String
let kind: CinderValueKind
let allowsMultiple: Bool
let isClassMethod: Bool
let function: UInt
let imagePath: String
}
支持 multiple 多值注册
默认情况下,一个 key 只应该拥有一条记录:
swift
#CinderString("root.name", "Home")
如果两个模块都使用相同单值 key,Cinder 会在查询时抛出:
swift
CinderError.duplicateKey("root.name")
对于 service、插件和扩展等需要注册多条值的情况,可以显式声明多值注册:
swift
@CinderAction("application.hooks", multiple: true)
func initializeAnalytics() {}
@CinderAction("application.hooks", multiple: true)
func initializeAccount() {}
调用方使用对应的 multiple API:
swift
try Cinder.shared.executeMultipleActions(
for: "application.hooks"
)
Cinder 内部会检查:
- 相同 key 的所有记录都是
multiple: true - 所有记录的 kind 都符合调用 API
- 按 Mach-O image 和 section 中出现的顺序执行。这个顺序和编译次序相关
总结
Cinder 的实现并不复杂:利用宏在编译阶段生成注册记录,把它们写入 Mach-O,在运行时统一扫描和读取。它解决的不是如何根据字符串动态查找 Swift 方法,而是如何让互不依赖的组件声明和发现一组编译期已经确定的能力。
相比让每个模块在启动时主动调用 register(),这种方式省去了集中注册表,也减少了模块接入时需要修改的代码。新增一个服务、生命周期处理器或插件时,注册代码可以和实现放在一起,宿主只负责按约定的 key 查询和执行。
当然,Cinder 并不能替代所有依赖注入或路由方案。key 仍然是一种需要双方共同维护的契约;多值记录的顺序与编译、链接结果有关,业务不应该依赖它决定执行先后;它也更适合编译期已经确定的静态扩展点,而不是运行时动态加载和卸载的插件。
Swift 逐渐补齐 @used 和 @section 之后,以前需要借助 Objective-C 或额外构建脚本完成的事情,现在可以用更贴近 Swift 的方式实现。Cinder 是基于这套能力做的一次尝试:把注册从启动阶段移到编译阶段,让模块发现和调用这件事尽量简单一些。