Skip to main content

alef_core/config/resolved/
ffi.rs

1//! FFI-related methods for `ResolvedCrateConfig`.
2
3use crate::config::KotlinFfiStyle;
4
5use super::ResolvedCrateConfig;
6
7impl ResolvedCrateConfig {
8    /// Get the JNI native library name used by Android JNI Bridge objects.
9    ///
10    /// Returns `<ffi_prefix>_jni`, parallel to [`Self::ffi_lib_name`].
11    /// This is the library name passed to `System.loadLibrary(...)` in the
12    /// emitted Kotlin Bridge object when `KotlinFfiStyle::Jni` is active.
13    pub fn jni_lib_name(&self) -> String {
14        format!("{}_jni", self.ffi_prefix())
15    }
16
17    /// Returns the configured Kotlin FFI emission style.
18    ///
19    /// Reads `[crates.kotlin] ffi_style`; defaults to `KotlinFfiStyle::Panama`.
20    /// The `alef-backend-kotlin-android` backend overrides this to `Jni`
21    /// unconditionally via [`Self::with_kotlin_ffi_style`].
22    pub fn kotlin_ffi_style(&self) -> KotlinFfiStyle {
23        self.kotlin.as_ref().map(|k| k.ffi_style).unwrap_or_default()
24    }
25
26    /// Return a clone of this config with the Kotlin FFI style forced to `style`.
27    ///
28    /// Used by the Android backend so all downstream emitters see `Jni` mode
29    /// regardless of what the user wrote in `[crates.kotlin] ffi_style`.
30    pub fn with_kotlin_ffi_style(mut self, style: KotlinFfiStyle) -> Self {
31        self.kotlin.get_or_insert_with(Default::default).ffi_style = style;
32        self
33    }
34
35    /// Get the FFI prefix (e.g., `"kreuzberg"`). Used by FFI, Go, Java, C# backends.
36    ///
37    /// Returns `[ffi] prefix` if set, otherwise derives from the crate name by
38    /// replacing hyphens with underscores.
39    pub fn ffi_prefix(&self) -> String {
40        self.ffi
41            .as_ref()
42            .and_then(|f| f.prefix.as_ref())
43            .cloned()
44            .unwrap_or_else(|| self.name.replace('-', "_"))
45    }
46
47    /// Get the FFI native library name (for Go cgo, Java Panama, C# P/Invoke).
48    ///
49    /// Resolution order:
50    /// 1. `[ffi] lib_name` explicit override
51    /// 2. Directory name of the user-supplied `[crates.output] ffi` path with
52    ///    hyphens replaced by underscores (e.g. `crates/html-to-markdown-ffi/src/`
53    ///    → `html_to_markdown_ffi`). Walks components from the end and skips
54    ///    `src`/`lib`/`include` to find the crate directory.
55    /// 3. `{ffi_prefix}_ffi` fallback
56    pub fn ffi_lib_name(&self) -> String {
57        // 1. Explicit override in [ffi] section.
58        if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
59            return name.clone();
60        }
61
62        // 2. Derive from the user-supplied `[crates.output] ffi` path. We use
63        //    `explicit_output` (the raw user input) — NOT `output_paths` — so a
64        //    template-derived FFI dir does not accidentally drive the lib name.
65        if let Some(ffi_path) = self.explicit_output.ffi.as_ref() {
66            let crate_dir = ffi_path
67                .components()
68                .filter_map(|c| {
69                    if let std::path::Component::Normal(s) = c {
70                        s.to_str()
71                    } else {
72                        None
73                    }
74                })
75                .rev()
76                .find(|&s| s != "src" && s != "lib" && s != "include");
77            if let Some(dir) = crate_dir {
78                return dir.replace('-', "_");
79            }
80        }
81
82        // 3. Default fallback.
83        format!("{}_ffi", self.ffi_prefix())
84    }
85
86    /// Get the FFI header name.
87    ///
88    /// Returns `[ffi] header_name` if set, otherwise `"{ffi_prefix}.h"`.
89    pub fn ffi_header_name(&self) -> String {
90        self.ffi
91            .as_ref()
92            .and_then(|f| f.header_name.as_ref())
93            .cloned()
94            .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
95    }
96
97    /// Resolve the Rust expression used by FFI plugin shims
98    /// (`plugin_impl_initialize`, `plugin_impl_shutdown`) to construct an
99    /// error value from a runtime `String` named `msg`.
100    ///
101    /// Returns `[ffi] plugin_error_constructor` verbatim when set; otherwise
102    /// `None` so callers can fall back to a generic constructor that doesn't
103    /// depend on a specific error variant shape.
104    pub fn ffi_plugin_error_constructor(&self) -> Option<String> {
105        self.ffi
106            .as_ref()
107            .and_then(|f| f.plugin_error_constructor.as_ref())
108            .cloned()
109    }
110
111    /// Get the relative path to the FFI crate from the e2e test directory.
112    ///
113    /// Used by C e2e tests to locate the compiled FFI library when building
114    /// against a local checkout rather than a downloaded release.
115    ///
116    /// Resolution order:
117    /// 1. Directory name of the user-supplied `[crates.output] ffi` path,
118    ///    skipping trailing `src`/`lib`/`include` components, prefixed with
119    ///    `../../` so the path resolves from `e2e/c/` back to the repo root.
120    ///    E.g. `crates/my-lib-ffi/src/` → `../../crates/my-lib-ffi`.
121    /// 2. `../../crates/{name}-ffi` fallback derived from the crate name.
122    pub fn ffi_crate_path(&self) -> String {
123        if let Some(ffi_path) = self.explicit_output.ffi.as_ref() {
124            // Walk path components from the end, skipping src/lib/include.
125            let components: Vec<&str> = ffi_path
126                .components()
127                .filter_map(|c| {
128                    if let std::path::Component::Normal(s) = c {
129                        s.to_str()
130                    } else {
131                        None
132                    }
133                })
134                .collect();
135            // Find the crate directory component (first non-leaf from the right
136            // after skipping src/lib/include).
137            if let Some(idx) = components
138                .iter()
139                .rposition(|&s| s != "src" && s != "lib" && s != "include")
140            {
141                // Reconstruct the path up to and including this component.
142                let meaningful = &components[..=idx];
143                return format!("../../{}", meaningful.join("/"));
144            }
145        }
146        format!("../../crates/{}-ffi", self.name)
147    }
148
149    /// Get the relative path to the WASM crate's `pkg/` directory from the
150    /// e2e test directory.
151    ///
152    /// Used by WASM e2e tests to import the wasm-pack build output when
153    /// working against a local checkout rather than a published npm package.
154    ///
155    /// Resolution order:
156    /// 1. Directory name of the user-supplied `[crates.output] wasm` path,
157    ///    skipping trailing `src`/`lib`/`include` components, prefixed with
158    ///    `../../` and suffixed with `/pkg`.
159    ///    E.g. `crates/my-lib-wasm/src/` → `../../crates/my-lib-wasm/pkg`.
160    /// 2. `../../crates/{name}-wasm/pkg` fallback derived from the crate name.
161    pub fn wasm_crate_path(&self) -> String {
162        if let Some(wasm_path) = self.explicit_output.wasm.as_ref() {
163            let components: Vec<&str> = wasm_path
164                .components()
165                .filter_map(|c| {
166                    if let std::path::Component::Normal(s) = c {
167                        s.to_str()
168                    } else {
169                        None
170                    }
171                })
172                .collect();
173            if let Some(idx) = components
174                .iter()
175                .rposition(|&s| s != "src" && s != "lib" && s != "include")
176            {
177                let meaningful = &components[..=idx];
178                return format!("../../{}/pkg", meaningful.join("/"));
179            }
180        }
181        format!("../../crates/{}-wasm/pkg", self.name)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::config::new_config::NewAlefConfig;
188
189    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
190        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
191        cfg.resolve().unwrap().remove(0)
192    }
193
194    fn minimal_ffi() -> super::super::ResolvedCrateConfig {
195        resolved_one(
196            r#"
197[workspace]
198languages = ["ffi"]
199
200[[crates]]
201name = "my-lib"
202sources = ["src/lib.rs"]
203"#,
204        )
205    }
206
207    #[test]
208    fn ffi_prefix_defaults_to_snake_case_name() {
209        let r = minimal_ffi();
210        assert_eq!(r.ffi_prefix(), "my_lib");
211    }
212
213    #[test]
214    fn ffi_prefix_explicit_wins() {
215        let r = resolved_one(
216            r#"
217[workspace]
218languages = ["ffi"]
219
220[[crates]]
221name = "my-lib"
222sources = ["src/lib.rs"]
223
224[crates.ffi]
225prefix = "custom_prefix"
226"#,
227        );
228        assert_eq!(r.ffi_prefix(), "custom_prefix");
229    }
230
231    #[test]
232    fn ffi_lib_name_falls_back_to_prefix_ffi() {
233        let r = minimal_ffi();
234        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
235    }
236
237    #[test]
238    fn ffi_lib_name_explicit_wins() {
239        let r = resolved_one(
240            r#"
241[workspace]
242languages = ["ffi"]
243
244[[crates]]
245name = "my-lib"
246sources = ["src/lib.rs"]
247
248[crates.ffi]
249lib_name = "libmy_custom"
250"#,
251        );
252        assert_eq!(r.ffi_lib_name(), "libmy_custom");
253    }
254
255    #[test]
256    fn ffi_header_name_defaults_to_prefix_h() {
257        let r = minimal_ffi();
258        assert_eq!(r.ffi_header_name(), "my_lib.h");
259    }
260
261    #[test]
262    fn ffi_lib_name_derives_from_explicit_output_path() {
263        let r = resolved_one(
264            r#"
265[workspace]
266languages = ["ffi"]
267
268[[crates]]
269name = "my-lib"
270sources = ["src/lib.rs"]
271
272[crates.output]
273ffi = "crates/html-to-markdown-ffi/src/"
274"#,
275        );
276        // Step 2 of resolution: derive from `[crates.output] ffi` path,
277        // skipping `src`/`lib`/`include` and replacing hyphens with underscores.
278        assert_eq!(r.ffi_lib_name(), "html_to_markdown_ffi");
279    }
280
281    #[test]
282    fn ffi_lib_name_explicit_lib_name_overrides_output_path_derivation() {
283        let r = resolved_one(
284            r#"
285[workspace]
286languages = ["ffi"]
287
288[[crates]]
289name = "my-lib"
290sources = ["src/lib.rs"]
291
292[crates.ffi]
293lib_name = "explicit_wins"
294
295[crates.output]
296ffi = "crates/html-to-markdown-ffi/src/"
297"#,
298        );
299        // Step 1 (explicit lib_name) takes precedence over step 2 (output path).
300        assert_eq!(r.ffi_lib_name(), "explicit_wins");
301    }
302
303    #[test]
304    fn ffi_lib_name_template_derived_output_does_not_drive_lib_name() {
305        // No explicit `[crates.output] ffi`. The template-derived path
306        // (e.g. `packages/ffi/my-lib/`) must NOT drive the lib_name —
307        // it falls through to step 3 (`{ffi_prefix}_ffi`).
308        let r = minimal_ffi();
309        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
310    }
311
312    #[test]
313    fn ffi_header_name_explicit_wins() {
314        let r = resolved_one(
315            r#"
316[workspace]
317languages = ["ffi"]
318
319[[crates]]
320name = "my-lib"
321sources = ["src/lib.rs"]
322
323[crates.ffi]
324header_name = "custom.h"
325"#,
326        );
327        assert_eq!(r.ffi_header_name(), "custom.h");
328    }
329}