Skip to main content

alef_codegen/c_consumer/
mod.rs

1//! Shared C-FFI consumer scaffolding for language backends.
2//!
3//! This module provides utilities for generating language bindings that consume
4//! the C FFI layer produced by cbindgen. Each consumer backend (Go, Java, C#, Zig)
5//! uses the same C interface:
6//! - A C header file (`config.ffi_header_name()`)
7//! - A library name (`config.ffi_lib_name()`)
8//! - A symbol prefix (`config.ffi_prefix()`)
9//! - Standard helper symbols: `{prefix}_free_string`, `{prefix}_last_error_code`, `{prefix}_last_error_context`
10
11use alef_core::config::{ResolvedCrateConfig, resolve_output_dir};
12use std::path::PathBuf;
13
14/// Context capturing the shared FFI consumer inputs across all language backends.
15pub struct CConsumerContext<'a> {
16    /// Reference to the resolved crate configuration.
17    pub config: &'a ResolvedCrateConfig,
18    /// C header filename (e.g., "html_to_markdown.h").
19    pub header: String,
20    /// C library name used for linking (e.g., "html_to_markdown").
21    pub lib_name: String,
22    /// C symbol prefix for FFI functions (e.g., "htm").
23    pub prefix: String,
24}
25
26impl<'a> CConsumerContext<'a> {
27    /// Create a new CConsumerContext from the resolved crate configuration.
28    pub fn from_config(config: &'a ResolvedCrateConfig) -> Self {
29        Self {
30            config,
31            header: config.ffi_header_name(),
32            lib_name: config.ffi_lib_name(),
33            prefix: config.ffi_prefix(),
34        }
35    }
36}
37
38/// Return the C symbol name for freeing FFI-allocated strings.
39///
40/// Format: `{prefix}_free_string`
41///
42/// # Example
43/// ```ignore
44/// let sym = free_string_symbol("htm");
45/// assert_eq!(sym, "htm_free_string");
46/// ```
47pub fn free_string_symbol(prefix: &str) -> String {
48    format!("{prefix}_free_string")
49}
50
51/// Return the C symbol name for reading the thread-local last error code.
52///
53/// Format: `{prefix}_last_error_code`
54///
55/// # Example
56/// ```ignore
57/// let sym = last_error_code_symbol("krz");
58/// assert_eq!(sym, "krz_last_error_code");
59/// ```
60pub fn last_error_code_symbol(prefix: &str) -> String {
61    format!("{prefix}_last_error_code")
62}
63
64/// Return the C symbol name for reading the thread-local last error context message.
65///
66/// Format: `{prefix}_last_error_context`
67///
68/// # Example
69/// ```ignore
70/// let sym = last_error_context_symbol("krz");
71/// assert_eq!(sym, "krz_last_error_context");
72/// ```
73pub fn last_error_context_symbol(prefix: &str) -> String {
74    format!("{prefix}_last_error_context")
75}
76
77/// Resolve the per-backend output directory for generated files.
78///
79/// This helper wraps `resolve_output_dir` with a sensible default for C-FFI consumers,
80/// allowing backends to pass a language-specific default (e.g., "packages/go/", "packages/java/src/main/java/").
81///
82/// # Arguments
83/// - `config`: The Alef configuration.
84/// - `default`: The backend-specific default output directory (e.g., "packages/go/").
85///
86/// # Returns
87/// A PathBuf representing the resolved output directory.
88pub fn default_output_dir(config: &ResolvedCrateConfig, default: &str) -> PathBuf {
89    let resolved = resolve_output_dir(None, &config.name, default);
90    PathBuf::from(resolved)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use alef_core::config::NewAlefConfig;
97
98    fn make_config() -> ResolvedCrateConfig {
99        let cfg: NewAlefConfig = toml::from_str(
100            r#"
101[workspace]
102languages = ["python"]
103
104[[crates]]
105name = "my-lib"
106sources = ["src/lib.rs"]
107"#,
108        )
109        .unwrap();
110        cfg.resolve().unwrap().remove(0)
111    }
112
113    #[test]
114    fn free_string_symbol_produces_expected_format() {
115        assert_eq!(free_string_symbol("htm"), "htm_free_string");
116    }
117
118    #[test]
119    fn last_error_code_symbol_produces_expected_format() {
120        assert_eq!(last_error_code_symbol("krz"), "krz_last_error_code");
121    }
122
123    #[test]
124    fn last_error_context_symbol_produces_expected_format() {
125        assert_eq!(last_error_context_symbol("krz"), "krz_last_error_context");
126    }
127
128    #[test]
129    fn from_config_reads_ffi_fields() {
130        let config = make_config();
131        let ctx = CConsumerContext::from_config(&config);
132        assert!(!ctx.header.is_empty());
133        assert!(!ctx.lib_name.is_empty());
134        assert!(!ctx.prefix.is_empty());
135    }
136
137    #[test]
138    fn default_output_dir_uses_provided_default() {
139        let config = make_config();
140        let dir = default_output_dir(&config, "packages/go/");
141        // The result should include "packages/go/" as the default.
142        assert!(dir.to_string_lossy().contains("go"));
143    }
144}