【GUI】入门 Noise(二):深度解析:Racket 嵌入 Swift 应用的底层机制
引言
Noise 是一个 Swift 包装器,用于简化在 Swift 应用中嵌入 Racket CS 运行时的复杂过程。乍一看,它似乎只是简单地"包装了一个 Racket 库",但实际上,整个集成过程涉及多个层次和组件,理解其工作原理需要深入分析编译和运行时两个阶段。
本文将深入探讨 Noise 的核心架构,解答以下关键问题:
- Racket CS 的最小运行时包含哪些文件?
- 这些文件如何参与 Swift 的编译过程?
- 谁来加载 boot 文件,何时加载?
- 编译顺序和依赖关系是什么?
- 头文件被谁使用?
- noise-serde-lib 的真实角色是什么?
架构概览:三层分离的设计
Noise 采用三层分离的架构设计:
┌─────────────────────────────────────────┐
│ 你的 Swift 应用 │
└────────────┬────────────────────────────┘
│
┌────────────▼────────────────────────────┐
│ Noise 框架 (Swift 代码) │
│ - Racket.swift (运行时封装) │
│ - Serde.swift (序列化) │
│ - Backend.swift (RPC) │
└────────────┬────────────────────────────┘
│
┌────────────▼────────────────────────────┐
│ RacketCS.xcframework (静态库) │
│ - libracketcs.a (Racket VM) │
│ - racket.h, racketcs.h (头文件) │
└─────────────────────────────────────────┘
核心理解:每一层都有独立的职责,彼此之间通过明确定义的接口通信。
一、Racket CS 的最小运行时包含哪些文件?
Noise 需要从 Racket 中提取三类文件:
1. 静态库文件 (.a)
每个平台对应的 Racket CS 编译产物:
Lib/
├── libracketcs-arm64-ios.a # iOS 设备架构
├── libracketcs-arm64-iphonesimulator.a # iOS 模拟器架构
├── libracketcs-arm64-macos.a # macOS ARM64 架构
└── libracketcs-x86_64-macos.a # macOS Intel 架构
这些静态库被封装成 .xcframework:
RacketCS-macos.xcframework: Lib/include/* Lib/libracketcs-universal-macos.a
xcodebuild -create-xcframework \
-library Lib/libracketcs-universal-macos.a \
-headers Lib/include \
-output $@
静态库包含:Chez Scheme 虚拟机、Racket CS 核心运行时、C API 函数实现
2. 头文件 (.h)
Lib/include/
├── racket.h # 主入口,根据平台包含对应的 chezscheme.h
├── racketcs.h # Racket CS API 定义
├── racketcsboot.h # 启动参数结构定义
├── chezscheme-arm64-macos.h # macOS ARM64 的 Chez Scheme 接口
├── chezscheme-x86_64-macos.h # macOS Intel 的 Chez Scheme 接口
├── chezscheme-arm64-ios.h # iOS 设备的 Chez Scheme 接口
└── chezscheme-arm64-iphonesimulator.h # iOS 模拟器的 Chez Scheme 接口
头文件包含:C 函数声明、数据结构定义、宏定义
3. Boot 文件
Sources/NoiseBoot_iOS/boot/arm64-ios/
├── petite.boot # Petite Scheme 基础镜像
├── scheme.boot # 完整的 Scheme 镜像
└── racket.boot # Racket 语言扩展镜像
Sources/NoiseBoot_macOS/boot/arm64-macos/
├── petite.boot
├── scheme.boot
└── racket.boot
Boot 文件包含:编译后的字节码、初始数据结构、核心库代码
二、编译流程详解
阶段 1:Racket 源码 → 静态库
这是在构建 Racket CS 时完成的,与 Noise 无关:
Racket 源码 (C/Scheme)
↓ 配置和编译 (configure & make)
libracketcs.a (静态库)
↓ xcodebuild 打包
RacketCS.xcframework (binary target)
阶段 2:Swift 项目的编译
看 Package.swift 的定义:
.binaryTarget(
name: "RacketCS-ios",
path: "RacketCS-ios.xcframework"
),
.binaryTarget(
name: "RacketCS-macos",
path: "RacketCS-macos.xcframework"
),
.target(
name: "Noise",
dependencies: [
.target(name: "NoiseBoot_iOS", condition: .when(platforms: [.iOS])),
.target(name: "NoiseBoot_macOS", condition: .when(platforms: [.macOS])),
.target(name: "RacketCS-ios", condition: .when(platforms: [.iOS])),
.target(name: "RacketCS-macos", condition: .when(platforms: [.macOS])),
],
linkerSettings: [
.linkedLibrary("curses", .when(platforms: [.macOS])),
.linkedLibrary("iconv"),
]
)
编译顺序:
-
预编译阶段(已存在于仓库中):
RacketCS-ios.xcframeworkRacketCS-macos.xcframework
-
编译阶段(Swift 编译器处理):
NoiseBoot_iOS (只有资源) NoiseBoot_macOS (只有资源) Noise (Swift 代码,依赖上述) NoiseBackend (Swift 代码,依赖 Noise) NoiseSerde (Swift 代码) 你的应用 (依赖上述所有) -
链接阶段:
- Swift 调用 Clang 模块导入器
- 读取
RacketCS.xcframework中的头文件 - 链接静态库中的符号
关键点:import RacketCS 在 Racket.swift 并不是导入一个 Swift 模块,而是告诉 Swift Package Manager 链接对应的 binaryTarget。
三、头文件被谁使用?如何使用?
当你编写 import RacketCS 时,Swift 编译器会:
- 找到对应的 binaryTarget(
RacketCS-macos或RacketCS-ios) - 读取头文件:从
xcframework的Headers/目录 - 解析头文件:使用 Clang 的模块映射功能
头文件的组织
racket.h 是主入口(见 racket.h):
#if defined(__x86_64__)
# include "chezscheme-x86_64-macos.h"
#elif defined(__arm64__)
# include "TargetConditionals.h"
# if TARGET_OS_IPHONE
# define _Nonnull
# define _Nullable
# include "chezscheme-arm64-ios.h"
# else
# include "chezscheme-arm64-macos.h"
# endif
#endif
#include "racketcs.h"
根据编译目标自动选择正确的平台头文件。
Swift 如何调用 C 函数
Swift 编译器通过头文件了解 C 函数的签名,例如 racketcs.h 中的定义(racketcs.h):
RACKET_API_EXTERN ptr racket_apply(ptr proc, ptr arg_list);
RACKET_API_EXTERN ptr racket_primitive(const char *name);
RACKET_API_EXTERN void racket_embedded_load_file(const char *path, int as_predefined);
Swift 会自动将这些函数映射为 Swift 函数调用,可以直接在 Swift 代码中使用。
总结:头文件是 Swift 和 C 之间的"语言桥梁",定义了双方都能理解的接口。
四、谁来加载 Boot 文件?何时加载?
Boot 文件的加载过程
Boot 文件不是在编译时加载,而是在运行时由 Swift 代码加载。
看 Racket.swift:
public init(execPath: String = "racket") {
var args = racket_boot_arguments_t()
args.exec_file = execPath.utf8CString.cstring()
// 获取 boot 文件路径
args.boot1_path = NoiseBoot.petiteURL.path.utf8CString.cstring()
args.boot2_path = NoiseBoot.schemeURL.path.utf8CString.cstring()
args.boot3_path = NoiseBoot.racketURL.path.utf8CString.cstring()
// 调用 C 函数初始化 Racket VM
racket_boot(&args)
racket_deactivate_thread()
// 清理
args.exec_file.deallocate()
args.boot1_path.deallocate()
args.boot2_path.deallocate()
args.boot3_path.deallocate()
}
Boot 文件路径的获取
在 NoiseBoot.swift 中:
public struct NoiseBoot {
public static let petiteURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/petite", withExtension: "boot")!
public static let schemeURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/scheme", withExtension: "boot")!
public static let racketURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/racket", withExtension: "boot")!
}
这些 boot 文件在编译时通过 .copy("boot") 作为资源复制到应用 bundle 中。
racket_boot() 做了什么?
racket_boot() 是 C 函数,在静态库 libracketcs.a 中实现:
- 读取
petite.boot,初始化 Petite Scheme - 读取
scheme.boot,扩展为完整 Scheme - 读取
racket.boot,加载 Racket 语言特性 - 设置参数(如
exec_file,argv等) - 启动 Racket VM,进入可运行状态
关键理解:boot 文件是 Racket VM 的"启动镜像",包含了解释器和基础库的字节码。没有它们,Racket VM 就像一个空壳,无法执行任何代码。
五、运行流程详解
完整的启动流程
1. Swift 应用启动
↓
2. 创建 Racket 实例
let racket = Racket()
↓
3. Racket.init() 执行:
- 获取 boot 文件路径
- 调用 racket_boot(&args)
- racket_boot() 读取三个 boot 文件
- 初始化 Racket VM
↓
4. 加载 Racket 代码
racket.load(zo: URL(fileURLWithPath: "/path/to/noise-serde.zo"))
↓ 调用 racket_embedded_load_file()
- 编译 Racket 代码到内存
- 注册模块和函数
↓
5. 使用 Racket 功能
racket.require("noise/serde", from: "module")
- 调用 racket_dynamic_require()
- 加载指定的模块
- 返回模块的导出对象
↓
6. Swift 和 Racket 交互
- Swift → Racket: 调用 racket_apply()
- Racket → Swift: 使用 NoiseSerde 序列化
值包装和垃圾回收
Swift 通过 Val 类型包装 Racket 的指针(见 Racket.swift):
struct Val {
let ptr: ptr // void* 指针,指向 Racket 堆中的对象
}
关键机制:
- Racket 使用自己的垃圾回收器
- Swift 的
Val只是轻量级包装,不拥有所有权 - 所有 Racket 操作必须在创建 Racket 实例的线程上执行
- 使用
bracket()方法确保线程激活/停放的配对
六、noise-serde-lib 的双重角色
这是最容易误解的部分。很多人认为 noise-serde-lib 只是一个代码生成器,但实际上它有两个角色:
角色 1:编译时代码生成器
codegen.rkt 将 Racket 的类型定义转换为 Swift 代码:
#lang racket/base
(define-record person
[name : String]
[age : Int32])
运行 racket -l noise/codegen 生成:
public struct Person: Readable, Writable {
public let name: String
public let age: Int32
public static func read(from inp: InputPort, using buf: inout Data) -> Person {
return Person(
name: String.read(from: inp, using: &buf),
age: Int32.read(from: inp, using: &buf)
)
}
public func write(to out: OutputPort) {
name.write(to: out)
age.write(to: out)
}
}
角色 2:运行时序列化库
private/serde.rkt 提供 Racket 端的序列化实现:
(define (read-record info [in (current-input-port)])
(apply
(record-info-constructor info)
(for/list ([f (in-list (record-info-fields info))])
(read-field (record-field-type f) in))))
(define (write-record info v [out (current-output-port)])
(for ([f (in-list (record-info-fields info))])
(define type (record-field-type f))
(define value ((record-field-accessor f) v))
(write-field type value out)))
为什么两端都需要序列化?
因为序列化是双向的:
Swift → Racket:
Swift Person → Writable.write() → 字节流 → Racket read-record() → Racket Person
Racket → Swift:
Racket Person → write-record() → 字节流 → Swift Readable.read() → Swift Person
运行时加载
noise-serde-lib 的 Racket 代码会被编译成 .zo 文件,运行时加载:
racket.load(zo: URL(fileURLWithPath: "/path/to/noise-serde.zo"))
let serde = racket.require("serde", from: "module")
关键理解:noise-serde-lib 不在静态库中,而是作为编译后的 Racket 代码(.zo)被加载到 Racket VM 中执行。
七、完整的集成示例
开发阶段
;; 定义数据结构
#lang racket
(require noise-serde-lib)
(define-record user
[id : Varint]
[name : String]
[email : Optional String]
[scores : Listof Float32])
(define-enum role
[admin]
[user]
[guest])
运行代码生成器:
racket -l noise/codegen -t definitions.rkt > NoiseModels.swift
生成的 Swift 代码:
public struct User: Readable, Writable {
public let id: UVarint
public let name: String
public let email: String?
public let scores: [Float32]
}
public enum Role: Readable, Writable {
case admin
case user
case guest
}
运行阶段
import Noise
import NoiseSerde
import NoiseBackend
// 初始化 Racket VM
let racket = Racket()
// 加载 noise-serde 运行时
racket.load(zo: URL(fileURLWithPath: Bundle.module.path(forResource: "noise-serde", ofType: "zo")!))
// 加载你的 Racket 代码
racket.load(zo: URL(fileURLWithPath: Bundle.module.path(forResource: "my-app", ofType: "zo")!))
// 创建一个 Swift 对象
let user = User(id: 123, name: "Alice", email: "alice@example.com", scores: [0.9, 0.8, 0.95])
// 序列化为字节数据
var buffer = Data()
let outputPort = DataOutputPort(buffer: &buffer)
user.write(to: outputPort)
// 在 Racket 端反序列化(通过 RPC)
let result = backend.call("process-user", with: user)
// Racket 返回的结果自动反序列化为 Swift 类型
let processed: User = result.get()
八、总结
Noise 的集成可以概括为以下几个核心要点:
文件组成
| 文件类型 | 作用 | 谁提供 |
|---|---|---|
| 静态库 (.a) | Racket VM 的机器码 | Racket CS 构建 |
| 头文件 (.h) | C 函数接口声明 | Racket CS + Noise 封装 |
| Boot 文件 (.boot) | Racket VM 启动镜像 | Racket CS 构建 |
| .zo 文件 | Racket 编译后的字节码 | 你的 Racket 代码 + noise-serde-lib |
编译时流程
Racket CS 源码 → .a → .xcframework
↓
Swift Package Manager
↓
Noise Swift 代码 ← 头文件 ← .xcframework
↓
链接生成 Noise.framework
运行时流程
Swift 应用启动
↓
Racket.init() → racket_boot()
↓
读取 boot 文件 → 初始化 Racket VM
↓
加载 .zo 文件 → 注册 Racket 模块
↓
Swift ↔ Racket 通信
关键理解
- Noise 不是一个静态链接库,而是一个 Swift 库,它依赖 RacketCS.xcframework
- RacketCS.xcframework 只包含 VM,不包含业务逻辑
- Boot 文件由 Swift 代码加载,在运行时初始化 Racket VM
- 头文件被 Swift 编译器使用,生成调用 C 函数的代码
- noise-serde-lib 是双重角色:编译时生成 Swift 代码,运行时提供 Racket 序列化逻辑
Noise 的设计体现了语言的互操作性之美,通过精心设计的分层架构,让 Swift 和 Racket 能够无缝协作。希望本文能够帮助你更好地理解这个复杂但优雅的系统。