Cinder:基于 Swift Macro 与 Mach-O Section 的解耦注册机制

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@sectionembedded-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 核心功能由以下三部分组成:

  1. Swift 宏:在编译期生成统一格式的注册记录和函数适配器。
  2. Mach-O 自定义 section:保存所有注册记录。
  3. 运行时扫描器:遍历已加载的 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 内部会检查:

  1. 相同 key 的所有记录都是 multiple: true
  2. 所有记录的 kind 都符合调用 API
  3. 按 Mach-O image 和 section 中出现的顺序执行。这个顺序和编译次序相关

总结

Cinder 的实现并不复杂:利用宏在编译阶段生成注册记录,把它们写入 Mach-O,在运行时统一扫描和读取。它解决的不是如何根据字符串动态查找 Swift 方法,而是如何让互不依赖的组件声明和发现一组编译期已经确定的能力。

相比让每个模块在启动时主动调用 register(),这种方式省去了集中注册表,也减少了模块接入时需要修改的代码。新增一个服务、生命周期处理器或插件时,注册代码可以和实现放在一起,宿主只负责按约定的 key 查询和执行。

当然,Cinder 并不能替代所有依赖注入或路由方案。key 仍然是一种需要双方共同维护的契约;多值记录的顺序与编译、链接结果有关,业务不应该依赖它决定执行先后;它也更适合编译期已经确定的静态扩展点,而不是运行时动态加载和卸载的插件。

Swift 逐渐补齐 @used@section 之后,以前需要借助 Objective-C 或额外构建脚本完成的事情,现在可以用更贴近 Swift 的方式实现。Cinder 是基于这套能力做的一次尝试:把注册从启动阶段移到编译阶段,让模块发现和调用这件事尽量简单一些。

相关推荐
末代iOS程序员华仔2 小时前
从 Figma 到上架:使用 Codex 与 MCP 全流程实现 AI 聊天 App
flutter·ios·swift
一牛4 小时前
Swift 循环引用
macos·ios
迷雾悠悠19 小时前
企业微信iPad协议聚合聊天系统:从原理到实战开发指南
ios·企业微信·ipad
LDyun_1 天前
.2026主流云手机测评,8款机型高压挂机实测
ios·智能手机·安卓·玩游戏
pop_xiaoli1 天前
【iOS】YYModel源码学习
学习·ios·objective-c·cocoa
霍霍哈嗨1 天前
swiftUI框架基础
ios·swiftui·swift
末代iOS程序员华仔1 天前
iOS 开发到上架 App Store 全流程详解
ios·objective-c·swift
2501_916007471 天前
抓包工具对比 Charles、Fiddler、TraceEagle、Wireshark 与 Proxyman
前端·网络协议·ios·adb·https·fiddler·wireshark
LDyun_2 天前
2026暑期云手机选型指南:从稳定性、成本到风控六大核心评判维度
arm开发·ios·智能手机·安卓·玩游戏