mxiris-reverse-engineering/machoswiftsection
A Swift library for parsing mach-o files to obtain Swift information.
Requirements
- Swift 6.2+
- Xcode 26.0+
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+ / visionOS 1+
MachOSwiftSection Library
Roadmap
- [x] Protocol Descriptors
- [x] Protocol Conformance Descriptors
- [x] Type Context Descriptors
- [x] Associated Type Descriptors
- [x] Method Symbol For Dyld Caches
- [x] Builtin Type Descriptors
- [x] Swift Interface Support
- [x] Runtime Metadata Inspection (
SwiftInspection) - [ ] Type Member Layout (WIP, MachOImage only)
- [ ] Swift Section MCP
Swift Package Manager
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection", from: "0.10.0"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "MachOSwiftSection", package: "MachOSwiftSection"),
// Optional higher-level products:
.product(name: "SwiftInspection", package: "MachOSwiftSection"),
.product(name: "SwiftDump", package: "MachOSwiftSection"),
.product(name: "SwiftInterface", package: "MachOSwiftSection"),
.product(name: "TypeIndexing", package: "MachOSwiftSection"),
]
),
]| Product | Purpose | | --- | --- | | MachOSwiftSection | Low-level parsing of _swift5* sections (raw descriptors). | | SwiftInspection | Runtime metadata inspection — EnumLayoutCalculator (multi-payload enum layouts), ClassHierarchyDumper, MetadataReader. | | SwiftDump | High-level type wrappers (Struct, Enum, Class, Protocol, ProtocolConformance, …). | | SwiftInterface | End-to-end Swift interface generation. | | TypeIndexing | Index types / extensions / conformances for cross-binary analysis. |
Usage
Basic
Swift information from MachOImage or MachOFile can be retrieved via the swift property.
import MachOKit
import MachOSwiftSection
let machO //` MachOFile` or `MachOImage`
// Protocol Descriptors
let protocolDescriptors = try machO.swift.protocolDescriptors
for protocolDescriptor in protocolDescriptors {
let protocolType = try Protocol(descriptor: protocolDescriptor, in: machO)
// do somethings ...
}
// Protocol Conformance Descriptors
let protocolConformanceDescriptors = try machO.swift.protocolConformanceDescriptors
for protocolConformanceDescriptor in protocolConformanceDescriptors {
let protocolConformance = try ProtocolConformance(descriptor: protocolConformanceDescriptor, in: machO)
// do somethings ...
}
// Type/Nominal Descriptors
let typeContextDescriptors = try machO.swift.typesContextDescriptors
for typeContextDescriptor in typeContextDescriptors {
switch typeContextDescriptor {
case .type(let typeContextDescriptorWrapper):
switch typeContextDescriptorWrapper {
case .enum(let enumDescriptor):
let enumType = try Enum(descriptor: enumDescriptor, in: machO)
// do somethings ...
case .struct(let structDescriptor):
let structType = try Struct(descriptor: structDescriptor, in: machO)
// do somethings ...
case .class(let classDescriptor):
let classType = try Class(descriptor: classDescriptor, in: machO)
// do somethings ...
}
default:
break
}
}Generate Complete Swift Interface
For generating complete Swift interface files, you can use the SwiftInterface library which provides a more comprehensive interface generation capability.
import MachOKit
import SwiftInterface
let builder = try SwiftInterfaceBuilder(configuration: .init(), eventHandlers: [], in: machO)
try await builder.prepare()
let result = try await builder.printRoot()Generated interfaces reflect a wide range of Swift language features:
- Type / member attributes:
@objc,@nonobjc,dynamic,@retroactive,@globalActor,@escaping,consuming/borrowingparameter modifiers distributed actordeclarations anddistributed funcmembersdeinitfor classes and noncopyable types- VTable offset comments alongside class members, ordered to match the on-disk layout
- Expanded field offsets for nested struct fields, rendered as a tree
- Inverted protocols (
~Copyable,~Escapable) on types and generic requirements
Inspect Runtime Metadata
SwiftInspection exposes higher-level inspection utilities built on top of MachOSwiftSection:
EnumLayoutCalculator— compute the on-disk layout of Swift enums, including single-payload and multi-payload (tagged and untagged) cases. Mirrors the ABI rules inswift/ABI/Enum.h.ClassHierarchyDumper— walk a class's inheritance chain across Swift/ObjC boundaries (requires@_spi(Internals) import SwiftInspection,MachOImageonly).MetadataReader— demangle types, symbols, context descriptors, and build generic signatures against a Mach-O.
swift-section CLI Tool
Installation
You can get the swift-section CLI tool in three ways:
- GitHub Releases: Download from GitHub releases
- Homebrew: Install via
brew install swift-section - Build from Source: Build with
./build-executable-product.sh(requires Xcode 26.0 / Swift 6.2+ toolchain)
Usage
The swift-section CLI tool provides six subcommands: dump, interface, diff, snapshot, evolution, and transformer.
[!IMPORTANT] As of 0.10.0, when the input is a fat / universal binary you must pass
--architecture <arch>. The tool no longer picks a default slice silently.
dump - Dump Swift Information
Dump Swift information from a Mach-O file or dyld shared cache.
swift-section dump [options] [file-path]Basic usage:
# Dump all Swift information from a Mach-O file
swift-section dump /path/to/binary
# Dump only types and protocols
swift-section dump --sections types,protocols /path/to/binary
# Save output to file
swift-section dump --output-path output.txt /path/to/binary
# Use specific architecture (required for fat binaries)
swift-section dump --architecture arm64 /path/to/binaryStatic memory-layout comments (computed offline, no process loaded):
# Field offsets for struct/class stored properties
swift-section dump --emit-field-offsets /path/to/binary
# Field offsets + per-field type layout (size / stride / alignment)
swift-section dump --emit-field-offsets --emit-type-layout /path/to/binary
# Expand nested struct fields with their absolute offsets
swift-section dump --emit-expanded-field-offsets /path/to/binary
# Enum layout (strategy / per-case / spare bits)
swift-section dump --emit-enum-layout /path/to/binary
# Enum layout with a different comment style — detailed (default), explained
# (bit ranges in plain words), standard (no per-byte lines), inline (one line
# per case with the byte summary), or compact
swift-section dump --enum-layout-style explained /path/to/binaryHeader and export-status annotations:
# Leading header block: generator, image path, UUID, architecture,
# library-evolution detection (dispatch-thunk count), and a short digest of
# facts the binary provably cannot recover (IUO spelling, @available, …)
swift-section dump --emit-header /path/to/binary
# Annotate member-symbol lines whose symbol (including its Tj/Tq/Tu derived
# forms) has no export-trie entry — a symbol-table fact, not an access-level
# guess. Override implementation symbols and @objc members are exempt (they
# are reachable through the parent's dispatch thunk / objc_msgSend without
# any exported symbol of their own). Nothing is emitted when the image
# carries no export information.
swift-section dump --emit-export-status /path/to/binaryEvery comment kind above can also be reformatted with your own template — see transformer. Passing a template option implies the matching --emit-… flag.
These offsets are computed statically by the SwiftLayout engine — no runtime, no metadata accessor, no loading the binary into a process — so they work on any on-disk Mach-O file (including resilient classes and cross-module field types, resolved through the dependency closure over the dyld shared cache, as well as value-generic and parameter-pack instantiations such as InlineArray<5, Int8> or Variadic<Int, String> fields). The interface command's --emit-offset-comments / --emit-expanded-field-offsets flags use the same static engine.
Working with dyld shared cache:
# Dump from system dyld shared cache
swift-section dump --uses-system-dyld-shared-cache --cache-image-name SwiftUICore
# Dump from specific dyld shared cache
swift-section dump --dyld-shared-cache --cache-image-path /path/to/cache /path/to/dyld_shared_cacheDump output includes richer annotations:
- Protocol witness table (PWT) entries are annotated with the requirement they satisfy
- Inverted protocol constraints (
~Copyable,~Escapable) are rendered on types and generic requirements - Protocol conformances can include the PWT address
interface - Generate Swift Interface
Generate a complete Swift interface file from a Mach-O file, similar to Swift's generated interfaces.
swift-section interface [options] [file-path]Basic usage:
# Generate Swift interface from a Mach-O file
swift-section interface /path/to/binary
# Save interface to file
swift-section interface --output-path interface.swiftinterface /path/to/binary
# Use specific architecture (required for fat binaries)
swift-section interface --architecture arm64 /path/to/binaryStatic memory-layout comments:
# Field offsets (and PWT offsets) on the generated interface
swift-section interface --emit-offset-comments /path/to/binary
# Per-field type layout (size / stride / alignment) and enum layout
swift-section interface --emit-type-layout --emit-enum-layout /path/to/binaryThese use the same static SwiftLayout engine as dump, and accept the same comment-template options — see transformer.
Header and export-status annotations:
# Leading header block ahead of the imports: generator, image path, UUID,
# architecture, library-evolution detection (dispatch-thunk count), and a
# short digest of facts the binary provably cannot recover
swift-section interface --emit-header /path/to/binary
# Annotate members none of whose symbols (including Tj/Tq/Tu derived forms)
# have an export-trie entry with a `// not exported` comment. `override` and
# `@objc` members are exempt — they are reachable through the parent's
# dispatch thunk / objc_msgSend without any exported symbol of their own.
swift-section interface --emit-export-status /path/to/binary
# Print only the declarations the image exports — the filtering counterpart of
# --emit-export-status. Types and protocols are ruled by their descriptor
# symbol's export-trie entry, extensions by whether their target is an
# in-image non-exported declaration, members by the same derived-form verdict
# the annotation uses. Still a symbol-table fact, never an access-level guess:
# anything without export evidence (and every `override` / `@objc` member) is
# kept, so an `-enable-testing` build keeps its `internal` declarations.
swift-section interface --exported-only /path/to/binaryAll three flags default to off, keeping default output byte-identical.
Working with dyld shared cache:
# Dump from system dyld shared cache
swift-section interface --uses-system-dyld-shared-cache --cache-image-name SwiftUICore
# Dump from specific dyld shared cache
swift-section interface --dyld-shared-cache --cache-image-path /path/to/cache /path/to/dyld_shared_cachediff - Compare the ABI of Two Versions
Diff the Swift ABI of two versions of the same module at the binary level — field retypes, enum-case tag renumbering, accessor changes, added/removed conformances — details a .swiftinterface diff cannot see. Extension changes are attributed per conformance / per conditional block (Target: Protocol where …), so adding or dropping a single conformance reads as one container-level change. Protocols whose requirement symbols are stripped (the OS-framework norm) still diff by their witness-table slots, so a protocol gaining or losing a requirement is visible with zero symbols; compare binaries in similar strip states, since a symbol-rich vs stripped pair reports the same requirement as a member swap.
# Change-list report with a breaking/backward-compatible verdict
swift-section diff old/Foo.framework/Foo new/Foo.framework/Foo
# Either side may be a persisted baseline produced by `snapshot`
swift-section diff baseline.json new/Foo.framework/Foo
# Machine-readable output / CI gating
swift-section diff old.dylib new.dylib --json
swift-section diff old.dylib new.dylib --summary-only --fail-on-breaking
# Full interface annotated with +/- diff markers (needs two binaries)
swift-section diff old.dylib new.dylib --interface --format unifiedsnapshot - Persist an ABI Baseline
Index a binary once and freeze its ABI into a versioned JSON baseline; later diffs and evolution runs can consume the JSON without the original binary.
swift-section snapshot /path/to/binary --label 1.0 -o baseline-1.0.json
# From a dyld shared cache image
swift-section snapshot --dyld-shared-cache -n SwiftUICore /path/to/dyld_shared_cache --label 26.0 -o swiftuicore-26.0.jsonevolution - Track ABI Across Many Versions
Track one module's ABI across an ordered series of versions (oldest first) and report each declaration's lifeline: introduced / modified / removed / re-added, with a per-transition additive-or-breaking verdict. Inputs mix freely between binaries, dyld shared caches, and snapshot baselines.
# Three OS versions of the same framework, one report
swift-section evolution 17.0.json 18.0.json /path/to/Foo-26.0.dylib --labels 17.0,18.0,26.0
# Across dyld shared caches (extracts the same image from each cache)
swift-section evolution --dyld-shared-cache -n SwiftUICore cache-17 cache-18 cache-26
# Summary or JSON, and CI gating on any breaking transition
swift-section evolution v1.json v2.json v3.json --summary-only --fail-on-breaking
swift-section evolution v1.json v2.json v3.json --jsonWith --interface, the same axis renders as a single annotated union interface instead of the lineage list: every declaration that ever existed appears once (rendered from the last version that has it), declarations that changed carry a trailing // [●●○] removed in 26.0-style comment (presence bitmap + event phrases; the legend at the top maps bitmap positions to version labels), and declarations present throughout with no changes stay bare. A member whose signature changed shows its newest generation, with the old shape in the comment (modified in 26.0: old → new). Because the interface renders from live models, every input must be a binary or dyld shared cache in this mode — snapshot JSON inputs are rejected.
# The union interface with lifecycle annotations, colorized on a terminal
swift-section evolution --interface v17/Foo.dylib v18/Foo.dylib v26/Foo.dylib --labels 17.0,18.0,26.0
# Across dyld shared caches, written to a file, gating CI on breaking changes
swift-section evolution --interface --dyld-shared-cache -n SwiftUICore cache-17 cache-18 cache-26 --fail-on-breaking -o SwiftUICore-evolution.swifttransformer - Customize Comment Formats
The memory-layout comments dump and interface emit are rendered from token templates. Each of the five comment kinds — field offset, vtable offset, member address, type layout, enum layout — has its own template, its own ${token} placeholders, and a set of built-in templates you can name. This subcommand lists them and builds reusable configurations; the template options themselves are accepted directly by dump and interface.
# What can a template say?
swift-section transformer tokens # every module
swift-section transformer tokens --module enum-layout # one module
# Built-in templates — their names are accepted by the template options
swift-section transformer templates --module field-offsetUsing a template. Pass either a built-in template name or a literal template containing ${token} placeholders. A name that matches nothing is an error rather than a silently constant comment.
# Built-in template by name: "// 0x0 ..< 0x10"
swift-section dump --field-offset-template range /path/to/binary
# Literal template: "// @0x0"
swift-section dump --field-offset-template '@${startOffset}' /path/to/binary
# Enum layout is three templates: strategy line, per case, per fixed byte
swift-section dump \
--enum-layout-template strategyOnly \
--enum-layout-case-template inlineSummary \
/path/to/binaryPassing any template option enables the comment kind it formats, so no separate --emit-… flag is needed. Numbers can be switched between hexadecimal and decimal per module (--field-offset-hex / --no-field-offset-hex, and so on).
Reusable configurations. A whole set of templates can be frozen into a JSON file and replayed with --transformer-config. The file format is the one RuntimeViewer persists, so a configuration tuned in its settings UI works here unchanged.
# Freeze a command line into a file
swift-section transformer config \
--field-offset-template range \
--enum-layout-style compact \
--output-path comments.json
# Replay it
swift-section dump --transformer-config comments.json /path/to/binary
swift-section interface --transformer-config comments.json /path/to/binaryRunning Tests
The snapshot tests in this repository rely on a fixture framework (SymbolTestsCore) built from an Xcode project in Tests/Projects/SymbolTests/. The framework binary is not checked in — rebuild it once after cloning:
./Scripts/build-test-fixtures.shThen run the tests:
swift package update
swift testSkipping the fixture build causes MachOFileTests to throw a "file not found" error at Tests/Projects/SymbolTests/DerivedData/.../SymbolTestsCore during test init(), before any assertions run.
To regenerate snapshots after a legitimate Swift-compiler / metadata change:
SNAPSHOT_TESTING_RECORD=all swift test \
--filter SymbolTestsCoreDumpSnapshotTests \
--filter SymbolTestsCoreInterfaceSnapshotTestsCommit the updated Snapshots/ files alongside the source change that prompted the regeneration.
License
MachOSwiftSection is released under the MIT License. See LICENSE
Package Metadata
Repository: mxiris-reverse-engineering/machoswiftsection
Default branch: main
README: README.md