1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
use crate::codegen::shared::binding_fields;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use crate::core::config::{
AdapterConfig, AdapterPattern, Language, ResolvedCrateConfig, TraitBridgeConfig, resolve_output_dir,
};
use crate::core::ir::{ApiSurface, TypeDef};
use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::backends::swift::gen_rust_crate;
use crate::backends::swift::type_map::SwiftMapper;
mod boxes;
mod bridge_artifacts;
mod client;
mod dto;
mod enums;
mod errors;
mod forwarders;
pub(crate) mod opaque_handles;
mod overloads;
pub mod plugin_marshal;
pub mod service_api;
mod streaming;
pub mod trait_bridge;
pub struct SwiftBackend;
fn effective_exclude_types(config: &ResolvedCrateConfig, api: &ApiSurface) -> std::collections::HashSet<String> {
let mut exclude_types: std::collections::HashSet<String> = config
.ffi
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
if let Some(swift) = &config.swift {
exclude_types.extend(swift.exclude_types.iter().cloned());
}
exclude_types.extend(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.clone()));
exclude_types.extend(api.enums.iter().filter(|e| e.binding_excluded).map(|e| e.name.clone()));
exclude_types.extend(api.excluded_type_paths.keys().cloned());
// Declared opaque types are external host-runtime references whose actual Rust path
// carries generic parameters the injected IR cannot model; skip them.
exclude_types.extend(config.opaque_types.keys().cloned());
exclude_types
}
fn emit_sendable_conformance(out: &mut String, type_name: &str, mark: Option<&str>, comments: &[&str]) {
out.push_str(&crate::backends::swift::template_env::render(
"swift_sendable_conformance.swift.jinja",
minijinja::context! {
type_name => type_name,
mark => mark,
comments => comments,
},
));
}
impl Backend for SwiftBackend {
fn name(&self) -> &str {
"swift"
}
fn language(&self) -> Language {
Language::Swift
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
supports_callbacks: false,
supports_streaming: true,
supports_service_api: true,
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let module_name = config.swift_module();
let mapper = SwiftMapper;
// The Swift host facade is a single compiled surface with no Rust-cfg gating, so
// same-named cfg-variant functions (real impl + no-ORT stub fallback) must collapse to a
// single forwarder to avoid duplicate-declaration errors. The Rust-side `gen_rust_crate`
// bridge keeps the original multi-entry surface because it cfg-filters per configured
// feature set itself; we hand it `original_api` below. See codegen::fn_dedup.
let original_api = api;
let deduped_api = api.with_deduped_functions();
let api = &deduped_api;
// Function-wrapper emission is disabled in this phase (see comment below);
// `swift.exclude_functions` therefore has no effect on the host wrapper but
// is still consumed by the Rust-side bridge crate via gen_rust_crate::emit.
let exclude_types = effective_exclude_types(config, api);
// The Rust-side gen_rust_crate uses the same `exclude_fields` set to gate which
// fields appear in the `#[swift_bridge(init)] fn new(...)` constructor extern.
// We mirror that set here so `intoRust()` knows when a bulk constructor extern
// is available and which fields it accepts.
let exclude_fields: std::collections::HashSet<String> = config
.swift
.as_ref()
.map(|c| c.exclude_fields.iter().cloned().collect())
.unwrap_or_default();
let mut imports: BTreeSet<String> = BTreeSet::new();
// Foundation is always included — Codable, Data, URL all live there.
imports.insert("import Foundation".to_string());
// RustBridge is the Swift module generated by swift-bridge from the Rust bridge crate.
// It provides opaque type definitions and FFI entry points.
if !api.types.is_empty() || !api.enums.is_empty() || !api.errors.is_empty() {
imports.insert("import RustBridge".to_string());
}
let mut body = String::new();
// Unit serde enums (all-unit + has_serde) are emitted as `public enum S: String, Codable`
// with serde-derived raw values. They are Codable so structs containing them as fields
// can also derive Codable. Collect their names before the fixed-point loop so the loop
// can accept `Named(enum_S)` fields.
let unit_serde_enum_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|e| !exclude_types.contains(&e.name))
.filter(|e| e.has_serde && e.variants.iter().all(|v| v.fields.is_empty()))
.map(|e| e.name.clone())
.collect();
// Compute which struct DTO types will actually be emitted as first-class Swift
// structs via a fixed-point iteration.
//
// A type is first-class iff:
// 1. It is non-opaque, has_serde, non-trait, non-excluded, and has visible fields.
// 2. Every visible field's TypeRef is "supported" given the current first-class set.
//
// Unit serde enums (above) are Codable and seed the initial `known_dto_names` set.
// Struct DTOs are added iteratively: a struct becomes first-class when all its fields
// are known-supported (primitive, String, known first-class struct, or unit serde enum).
//
// Convergence is guaranteed: each round only adds types (monotonically growing set).
let candidate_types: Vec<&crate::core::ir::TypeDef> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_serde && !exclude_types.contains(&t.name))
.filter(|t| !t.fields.is_empty())
.collect();
// Collect every serde data-variant enum (both untagged AND tagged) that bridges
// as a JSON `RustString` at the FFI boundary rather than as an opaque
// `RustBridge.{Name}Ref`. swift-bridge does not emit an `init(_ rb:Ref)` for
// either form — untagged enums (`#[serde(untagged)]`) are emitted directly as
// JSON, and tagged enums (default external tagging or `#[serde(tag = "…")]`)
// are also returned as JSON strings from the bridge accessor. Containing structs
// must decode these via JSONDecoder rather than via the broken `try {Name}(rb.field())`
// path. The set name is kept as `untagged_enum_names` for legacy continuity; it
// captures any Codable enum with data variants whose bridge accessor returns
// RustString and whose Swift target type only has `init(from: Decoder)`.
let untagged_enum_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|e| !exclude_types.contains(&e.name))
.filter(|e| e.has_serde && e.variants.iter().any(|v| !v.fields.is_empty()))
.map(|e| e.name.clone())
.collect();
// Seed with unit serde enum names + every data-variant Codable enum name (tagged +
// untagged, all collected above). All are Codable and can appear as struct fields.
// They are included so that `first_class_field_supported` accepts them as valid
// field types, allowing containing structs to be emitted as first-class Swift
// structs. The init code then routes Named fields of these types through
// JSONDecoder rather than the Ref path.
let mut known_dto_names: std::collections::HashSet<String> = unit_serde_enum_names.clone();
known_dto_names.extend(untagged_enum_names.iter().cloned());
loop {
let prev_len = known_dto_names.len();
for ty in &candidate_types {
if known_dto_names.contains(&ty.name) {
continue;
}
// Check if all visible (binding-non-excluded) fields are supported given the
// current known set (struct DTOs + unit serde enums + tagged enums).
let all_supported = binding_fields(&ty.fields)
.all(|field| dto::first_class_field_supported(&field.ty, &known_dto_names));
if all_supported {
known_dto_names.insert(ty.name.clone());
}
}
if known_dto_names.len() == prev_len {
break; // stable — no new types added this round
}
}
// Emit typealiases for all struct types exposed by swift-bridge.
// swift-bridge exposes types that are declared in the extern "Rust" block,
// so we generate typealiases for all non-excluded types to provide a
// stable Swift API that references RustBridge types.
// Skip opaque handle types with methods — those get a full class wrapper below
// instead of a typealias.
for ty in api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
.filter(|t| t.methods.is_empty() || !t.is_opaque && t.has_serde)
{
client::emit_doc_comment(&ty.doc, "", &mut body);
if dto::can_emit_first_class_struct(ty, &mapper, &exclude_fields, &known_dto_names) {
// Compute the Swift error enum name as emitted by `errors::emit_error`:
// it uses `error.name` directly, except bare `Error` is renamed to
// `{module_name}Error` to avoid the parser ambiguity of
// `public enum Error: Error`. DTO call sites must reference this
// emitted name, NOT `config.error_type_name()` (which defaults to
// "Error" when not configured even for crates whose actual Rust
// error type is named differently, e.g. `CrawlError`).
let dto_error_name = api
.errors
.first()
.map(|e| {
if e.name == "Error" {
format!("{module_name}Error")
} else {
e.name.clone()
}
})
.unwrap_or_else(|| {
let raw = config.error_type_name();
if raw == "Error" {
format!("{module_name}Error")
} else {
raw
}
});
dto::emit_first_class_struct(
ty,
&mapper,
&exclude_fields,
&known_dto_names,
&unit_serde_enum_names,
&untagged_enum_names,
&dto_error_name,
&mut body,
);
} else {
body.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &ty.name,
},
));
}
body.push('\n');
}
// Collect result enum names from trait bridges — these are emitted as first-class
// Swift enums that JSON-decode locally, so they don't call a Rust-side from_json fn.
let result_type_enums: std::collections::HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.result_type.as_deref().map(|s| s.to_string()))
.collect();
// Enums are emitted as native Swift enums with unit variants only.
// They are NOT typealiased to RustBridge since swift-bridge's automatic generation
// is unreliable (not all enum types are exposed). Instead, we emit them directly
// as Swift enums that mirror the unit variants from the Rust bridge enum wrapper.
for en in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
if result_type_enums.contains(&en.name) {
// Result-type enums are emitted without intoRust() because Swift decodes them
// locally via JSONDecoder, not via a Rust-side from_json function.
enums::emit_enum_without_into_rust(en, &mut body, &mapper, &known_dto_names);
} else {
enums::emit_enum(en, &mut body, &mapper, &known_dto_names);
}
body.push('\n');
}
for error in &api.errors {
errors::emit_error(error, &module_name, &mut body, &mapper);
body.push('\n');
}
// Emit Swift class wrappers only for opaque handle types that expose methods
// AND have an explicit `client_constructor_body` in alef.toml. Without an
// override the Rust bridge crate does not emit a `create_<type>` shim
// (see gen_rust_crate::mod.rs), so a class wrapper with `init(apiKey,
// baseUrl)` would reference a non-existent bridge function.
// Opaque types without an override are returned by Rust APIs, not
// constructed in Swift — they remain accessible via their `RustBridge`
// typealiases / direct method shims.
let client_constructor_types: std::collections::HashSet<&str> = config
.swift
.as_ref()
.map(|c| c.client_constructor_body.keys().map(String::as_str).collect())
.unwrap_or_default();
// Collect first-class struct type names for streaming chunk decode dispatch.
// Must match the actual emission decision in `can_emit_first_class_struct`
// (Codable struct with all-supported fields).
// Types failing that check are emitted as typealiases to RustBridge.X and so
// do not have a `func intoRust()` extension or auto-derived Codable conformance.
let first_class_types: std::collections::HashSet<String> = api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
.filter(|t| dto::can_emit_first_class_struct(t, &mapper, &exclude_fields, &known_dto_names))
.map(|t| t.name.clone())
.collect();
// Tracks every type that has already received an `extension RustBridge.X:
// @unchecked Sendable {}` so the streaming-specific emissions and the
// catch-all block below never declare the conformance twice (Swift warns
// on a redundant conformance).
let mut sendable_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
for ty in api.types.iter().filter(|t| {
!t.is_trait
&& !exclude_types.contains(&t.name)
&& !t.methods.is_empty()
&& (t.is_opaque || !t.has_serde)
&& client_constructor_types.contains(t.name.as_str())
}) {
client::emit_client_class(
ty.name.as_str(),
&ty.methods,
&mapper,
config,
&first_class_types,
&mut body,
);
body.push('\n');
// Emit `@unchecked Sendable` conformance for every StreamHandle owned by this
// client type. swift-bridge emits opaque types as plain Swift classes without
// `Sendable`; Swift 6 strict-concurrency rejects passing them across
// `Task.detached` boundaries. The Rust side wraps a Mutex<stream> and a
// tokio Runtime — both thread-safe — so `@unchecked` is correct.
// Collect streaming adapters owned by this client type.
let streaming_adapters: Vec<&AdapterConfig> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "swift"))
.filter(|a| a.owner_type.as_deref() == Some(ty.name.as_str()))
.collect();
if !streaming_adapters.is_empty() {
// The streaming methods capture `self.inner` (typed `RustBridge.{ty.name}`)
// inside a `Task.detached` closure. Swift 6 strict-concurrency requires every
// captured value to be `Sendable`. The Rust type is `Send + Sync`, so
// `@unchecked` is correct.
let inner_ty = ty.name.as_str();
if sendable_emitted.insert(inner_ty.to_string()) {
emit_sendable_conformance(
&mut body,
inner_ty,
Some("streaming client inner"),
&[
"swift-bridge opaque types are not automatically Sendable.",
"Captured by Task.detached in streaming methods — Rust type is Send + Sync.",
],
);
}
}
for adapter in &streaming_adapters {
client::emit_stream_handle_sendable(adapter, ty.name.as_str(), &mut body);
}
// Emit `@unchecked Sendable` conformance for every opaque param type
// used in streaming adapter calls. These types are passed into
// `Task.detached` closures; Swift 6 strict-concurrency requires them
// to be `Sendable`. They are swift-bridge opaque classes backed by
// Rust types that are `Send + Sync`, so `@unchecked` is correct.
{
for adapter in &streaming_adapters {
for param in &adapter.params {
let simple_ty = param.ty.rsplit("::").next().unwrap_or(¶m.ty).to_string();
if sendable_emitted.insert(simple_ty.clone()) {
emit_sendable_conformance(
&mut body,
&simple_ty,
Some("streaming request param"),
&[
"swift-bridge opaque types are not automatically Sendable.",
"Passed into Task.detached for streaming — Rust type is Send + Sync.",
],
);
}
}
}
}
}
// Emit lightweight convenience overloads for functions whose first parameter
// is TypeRef::Bytes or TypeRef::Path. Discovery is IR-driven: the emitter
// walks api.functions and detects candidates by signature shape, never by name.
client::emit_convenience_wrappers(api, &exclude_types, &mut body);
// Emit JSON-string convenience overloads for functions that take config-like
// struct parameters. These allow e2e tests to pass JSON strings for config
// objects, automatically decoding them via the `*FromJson` helpers.
// Also emit the _loadBytesFromPathOrUtf8 helper for path-or-content resolution.
overloads::emit_json_string_overloads(api, &exclude_types, &mut body);
// Emit public Swift forwarding functions for `*FromJson` helpers.
// The Rust bridge crate exposes `{type_snake}_from_json` as a swift-bridge
// free function accessible via `RustBridge.{swiftName}(json)`. Without a
// forwarding wrapper in this module, callers would need the `RustBridge.`
// prefix, which the generated e2e test layer doesn't use.
overloads::emit_from_json_forwarders(
api,
&exclude_types,
&mapper,
&exclude_fields,
&known_dto_names,
&mut body,
);
// Emit Swift protocol + adapter class for OptionsField inbound trait bridges.
// Each bridge in `config.trait_bridges` where `bind_via = "options_field"` gets:
// - A `public protocol {Trait}Protocol: AnyObject` with one method per trait method.
// - A private adapter class that wraps the protocol conformer.
// - A factory func `make{TraitCamel}Handle(...)`.
// NOTE: The `Swift{Trait}Box` class is emitted into Sources/RustBridge/ (separate file)
// because it is referenced by the swift-bridge generated @_cdecl shims there.
bridge_artifacts::emit_inbound_protocols(api, config, &exclude_types, &mut body);
// Emit top-level public forwarders for every free function in `api.functions`
// that is not already covered by the bytes/path convenience wrappers or
// the `*FromJson` forwarders. Without these,
// `embedTexts`, `listEmbeddingPresets`, `getEmbeddingPreset`, `renderPdfPageToPng`,
// `getExtensionsForMime`, `detectMimeType`, and the various `list_*` registry
// helpers are only reachable as `RustBridge.fnName(...)` — which requires
// users of the host module to also `import RustBridge`, violating
// the alef binding-parity promise that every public Rust free function is
// re-exposed as a top-level public function on the host module.
let client_class_names: std::collections::HashSet<String> =
client_constructor_types.iter().map(|&s| s.to_string()).collect();
let all_enum_names: std::collections::HashSet<String> = unit_serde_enum_names
.iter()
.chain(untagged_enum_names.iter())
.cloned()
.collect();
forwarders::emit_free_function_forwarders(
api,
config,
&known_dto_names,
&all_enum_names,
&client_class_names,
&exclude_types,
&mut body,
);
// Emit top-level public forwarders for trait-bridge register/unregister/clear
// entry points. These are synthesised from `[[trait_bridges]]` in alef.toml
// (not present in `api.functions`), so the free-function forwarder pass above
// does not reach them. Mirrors the extendr/napi/php fixes — every native
// binding exposes `register_<trait>` / `unregister_<trait>` / `clear_<trait>`
// as part of the public plugin registration surface.
forwarders::emit_trait_bridge_forwarders(config, &mut body);
// Emit AsyncThrowingStream free functions for streaming adapters whose owner
// type has no Swift client class wrapper (no client_constructor_body in alef.toml).
// For these adapters, emit_client_class is never called so the streaming methods
// must be top-level free functions taking the handle as the first parameter.
client::emit_streaming_free_functions(config, &first_class_types, &mut sendable_emitted, &mut body);
// Emit `@unchecked Sendable` conformance for all remaining opaque types.
// swift-bridge emits opaque types as plain Swift classes without `Sendable`
// conformance. Swift 6 strict-concurrency rejects passing non-Sendable values
// across `Task.detached` boundaries, which occurs in async function forwarders
// that wrap blocking Rust calls. All swift-bridge opaque types wrap Rust
// pointers to Send + Sync types, so `@unchecked` is correct for all of them.
//
// The set covers both genuinely-opaque IR types and types returned across the
// bridge as opaque handles (`compute_handle_returned_types`). The latter is
// essential: a result struct such as `ScrapeResult` may also be emitted as a
// first-class Swift struct, but it is still bridged as an opaque
// `RustBridge.ScrapeResult` class that an async forwarder returns out of a
// `Task.detached`. `sendable_emitted` prevents re-declaring a conformance the
// streaming emissions above already produced.
{
// Emit `@unchecked Sendable` conformance for every non-trait type that has a
// `RustBridge.<T>` opaque-class shadow (i.e. anything not excluded from the
// binding). swift-bridge emits all of these as plain classes wrapping a Rust
// pointer to a Send + Sync type, so the conformance is safe. Previously the
// filter only included `is_opaque || handle_returned` — that missed
// parameter-only DTOs such as `BatchFileItem` / `BatchBytesItem`, which then
// failed Swift 6 strict-concurrency checks when captured by async forwarders'
// `Task.detached` closures.
for ty in api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
{
if sendable_emitted.insert(ty.name.clone()) {
emit_sendable_conformance(
&mut body,
&ty.name,
None,
&["swift-bridge opaque type used across Task.detached boundaries — Rust type is Send + Sync."],
);
}
}
// Second pass: any Named type referenced as `Vec<Named>` (or bare Named) in
// an async forwarder return position also needs `@unchecked Sendable`.
// Such types may have been filtered out of `api.types` above (e.g. when an
// alef(skip)-annotated cfg-gated stub shadows the canonical) but still
// surface in the swift-bridge extern as `type T;` because they appear in a
// public return signature, and the forwarder body crosses the
// `Task.detached.value` boundary with `[T]`. Without this pass, Swift 6
// strict-concurrency rejects the closure return.
//
// Collects from `api.functions` only (async, free fns); methods are handled
// by their owner's client-class Sendable emission path.
fn collect_async_vec_named<'a>(
ty: &'a crate::core::ir::TypeRef,
names: &mut std::collections::HashSet<&'a str>,
) {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Vec(inner) | TypeRef::Optional(inner) => {
collect_async_vec_named(inner, names);
}
TypeRef::Named(n) => {
names.insert(n.as_str());
}
_ => {}
}
}
let mut referenced_async_named: std::collections::HashSet<&str> = std::collections::HashSet::new();
for f in &api.functions {
if !f.is_async || f.binding_excluded {
continue;
}
collect_async_vec_named(&f.return_type, &mut referenced_async_named);
}
// Note: deliberately do NOT skip `exclude_types` here — the whole point of
// this pass is to catch types whose canonical definition is shadowed by an
// `alef(skip)`-annotated cfg-gated stub (which lands them in `exclude_types`
// via `binding_excluded`). The swift-bridge extern still declares them as
// `type T;` because they appear in a public return signature, and the
// forwarder needs `T` to be Sendable to cross `Task.detached.value`.
for name in referenced_async_named {
if sendable_emitted.insert(name.to_string()) {
emit_sendable_conformance(
&mut body,
name,
None,
&[
"swift-bridge opaque type referenced in async forwarder return — Rust type is Send + Sync.",
"Auto-included even when the IR filter excluded it (e.g. cfg-gated alef(skip) stub).",
],
);
}
}
}
// swift-format-ignore-file tells swift-format to skip this file entirely.
// Generated Swift code violates several formatting rules (long lines,
// trailing commas in function signatures, etc.) that cannot be trivially
// fixed without a full reformatter post-processing step.
let imports = imports.iter().cloned().collect::<Vec<_>>().join("\n");
let mut content = crate::backends::swift::template_env::render(
"swift_module_header.swift.jinja",
minijinja::context! {
imports => imports,
},
);
content.push_str(&body);
let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
let base_path = PathBuf::from(&base_dir);
// Explicit `[crates.output] swift = "..."` is treated as the final
// package directory: the user controls layout. Without an override the
// backend constructs the canonical SwiftPM `Sources/<Module>/` layout
// under the template-derived base.
let path = if config.explicit_output.swift.is_some() {
base_path.join(format!("{module_name}.swift"))
} else {
base_path
.join("Sources")
.join(&module_name)
.join(format!("{module_name}.swift"))
};
let mut files = vec![GeneratedFile {
path,
content,
generated_header: false,
}];
// Phase 2C: emit the Rust-side swift-bridge crate
let rust_crate_files = gen_rust_crate::emit(original_api, config)?;
files.extend(rust_crate_files);
// Phase 2D: if the swift-bridge build output exists in target/*/out/, emit the
// properly prefixed bridge files into Sources/RustBridge{,C}/.
// These files are generated by `cargo build -p {crate}-swift` via
// swift_bridge_build and must be present in the SwiftPM package for it to compile.
let binding_crate_name = format!("{}-swift", &api.crate_name);
let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
let package_root = PathBuf::from(&base_dir)
.ancestors()
// walk up until we find Sources/ sibling, or stop at root
.find(|p| p.join("Sources").is_dir())
.map(|p| p.to_path_buf())
// fallback: use the configured base dir stripped of Sources/<Module>
.unwrap_or_else(|| {
// base_dir is e.g. "packages/swift/Sources/SampleLlm"
// go up two levels to get "packages/swift"
PathBuf::from(&base_dir)
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("packages/swift"))
});
if let Some(bridge_files) =
bridge_artifacts::emit_swift_bridge_files(&api.crate_name, &binding_crate_name, &package_root)?
{
files.extend(bridge_files);
}
// Emit Swift{Trait}Box.swift into Sources/RustBridge/ for every OptionsField bridge.
// This class is referenced by the @_cdecl shims generated into that same directory
// by swift-bridge, so it MUST live in the RustBridge module, not the host module.
let rust_bridge_sources = package_root.join("Sources").join("RustBridge");
for box_file in boxes::emit_inbound_box_files(api, config, &rust_bridge_sources) {
files.push(box_file);
}
// Emit Swift{Trait}Box.swift and ZSwiftPluginHelpers.swift for every FunctionParam bridge.
for box_file in boxes::emit_function_param_box_files(api, config, &rust_bridge_sources, &exclude_types) {
files.push(box_file);
}
// Emit class triples for opaque handle types marked with #[swift_bridge(already_declared)]
// in the Rust extern blocks. These are primarily streaming adapter owner types that need
// Swift class wrappers since swift-bridge skips generation for already_declared types.
if let Some(opaque_file) = opaque_handles::emit_opaque_class_declarations(config, &rust_bridge_sources) {
files.push(opaque_file);
}
// Emit trait bridge protocol and adapter files for outbound plugins.
// Swift{Trait}Bridge.swift files are emitted into Sources/RustBridge/ so that Box classes
// in the same target can reference the bridge protocols.
let trait_bridge_configs: Vec<(String, &TraitBridgeConfig, &TypeDef)> = config
.trait_bridges
.iter()
.filter_map(|b| {
api.types
.iter()
.find(|t| t.is_trait && t.name == b.trait_name)
.map(|t| (b.trait_name.clone(), b, t))
})
.collect();
let module_dir = if config.explicit_output.swift.is_some() {
base_path.clone()
} else {
base_path.join("Sources").join(&module_name)
};
for (filename, content) in trait_bridge::gen_trait_bridge_files(&trait_bridge_configs, &exclude_types) {
// Trait bridge protocol files (Swift{Trait}Bridge.swift and SwiftPluginBridge.swift)
// go into the RustBridge target so they are accessible from Box classes in the same target.
let path = rust_bridge_sources.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: false,
});
}
// Emit ref-property extensions (property-access aliases for zero-param string-returning methods).
// The extension filters via method.binding_excluded to skip methods that swift-bridge
// silently drops (e.g. signatures with non-FFI-friendly returns like SocketAddr).
if let Some((filename, content)) = bridge_artifacts::emit_ref_property_extensions(api) {
let path = module_dir.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: true,
});
}
// Emit bridge registration overloads file (register/unregister convenience functions + stub adapters).
if let Some((filename, content)) = trait_bridge::gen_bridge_registration_overloads_file(&trait_bridge_configs) {
let path = module_dir.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: false,
});
}
Ok(files)
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
service_api::generate(api, config)
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "swift",
crate_suffix: "-swift",
build_dep: BuildDependency::None,
// Build the Rust bridge crate first so swift-bridge codegen produces
// the Swift glue files that the Swift Package consumes.
post_build: vec![PostBuildStep::RunCommand {
cmd: "cargo",
args: vec!["build", "--release"],
}],
})
}
}