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