alef 0.68.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::super::FfiBackend;
use super::common::{resolved_one, sample_api, sample_config};
use crate::core::backend::Backend;
use crate::core::ir::{ApiSurface, FieldDef, FunctionDef, MethodDef, TypeDef, TypeRef};

fn generated_lib(api: &ApiSurface) -> String {
    let content = FfiBackend
        .generate_bindings(api, &sample_config())
        .unwrap()
        .into_iter()
        .find(|file| file.path.ends_with("lib.rs"))
        .unwrap()
        .content;
    syn::parse_file(&content).expect("generated FFI runtime must be valid Rust");
    content
}

fn generated_export_block<'a>(content: &'a str, function_name: &str) -> &'a str {
    let signature = format!("fn {function_name}(");
    let signature_start = content.find(&signature).unwrap();
    let block_start = content[..signature_start]
        .rfind("\n\n")
        .map_or(signature_start, |offset| offset + 2);
    let tail = &content[block_start..];
    let block_end = tail.find("\n}\n").map_or(tail.len(), |offset| offset + 3);
    &tail[..block_end]
}

#[test]
fn string_return_lengths_are_isolated_by_export() {
    let api = ApiSurface {
        crate_name: "sample-lib".to_string(),
        version: "1.0.0".to_string(),
        functions: vec![
            FunctionDef {
                name: "short_value".to_string(),
                rust_path: "sample_lib::short_value".to_string(),
                return_type: TypeRef::String,
                ..FunctionDef::default()
            },
            FunctionDef {
                name: "long_value".to_string(),
                rust_path: "sample_lib::long_value".to_string(),
                return_type: TypeRef::String,
                ..FunctionDef::default()
            },
        ],
        ..ApiSurface::default()
    };
    let lib = generated_lib(&api);

    assert!(lib.contains("LAST_RETURN_LENGTHS"));
    assert!(!lib.contains("static LAST_RETURN_LEN:"));
    assert!(lib.contains("set_last_return_len(\"my_lib_short_value\", 0);"));
    assert!(lib.contains("set_last_return_len(\"my_lib_long_value\", 0);"));
    assert!(lib.contains("last_return_len(\"my_lib_short_value\")"));
    assert!(lib.contains("last_return_len(\"my_lib_long_value\")"));
    let short_export = generated_export_block(&lib, "my_lib_short_value");
    assert!(
        short_export.find("catch_unwind").unwrap() < short_export.find("set_last_return_len").unwrap(),
        "return-length storage may allocate and must remain inside the panic boundary"
    );
}

#[test]
fn feature_gated_opaque_lifecycle_exports_are_symmetric() {
    let gated_type = TypeDef {
        name: "DownloadManager".to_string(),
        rust_path: "sample_lib::DownloadManager".to_string(),
        methods: vec![MethodDef {
            name: "new".to_string(),
            return_type: TypeRef::Named("DownloadManager".to_string()),
            is_static: true,
            ..MethodDef::default()
        }],
        is_opaque: true,
        cfg: Some(r#"feature = "download""#.to_string()),
        ..TypeDef::default()
    };
    let api = ApiSurface {
        crate_name: "sample-lib".to_string(),
        version: "1.0.0".to_string(),
        types: vec![gated_type],
        ..ApiSurface::default()
    };
    let lib = generated_lib(&api);

    let constructor = generated_export_block(&lib, "my_lib_download_manager_new");
    let destructor = generated_export_block(&lib, "my_lib_download_manager_free");
    assert!(constructor.contains("#[cfg(feature = \"download\")]"));
    assert!(destructor.contains("#[cfg(feature = \"download\")]"));

    let config = resolved_one(
        r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "sample-lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "sample"
"#,
    );
    let files = FfiBackend.generate_bindings(&api, &config).unwrap();
    let cbindgen = files.iter().find(|file| file.path.ends_with("cbindgen.toml")).unwrap();
    // Unquoted value: cbindgen's `[defines]` key matcher splits on `=` and only
    // trims whitespace, so a quoted value here never matches the unquoted
    // `cfg_value` cbindgen reads from `#[cfg(feature = "download")]` via
    // `LitStr::value()` — see the note in `cbindgen_feature_defines`. ~keep
    assert!(
        cbindgen
            .content
            .contains(r#""feature = download" = "SAMPLE_FEATURE_DOWNLOAD""#)
    );
}

#[test]
fn field_accessor_reports_conversion_errors_and_documents_ownership() {
    let lib = generated_lib(&sample_api());
    let accessor = generated_export_block(&lib, "my_lib_config_name");

    assert!(accessor.contains("catch_ffi_panic"));
    assert!(accessor.contains("set_last_error(ALEF_FFI_CONVERSION_ERROR"));
    assert!(accessor.contains("FFI field value contains an interior NUL byte"));
    assert!(accessor.contains("A non-null returned pointer is owned by the caller."));
    assert!(accessor.contains("It must be freed with `my_lib_free_string`."));

    let named_api = ApiSurface {
        crate_name: "sample-lib".to_string(),
        version: "1.0.0".to_string(),
        types: vec![
            TypeDef {
                name: "Metrics".to_string(),
                rust_path: "sample_lib::Metrics".to_string(),
                is_clone: true,
                ..TypeDef::default()
            },
            TypeDef {
                name: "ProcessResult".to_string(),
                rust_path: "sample_lib::ProcessResult".to_string(),
                fields: vec![FieldDef {
                    name: "metrics".to_string(),
                    ty: TypeRef::Named("Metrics".to_string()),
                    ..FieldDef::default()
                }],
                ..TypeDef::default()
            },
        ],
        ..ApiSurface::default()
    };
    let named_lib = generated_lib(&named_api);
    let named_accessor = generated_export_block(&named_lib, "my_lib_process_result_metrics");
    assert!(named_accessor.contains("A non-null returned handle is owned by the caller."));
    assert!(named_accessor.contains("It must be freed with `my_lib_metrics_free`."));
}