alef 0.25.12

Opinionated polyglot binding generator for Rust libraries
Documentation
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
//! Emits `Cargo.toml` and `build.rs` for the swift-bridge crate.

use crate::codegen::cfg as shared_cfg;
use crate::core::ir::ApiSurface;

/// Formats a features array for TOML output.
/// Uses multi-line format when `features.len() >= 3` or the rendered line exceeds 100 chars.
fn format_features_array(features: &[String]) -> String {
    if features.is_empty() {
        return String::new();
    }

    // Try single-line format first.
    let quoted = features.iter().map(|f| format!("\"{f}\"")).collect::<Vec<_>>();
    let single_line = quoted.join(", ");
    let single_line_full = format!(", features = [{single_line}]");

    // Use multi-line if we have 3+ features or the line would exceed 100 chars.
    if features.len() >= 3 || single_line_full.len() > 100 {
        let mut multi_line = String::from(", features = [\n");
        for feature in &quoted {
            multi_line.push_str("    ");
            multi_line.push_str(feature);
            multi_line.push_str(",\n");
        }
        multi_line.push(']');
        multi_line
    } else {
        single_line_full
    }
}

/// Emit the `Cargo.toml` content for the generated swift crate.
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_cargo_toml(
    crate_name: &str,
    core_dep_key: &str,
    _core_crate_dir: &str,
    version: &str,
    swift_bridge_ver: &str,
    swift_bridge_build_ver: &str,
    core_path: &str,
    features: &[String],
    extra_deps: &str,
    license: &str,
    has_streaming_adapters: bool,
    target_overrides: &[crate::core::config::languages::SwiftTargetDepOverride],
    api: &ApiSurface,
) -> String {
    let source_crate_name = core_dep_key;
    let features_block = if features.is_empty() {
        String::new()
    } else {
        format_features_array(features)
    };
    // When the Rust ident form of the umbrella crate name (`core_dep_key`)
    // differs from the actual cargo package name in the umbrella Cargo.toml
    // (`crate_name`), cargo will not
    // resolve the path dependency unless we add an explicit `package = "..."`
    // rename. Use `crate_name` (the [[crates]] `name` field, which is the
    // cargo package name) rather than `core_crate_dir` (the directory name)
    // because the two can differ.
    let package_rename_block = if core_dep_key != crate_name {
        format!(", package = \"{crate_name}\"")
    } else {
        String::new()
    };
    // Streaming adapter shims use `futures_util::StreamExt`, so the dep is
    // required only when the crate config declares streaming adapters.
    let streaming_deps = if has_streaming_adapters {
        "futures-util = \"0.3\"\n"
    } else {
        ""
    };
    let extra_deps_block = if extra_deps.trim().is_empty() {
        String::new()
    } else {
        format!("{extra_deps}\n")
    };
    // Emit the core-facade dep in dual form (`{ version = "...", path = "..." }`)
    // so in-repo dev path builds keep working while cargo-package flows can
    // strip the path to a registry version-dep. Features + the optional
    // `package = "..."` rename are appended as the inline-table suffix.
    //
    // When `target_overrides` is non-empty, the unconditional core dep is
    // gated on `cfg(not(any(<override cfgs>)))` and each override emits its own
    // `[target.'cfg(...)'.dependencies]` block (similar to the FFI and Dart
    // backends). This lets the Swift crate ship a reduced feature set on iOS,
    // Android, and Windows where libheif-sys / ORT cannot be linked.
    let core_dep_for_block = crate::scaffold::render_core_dep(
        source_crate_name,
        core_path,
        &format!("{features_block}{package_rename_block}"),
        version,
    );
    let target_override_blocks = if target_overrides.is_empty() {
        String::new()
    } else {
        let mut blocks = String::new();
        // Gate the default dep on cfg(not(any(<overrides>))) to keep one and only
        // one branch active per target.
        let neg_cfg = if target_overrides.len() == 1 {
            target_overrides[0].cfg.clone()
        } else {
            let any = target_overrides
                .iter()
                .map(|o| o.cfg.as_str())
                .collect::<Vec<_>>()
                .join(", ");
            format!("any({any})")
        };
        blocks.push_str(&format!(
            "\n[target.'cfg(not({neg_cfg}))'.dependencies]\n{core_dep_for_block}\n"
        ));
        for entry in target_overrides {
            let feat_list = entry
                .features
                .iter()
                .map(|f| format!("\"{f}\""))
                .collect::<Vec<_>>()
                .join(", ");
            let feats_block = if feat_list.is_empty() {
                String::new()
            } else {
                format!(", features = [{feat_list}]")
            };
            let default_block = if entry.default_features {
                String::new()
            } else {
                ", default-features = false".to_string()
            };
            let entry_dep = crate::scaffold::render_core_dep(
                source_crate_name,
                core_path,
                &format!("{feats_block}{default_block}{package_rename_block}"),
                version,
            );
            blocks.push_str(&format!("\n[target.'cfg({})'.dependencies]\n{entry_dep}\n", entry.cfg));
        }
        blocks
    };
    // Build [dependencies] block alphabetically sorted to match cargo-sort.
    // Order: ahash, async-trait, futures-util?, <core-crate>,
    // libc, serde, serde_json, swift-bridge, tokio.
    let mut dep_entries: Vec<String> = vec![
        "ahash = \"0.8\"".to_string(),
        "async-trait = \"0.1\"".to_string(),
        "libc = \"0.2\"".to_string(),
        "serde = { version = \"1\", features = [\"derive\"] }".to_string(),
        "serde_json = \"1\"".to_string(),
        format!("swift-bridge = \"{swift_bridge_ver}\""),
        "tokio = { version = \"1\", features = [\"rt\", \"rt-multi-thread\", \"macros\"] }".to_string(),
    ];
    // Only include the core dep in the unconditional `[dependencies]` block when
    // there are no target overrides — otherwise it lives in the per-target blocks
    // emitted via `target_override_blocks` to avoid double-declaration.
    if !core_dep_for_block.is_empty() && target_overrides.is_empty() {
        dep_entries.push(core_dep_for_block.clone());
    }
    if has_streaming_adapters {
        dep_entries.push("futures-util = \"0.3\"".to_string());
    }
    for line in extra_deps.lines() {
        let trimmed = line.trim_end();
        if !trimmed.is_empty() {
            dep_entries.push(trimmed.to_string());
        }
    }
    dep_entries.sort();
    let dep_block = dep_entries.join("\n");
    let _ = streaming_deps;
    let _ = extra_deps_block;

    // Collect every feature name referenced by a cfg attribute on any type, field,
    // enum variant, or function in the API surface and emit a forwarding `[features]`
    // table so the binding crate can re-export them to the core dep. Without this,
    // `#[cfg(feature = "X")]` arms emitted by the codegen produce
    // `error: unexpected cfg condition value: X` because the binding crate's
    // `Cargo.toml` does not declare that feature.
    let cfg_features = shared_cfg::collect_cfg_features(api);
    let features_table = if cfg_features.is_empty() {
        String::new()
    } else {
        let lines: Vec<String> = cfg_features
            .iter()
            .map(|name| format!(r#"{name} = ["{core_dep_key}/{name}"]"#))
            .collect();
        format!("[features]\n{}\n\n", lines.join("\n"))
    };

    // The [lints.rust] block keeps cfg(frb_expand) in the allow-list (FRB-internal
    // cfg key, not a Cargo feature). Feature values no longer need to be listed
    // here since they are now forwarded through the [features] table above.
    let lints_block = "[lints.rust]\nunexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(frb_expand)'] }";

    format!(
        r#"# Generated by alef. Do not edit by hand.
[package]
name = "{crate_name}-swift"
version = "{version}"
edition = "2024"
license = "{license}"

# `ahash`, `async-trait`, `libc`, `serde`, `serde_json`, and `tokio` are all
# conditionally referenced by alef-emitted code: `ahash` only when the
# umbrella crate exposes `AHashMap<Cow<str>, _>` parameters (the conditional
# `__*_ahash` shim rebuilds), `async-trait` and `tokio` only when the API
# surface includes async streaming adapters and runtime spawn, `libc` only
# when service API C callback functions are emitted, `serde` and
# `serde_json` only when JSON DTO conversions are emitted. They are listed
# unconditionally in `[dependencies]` so the manifest is stable across
# regens, and ignored here so cargo-machete does not flag downstream crates
# whose API surface does not trigger those paths as unused.
[package.metadata.cargo-machete]
ignored = ["ahash", "async-trait", "libc", "serde", "serde_json", "tokio"]

[lib]
crate-type = ["cdylib", "staticlib"]
# The `extern "Swift"` block emits linker references that are only resolvable
# when the crate is linked into a Swift target. `cargo test --workspace` on
# pure-Rust runners (e.g. windows-latest) would otherwise fail with
# undefined `__swift_bridge__$*$alef_visit_*` symbols.
test = false
doctest = false
bench = false

{features_table}[dependencies]
{dep_block}
{target_override_blocks}
{lints_block}

[build-dependencies]
swift-bridge-build = "{swift_bridge_build_ver}"
"#
    )
}

/// Emit the `build.rs` content for the generated swift crate.
pub(crate) fn emit_build_rs() -> String {
    r#"// Generated by alef. Do not edit by hand.
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR unset"));
    let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME unset");
    let bridges = vec!["src/lib.rs"];
    swift_bridge_build::parse_bridges(bridges).write_all_concatenated(out_dir, &crate_name);
    println!("cargo:rerun-if-changed=src/lib.rs");
}
"#
    .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::ir::{ApiSurface, EnumDef, EnumVariant};

    fn make_unit_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
        EnumVariant {
            name: name.to_string(),
            fields: vec![],
            doc: String::new(),
            is_default: false,
            serde_rename: None,
            is_tuple: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            originally_had_data_fields: false,
            cfg: cfg.map(|s| s.to_string()),
            version: Default::default(),
        }
    }

    /// When the API has cfg-gated enum variants the emitted Cargo.toml must declare
    /// a forwarding `[features]` block mapping each referenced feature to the core dep.
    #[test]
    fn cargo_toml_emits_forwarding_features_block_for_cfg_gated_variants() {
        let api = ApiSurface {
            enums: vec![EnumDef {
                name: "ImageOutputFormat".to_string(),
                variants: vec![
                    make_unit_variant("Heif", Some("feature = \"heic\"")),
                    make_unit_variant("Svg", Some("feature = \"svg\"")),
                    make_unit_variant("Jpeg", None),
                ],
                excluded_variants: vec![],
                ..Default::default()
            }],
            ..Default::default()
        };

        let content = emit_cargo_toml(
            "sample-lib",
            "sample_lib",
            "sample-lib",
            "0.1.0",
            "0.1.0",
            "0.1.0",
            "../..",
            &[],
            "",
            "MIT",
            false,
            &[],
            &api,
        );

        assert!(
            content.contains(r#"heic = ["sample_lib/heic"]"#),
            "Cargo.toml must forward `heic` feature to core dep; got:\n{}",
            content
        );
        assert!(
            content.contains(r#"svg = ["sample_lib/svg"]"#),
            "Cargo.toml must forward `svg` feature to core dep; got:\n{}",
            content
        );
        assert!(
            content.contains("[features]"),
            "Cargo.toml must contain a [features] section; got:\n{}",
            content
        );
        // frb_expand must still be declared.
        assert!(
            content.contains("'cfg(frb_expand)'"),
            "Cargo.toml must still include cfg(frb_expand); got:\n{}",
            content
        );
        // No feature values in check-cfg — forwarding replaces the allow-list.
        assert!(
            !content.contains("values("),
            "Cargo.toml must not contain check-cfg values() — forwarding replaces allow-list; got:\n{}",
            content
        );
        toml::from_str::<toml::Value>(&content).expect("generated Cargo.toml must be valid TOML");
    }

    /// When no item has a cfg attribute the `[features]` block must be omitted.
    #[test]
    fn cargo_toml_omits_features_block_when_no_cfg_attrs() {
        let api = ApiSurface {
            enums: vec![EnumDef {
                name: "SimpleEnum".to_string(),
                variants: vec![make_unit_variant("A", None), make_unit_variant("B", None)],
                excluded_variants: vec![],
                ..Default::default()
            }],
            ..Default::default()
        };

        let content = emit_cargo_toml(
            "sample-lib",
            "sample_lib",
            "sample-lib",
            "0.1.0",
            "0.1.0",
            "0.1.0",
            "../..",
            &[],
            "",
            "MIT",
            false,
            &[],
            &api,
        );

        assert!(
            content.contains("'cfg(frb_expand)'"),
            "Cargo.toml must include cfg(frb_expand); got:\n{}",
            content
        );
        assert!(
            !content.contains("[features]"),
            "Cargo.toml must not contain [features] block when no cfg attrs; got:\n{}",
            content
        );
        assert!(
            !content.contains("values("),
            "Cargo.toml must not contain feature values when no cfg attrs; got:\n{}",
            content
        );
        toml::from_str::<toml::Value>(&content).expect("generated Cargo.toml must be valid TOML");
    }

    /// cfg-gated types (not just variants) must also appear in the forwarding block.
    #[test]
    fn cargo_toml_forwarding_covers_type_level_cfg_attrs() {
        use crate::core::ir::TypeDef;

        let api = ApiSurface {
            types: vec![TypeDef {
                name: "PdfDoc".to_string(),
                rust_path: "mylib::PdfDoc".to_string(),
                cfg: Some(r#"feature = "pdf""#.to_string()),
                ..Default::default()
            }],
            ..Default::default()
        };

        let content = emit_cargo_toml(
            "sample-lib",
            "sample_lib",
            "sample-lib",
            "0.1.0",
            "0.1.0",
            "0.1.0",
            "../..",
            &[],
            "",
            "MIT",
            false,
            &[],
            &api,
        );

        assert!(
            content.contains(r#"pdf = ["sample_lib/pdf"]"#),
            "Cargo.toml must forward `pdf` feature from type-level cfg; got:\n{}",
            content
        );
        toml::from_str::<toml::Value>(&content).expect("generated Cargo.toml must be valid TOML");
    }
}