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}