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}