Swift code generation for PJSIP's C API. SwiftPM plugins that parse the PJSIP
headers your app actually builds against — sourced from
swift-pjsip — and generate the Swift
ergonomics the C importer doesn't give you.
import PJSIP hands you PJSIP's C types verbatim, and the rough edges show up
immediately:
- Printing a
pjsua_acc_configor apjsip_status_codein a debugger or log gives you nothing useful — no case names, no field summaries. - The PJSIP API surface is huge (dozens of config structs, enums with hundreds
of cases) and changes between releases. Hand-written
CustomStringConvertibleextensions rot silently the moment you bump the binary.
So the extensions are generated, not written: the generator walks the real headers
shipped inside the swift-pjsip xcframework, discovers the types transitively from
the roots you name, and emits @retroactive CustomStringConvertible /
CustomDebugStringConvertible conformances that match the exact PJSIP version you
link. Bump swift-pjsip, rebuild, and the helpers regenerate to match.
The package shape follows
apple/swift-openapi-generator:
a core library, a thin CLI, a build-tool plugin (regenerate on every build) and a
command plugin (generate on demand, commit the output) — use either or both.
| Product | Kind | Purpose |
|---|---|---|
PJSIPSwiftGenCore |
library | Header parsing, type discovery, code generation primitives. |
pjsip-swift-gen |
executable | Thin CLI wrapping the core library. |
PJSIPSwiftGenPlugin |
build tool | Runs the generator on every build of the consuming target. |
PJSIPSwiftGenCommand |
command | Runs the generator on demand (swift package plugin …). |
Declare swift-pjsip-gen and swift-pjsip in the consumer. (The plugin finds
PJSIP's headers by walking your dependency graph for the swift-pjsip package, so
the consumer must depend on it directly — which a PJSIP app does anyway.)
dependencies: [
.package(url: "https://github.com/laconicman/swift-pjsip", from: "0.1.0"),
.package(url: "https://github.com/laconicman/swift-pjsip-gen", from: "0.2.0"),
]Drop a pjsip-swift-gen.json into the target's source directory (for Xcode app
targets: add it to the target, so the plugin sees it among the input files):
{
"searchRoots": [""],
"rootTypes": ["pjsua_acc_config", "pjsua_call_info", "pjsip_status_code"],
"skipTypes": ["pj_str_t", "pjsip_hdr"],
"manualTypes": [],
"imports": ["PJSIP"]
}| Key | Meaning |
|---|---|
searchRoots |
Subdirectories of the headers root to scan. [""] scans the whole tree — right for the xcframework's flat Headers/. An entry that does not exist fails the run. |
rootTypes |
Seed types; their struct fields are walked transitively to discover more. An entry no scanned header declares, or an empty list, fails the run. |
skipTypes |
Never discover/generate these. |
manualTypes |
Discovered but not generated — you maintain a hand-written extension. |
imports |
Modules to import at the top of every generated file (e.g. ["PJSIP"]). |
pjprojectRoot |
(optional) Explicit headers path; only needed when auto-discovery can't run (see below). |
.target(
name: "MySipFeature",
dependencies: [.product(name: "PJSIP", package: "swift-pjsip")],
plugins: [.plugin(name: "PJSIPSwiftGenPlugin", package: "swift-pjsip-gen")]
)Generated files land in the plugin work directory and compile into the target automatically — nothing to commit.
swift package plugin --allow-writing-to-package-directory \
generate-pjsip-helpers [--target MySipFeature] [--output-dir Sources/MySipFeature/Generated]Defaults: every target containing a pjsip-swift-gen.json, output to
<target>/Generated.
For an enum, a debugDescription/description that names the case; for a struct, a
description summarizing its fields (with PJSIP's count + fixed-size-array pairs
rendered as real collections; the small helper that does it ships in the generated
file). Every file is marked auto-generated and
#if-guarded to mirror preprocessor conditions found in the headers.
// Auto-generated from pjsua.h. DO NOT EDIT MANUALLY.
import PJSIP
extension pjsua_state: @retroactive CustomDebugStringConvertible {
public var debugDescription: String { ... }
}In order:
- SwiftPM builds (and package targets under Xcode): walks the consumer's
dependency graph for the
swift-pjsippackage and reads<slice>/Headersfrom itsPJSIPbinary target's xcframework, wherever SwiftPM extracted it. - Xcode project targets: Xcode's plugin API exposes no dependency graph, so the
plugin walks up from its work directory inside DerivedData to
SourcePackages/artifacts/swift-pjsip/PJSIP/PJSIP.xcframework(for swift-pjsip 0.1.x, which committed the binary:SourcePackages/checkouts/swift-pjsip/Binaries/…). - Escape hatch:
pjprojectRootin the JSON config (resolved relative to the config file) — also how you point the generator at a rawpjprojectsource tree instead of the xcframework.
xcodebuild refuses to run unvalidated plugins non-interactively. In CI, pass:
xcodebuild ... -skipPackagePluginValidationSources/PJSIPSwiftGenCore/ parsing + discovery + generation (the only logic home)
Sources/pjsip-swift-gen/ CLI: `generate`, `list-outputs`
Plugins/PJSIPSwiftGenPlugin/ build-tool plugin (+ duplicated discovery sources, see below)
Plugins/PJSIPSwiftGenCommand/ command plugin
Tests/ core tests
scripts/check-duplicate-sources.sh
Two SwiftPM plugin constraints shape the design, and both are documented in detail
in docs/DESIGN.md:
- Plugin targets cannot import library targets, and in Xcode the plugin cannot
spawn its own executable during build planning (the tool path is still an
unresolved
${BUILD_DIR}placeholder at that point). The discovery sources are therefore deliberately duplicated into the build-tool plugin;scripts/check-duplicate-sources.sh --common Sources/PJSIPSwiftGenCore Plugins/PJSIPSwiftGenPluginguards byte-equality. - Outputs must be declared at build-planning time, so the plugin re-runs
discovery itself and declares the config file and every PJSIP header as inputs —
bumping
swift-pjsipinvalidates the build commands and regeneration happens exactly when it should.
Ideas under consideration — issues and PRs welcome:
- Import C enums as real Swift
enums (instead of the importer'sstatic lets), withOptionSetgeneration for flag-style enums. - Ergonomic
pj_str_t⇄Stringbridging helpers. - Carry Doxygen comments from the headers into the generated declarations.
- A minimal reproduction to retest whether the source duplication is still forced
(see
docs/DESIGN.md).
swift-pjsip— PJSIP as a single SPM binary package (the headers this generator consumes), including the reproducible xcframework build scripts.
MIT.