alef 0.66.0

Opinionated polyglot binding generator for Rust libraries
Documentation
// Generated by alef — raw `dart:ffi` backend (Phase 3).
// (version `template_versions::pub_dev::FFI_PACKAGE`) instead of

mod error_helpers;
mod functions;
mod type_map;
mod types;

use crate::codegen::c_consumer;
use crate::core::backend::GeneratedFile;
use crate::core::config::{ResolvedCrateConfig, resolve_output_dir};
use crate::core::ir::ApiSurface;
use std::path::PathBuf;

use error_helpers::emit_error_helpers;
use functions::emit_function;
use type_map::dart_module_name;
use types::{emit_enum, emit_type};

/// Emit `dart:ffi` Dart source files for the given API surface.
///
/// Returns two generated files:
///
/// 1. `packages/dart/lib/src/<module>_ffi.dart` — the full `dart:ffi` implementation.
/// 2. `packages/dart/lib/src/<module>.dart` — a thin re-export wrapper so the
///    public API surface is the same regardless of bridging mode.
pub(crate) fn emit(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    let module_name = dart_module_name(&config.name);
    let prefix = config.ffi_prefix();
    let lib_name = config.ffi_lib_name();

    let free_symbol = c_consumer::free_string_symbol(&prefix);
    let error_code_symbol = c_consumer::last_error_code_symbol(&prefix);
    let error_context_symbol = c_consumer::last_error_context_symbol(&prefix);

    let mut content = String::new();
    content.push_str(&crate::backends::dart::template_env::render(
        "dart_ffi_file_header.jinja",
        minijinja::context! {
            module_name => module_name.as_str(),
        },
    ));

    emit_lib_loader(&lib_name, &mut content);

    emit_error_helpers(
        &prefix,
        &free_symbol,
        &error_code_symbol,
        &error_context_symbol,
        &mut content,
    );

    let exclude_functions: std::collections::HashSet<&str> = config
        .dart
        .as_ref()
        .map(|c| c.exclude_functions.iter().map(String::as_str).collect())
        .unwrap_or_default();
    let exclude_types: std::collections::HashSet<&str> = config
        .dart
        .as_ref()
        .map(|c| c.exclude_types.iter().map(String::as_str).collect())
        .unwrap_or_default();

    let dart_capsule_types: std::collections::HashMap<String, crate::core::config::HostCapsuleTypeConfig> = config
        .dart
        .as_ref()
        .map(|c| c.capsule_types.clone())
        .unwrap_or_default();

    for ty in api.types.iter().filter(|t| !exclude_types.contains(t.name.as_str())) {
        emit_type(ty, &mut content);
        content.push('\n');
    }

    for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
        emit_enum(en, &mut content);
        content.push('\n');
    }

    for f in api
        .functions
        .iter()
        .filter(|f| !exclude_functions.contains(f.name.as_str()))
    {
        emit_function(
            f,
            &prefix,
            &free_symbol,
            &error_code_symbol,
            &dart_capsule_types,
            &mut content,
        );
        content.push('\n');
    }

    let dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
    let ffi_path = PathBuf::from(format!("{dir}/{module_name}_ffi.dart"));
    let wrapper_path = PathBuf::from(format!("{dir}/{module_name}.dart"));

    let wrapper_content = crate::backends::dart::template_env::render(
        "dart_ffi_wrapper_file.jinja",
        minijinja::context! {
            module_name => module_name.as_str(),
        },
    );

    // `type_map.rs` / `types.rs` / `functions.rs` hardcode `Pointer<Void>` against the IR's
    // TypeRef kind; no part of `gen_ffi` parses the cbindgen header, so a pointer-vs-`uint64_t`
    // straddle against the FFI crate is undetectable from the Dart side. Only the `_ffi.dart`
    // file is stamped — the wrapper is a pure re-export and names no handle type. Stamped
    // inside the backend body so the marker is part of the content `finalize_hashes` hashes;
    // stamping after `inject_hash_line` would leave the file permanently stale. ~keep
    let content = crate::core::hash::inject_stamp_line(
        &content,
        crate::core::hash::HANDLE_ABI_STAMP_KEY,
        crate::core::template_versions::abi::HANDLE_ABI_VERSION,
    );

    Ok(vec![
        GeneratedFile {
            path: ffi_path,
            content,
            generated_header: false,
        },
        GeneratedFile {
            path: wrapper_path,
            content: wrapper_content,
            generated_header: false,
        },
    ])
}

/// Emit the private `_lib` singleton and `_libraryPath()` helper.
fn emit_lib_loader(lib_name: &str, out: &mut String) {
    out.push_str("/// Private singleton: the loaded native library.\n");
    out.push_str("final DynamicLibrary _lib = DynamicLibrary.open(_libraryPath());\n\n");

    out.push_str("/// Resolve the platform-specific path for the native shared library.\n");
    out.push_str("String _libraryPath() {\n");
    out.push_str("  if (Platform.isMacOS) {\n");
    out.push_str(&crate::backends::dart::template_env::render(
        "ffi_lib_path_macos_return.jinja",
        minijinja::context! {
            lib_name => lib_name,
        },
    ));
    out.push_str("  } else if (Platform.isWindows) {\n");
    out.push_str(&crate::backends::dart::template_env::render(
        "ffi_lib_path_windows_return.jinja",
        minijinja::context! {
            lib_name => lib_name,
        },
    ));
    out.push_str("  } else {\n");
    out.push_str(&crate::backends::dart::template_env::render(
        "ffi_lib_path_linux_return.jinja",
        minijinja::context! {
            lib_name => lib_name,
        },
    ));
    out.push_str("  }\n");
    out.push_str("}\n\n");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lib_loader_contains_platform_branches() {
        let mut out = String::new();
        emit_lib_loader("my_lib", &mut out);
        assert!(out.contains("Platform.isMacOS"), "missing macOS branch");
        assert!(out.contains("Platform.isWindows"), "missing Windows branch");
        assert!(out.contains("libmy_lib.dylib"), "missing dylib");
        assert!(out.contains("my_lib.dll"), "missing dll");
        assert!(out.contains("libmy_lib.so"), "missing so");
    }

    #[test]
    fn error_helpers_reference_correct_symbols() {
        let mut out = String::new();
        emit_error_helpers(
            "mylib",
            "mylib_free_string",
            "mylib_last_error_code",
            "mylib_last_error_context",
            &mut out,
        );
        assert!(out.contains("mylib_free_string"), "missing free_string symbol");
        assert!(out.contains("mylib_last_error_code"), "missing error_code symbol");
        assert!(out.contains("mylib_last_error_context"), "missing error_context symbol");
        assert!(out.contains("_checkError"), "missing _checkError helper");
    }

    #[test]
    fn emitted_ffi_dart_carries_the_handle_abi_stamp() {
        let api = crate::core::ir::ApiSurface {
            crate_name: "sample".to_string(),
            types: vec![crate::core::ir::TypeDef {
                name: "Thing".to_string(),
                rust_path: "sample::Thing".to_string(),
                is_opaque: true,
                ..crate::core::ir::TypeDef::default()
            }],
            ..crate::core::ir::ApiSurface::default()
        };
        let config = crate::core::config::ResolvedCrateConfig {
            name: "sample".to_string(),
            ..crate::core::config::ResolvedCrateConfig::default()
        };

        let files = emit(&api, &config).expect("dart gen_ffi emission");
        let ffi_file = files
            .iter()
            .find(|file| file.path.to_string_lossy().ends_with("_ffi.dart"))
            .expect("generated dart:ffi implementation");

        assert!(
            ffi_file.content.contains("Pointer<Void>"),
            "fixture must actually emit the hardcoded handle type this stamp guards:\n{}",
            ffi_file.content
        );
        crate::backends::ffi::handle_abi_stamp::assert_stamped_before_hashing(&ffi_file.content, "dart _ffi.dart");

        let wrapper = files
            .iter()
            .find(|file| file.path != ffi_file.path)
            .expect("generated re-export wrapper");
        assert_eq!(
            crate::core::hash::extract_stamp(&wrapper.content, crate::core::hash::HANDLE_ABI_STAMP_KEY),
            None,
            "the re-export wrapper names no handle type and must stay unstamped"
        );
    }
}