alef 0.23.35

Opinionated polyglot binding generator for Rust libraries
Documentation
// Generated by alef — raw `dart:ffi` backend (Phase 3).
//
// This module emits Dart source that calls the cbindgen-produced C shared library
// directly via `dart:ffi`. It does NOT emit a Rust crate; the same C library
// consumed by Go / Java / C# / Zig is reused.
//
// When `style = "ffi"` in [dart] config, the scaffold should use `package:ffi`
// (version `template_versions::pub_dev::FFI_PACKAGE`) instead of
// `flutter_rust_bridge`. The scaffold module update is Phase 1b follow-up work.

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(),
        },
    ));

    // Platform-aware library loader.
    emit_lib_loader(&lib_name, &mut content);

    // Error helpers using the standard last-error pattern.
    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();

    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, &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(),
        },
    );

    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");
    }
}