Skip to main content

weaveffi_core/
utils.rs

1//! Shared text helpers for the generators: C symbol naming, the standard
2//! generated-file prelude/trailer, and cross-module type-name resolution.
3//!
4//! These are the small string-building routines every backend reaches for
5//! when stamping the "generated by WeaveFFI" banner onto a file or turning a
6//! qualified `module.Type` reference into its flattened C symbol. Centralizing
7//! them keeps the banner, the symbol-prefixing rule, and the dotted-path
8//! flattening identical across all of the language generators.
9
10/// Build the C symbol name for a function: `<prefix>_<module>_<func>`.
11///
12/// `prefix` is the configured ABI symbol prefix (default `"weaveffi"`).
13/// Every backend must route user-symbol construction through this (or the
14/// equivalent [`crate::model::BindingModel`] fields) so a non-default
15/// `c_prefix` is honored consistently across all eleven languages, not just
16/// the C and C++ headers.
17pub fn c_symbol_name(prefix: &str, module: &str, func: &str) -> String {
18    format!("{prefix}_{module}_{func}")
19}
20
21/// Comment syntax used to emit the standard prelude/trailer in generated files.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CommentStyle {
24    /// `// ...` line comments (C, C++, Swift, Kotlin, JS/TS, C#, Dart, Go).
25    DoubleSlash,
26    /// `# ...` line comments (Python, Ruby, YAML, TOML, CMake, GYP, gradle.properties).
27    Hash,
28    /// `<!-- ... -->` block comments (XML, HTML, Markdown).
29    Xml,
30}
31
32impl CommentStyle {
33    fn open(self) -> &'static str {
34        match self {
35            Self::DoubleSlash => "// ",
36            Self::Hash => "# ",
37            Self::Xml => "<!-- ",
38        }
39    }
40
41    fn close(self) -> &'static str {
42        match self {
43            Self::Xml => " -->",
44            _ => "",
45        }
46    }
47}
48
49/// Renders the standard `Generated by WeaveFFI {VERSION} from {input}` prelude
50/// followed by the `DO NOT EDIT` warning and a regenerate command. Trailing
51/// blank line included so generators can append their content directly.
52pub fn render_prelude(style: CommentStyle, input_basename: &str) -> String {
53    let version = env!("CARGO_PKG_VERSION");
54    let o = style.open();
55    let c = style.close();
56    format!(
57        "{o}Generated by WeaveFFI {version} from {input_basename}{c}\n\
58         {o}DO NOT EDIT. Your changes will be overwritten.{c}\n\
59         {o}To regenerate: weaveffi generate {input_basename} -o <out>{c}\n\n"
60    )
61}
62
63/// Renders the closing `END {filename}` marker. Caller is responsible for any
64/// preceding newline; the returned string ends with `\n`.
65pub fn render_trailer(style: CommentStyle, filename: &str) -> String {
66    let o = style.open();
67    let c = style.close();
68    format!("{o}END {filename}{c}\n")
69}
70
71/// Renders the JSON-friendly prelude as `"//"` key/value pairs (recognised by
72/// npm). Each line is two-space-indented and comma-terminated so it can be
73/// embedded at the top of any JSON object literal that opens with `{` on the
74/// previous line.
75pub fn render_json_prelude(input_basename: &str) -> String {
76    let version = env!("CARGO_PKG_VERSION");
77    format!(
78        "  \"//\": \"Generated by WeaveFFI {version} from {input_basename}\",\n  \
79         \"//warning\": \"DO NOT EDIT. Your changes will be overwritten.\",\n  \
80         \"//regenerate\": \"To regenerate: weaveffi generate {input_basename} -o <out>\",\n"
81    )
82}
83
84/// Runtime symbols (functions and types) that consumer code links against from
85/// the `weaveffi-abi` runtime: the `weaveffi_error`/`weaveffi_handle_t`/
86/// `weaveffi_cancel_token` types and every `#[no_mangle]` entry point that
87/// `weaveffi_abi::export_runtime!` emits into the consumer cdylib.
88///
89/// Generators that emit C/C++ headers use this list to produce
90/// `#define {prefix}_{name} weaveffi_{name}` aliases at the top of the header
91/// when a non-default `c_prefix` is configured, so consumer code can refer to
92/// runtime helpers by the prefixed name while still linking against the
93/// canonical `weaveffi_*` symbols supplied by `weaveffi-abi`.
94///
95/// This list must stay in lockstep with `export_runtime!`: every entry has to
96/// be a real exported C symbol (or a public C type), otherwise the generated
97/// `#define` would alias a name that does not exist. In particular `error_set`
98/// is intentionally absent: it is a Rust-only `pub fn` taking `&str`, never a
99/// C ABI symbol.
100pub const ABI_RUNTIME_SYMBOLS: &[&str] = &[
101    "error",
102    "handle_t",
103    "error_clear",
104    "free_string",
105    "free_bytes",
106    "arena_create",
107    "arena_destroy",
108    "arena_register",
109    "cancel_token",
110    "cancel_token_create",
111    "cancel_token_cancel",
112    "cancel_token_is_cancelled",
113    "cancel_token_destroy",
114];
115
116/// Render a `#define {prefix}_{name} weaveffi_{name}` block for runtime ABI
117/// symbols. Returns an empty string when `prefix == "weaveffi"`.
118pub fn render_abi_prefix_aliases(prefix: &str) -> String {
119    if prefix == "weaveffi" {
120        return String::new();
121    }
122    let mut out = String::new();
123    out.push_str("/* Aliases for weaveffi-abi runtime symbols */\n");
124    for sym in ABI_RUNTIME_SYMBOLS {
125        out.push_str(&format!("#define {prefix}_{sym} weaveffi_{sym}\n"));
126    }
127    out.push('\n');
128    out
129}
130
131/// Build the wrapper function name exposed to the foreign language.
132///
133/// When `strip_module_prefix` is `true`, returns just `func`.
134/// When `false`, returns `{module}_{func}`.
135pub fn wrapper_name(module: &str, func: &str, strip_module_prefix: bool) -> String {
136    if strip_module_prefix {
137        func.to_string()
138    } else {
139        format!("{module}_{func}")
140    }
141}
142
143/// Extract the local type name from a potentially qualified `module.TypeName`.
144///
145/// Uses `rsplit_once` so that *multi-level* module paths keep working: only the
146/// final dotted segment is the type name.
147///
148/// `"other.Contact"` → `"Contact"`, `"a.b.Widget"` → `"Widget"`,
149/// `"Contact"` → `"Contact"`.
150pub fn local_type_name(name: &str) -> &str {
151    name.rsplit_once('.').map_or(name, |(_, local)| local)
152}
153
154/// Build the C ABI struct name, resolving cross-module qualified references.
155///
156/// Qualified references use dot-separated module paths; the C ABI flattens
157/// those to underscore-joined symbol prefixes, so `rsplit_once` peels off the
158/// type name and the remaining dotted path becomes underscores.
159///
160/// `"other.Contact"` with any current module → `"{prefix}_other_Contact"`.
161/// `"a.b.Widget"` with any current module → `"{prefix}_a_b_Widget"`.
162/// `"Contact"` with current module `"math"` → `"{prefix}_math_Contact"`.
163pub fn c_abi_struct_name(name: &str, current_module: &str, prefix: &str) -> String {
164    if let Some((module_path, type_name)) = name.rsplit_once('.') {
165        let module_path = module_path.replace('.', "_");
166        format!("{prefix}_{module_path}_{type_name}")
167    } else {
168        format!("{prefix}_{current_module}_{name}")
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn c_symbol_name_uses_prefix() {
178        assert_eq!(
179            c_symbol_name("weaveffi", "calc", "add"),
180            "weaveffi_calc_add"
181        );
182        assert_eq!(c_symbol_name("myffi", "calc", "add"), "myffi_calc_add");
183    }
184
185    #[test]
186    fn local_type_name_unqualified() {
187        assert_eq!(local_type_name("Contact"), "Contact");
188    }
189
190    #[test]
191    fn local_type_name_qualified() {
192        assert_eq!(local_type_name("other.Contact"), "Contact");
193    }
194
195    #[test]
196    fn local_type_name_multi_level() {
197        assert_eq!(local_type_name("a.b.Widget"), "Widget");
198    }
199
200    #[test]
201    fn c_abi_struct_name_unqualified() {
202        assert_eq!(
203            c_abi_struct_name("Contact", "math", "weaveffi"),
204            "weaveffi_math_Contact"
205        );
206    }
207
208    #[test]
209    fn c_abi_struct_name_qualified() {
210        assert_eq!(
211            c_abi_struct_name("types.Name", "ops", "weaveffi"),
212            "weaveffi_types_Name"
213        );
214    }
215
216    #[test]
217    fn c_abi_struct_name_multi_level_flattens_path() {
218        assert_eq!(
219            c_abi_struct_name("a.b.Widget", "ops", "weaveffi"),
220            "weaveffi_a_b_Widget"
221        );
222    }
223
224    #[test]
225    fn abi_prefix_aliases_default_is_empty() {
226        assert!(render_abi_prefix_aliases("weaveffi").is_empty());
227    }
228
229    #[test]
230    fn abi_prefix_aliases_custom_lists_every_symbol() {
231        let out = render_abi_prefix_aliases("myffi");
232        for sym in ABI_RUNTIME_SYMBOLS {
233            let line = format!("#define myffi_{sym} weaveffi_{sym}");
234            assert!(out.contains(&line), "missing alias `{line}` in:\n{out}");
235        }
236    }
237
238    #[test]
239    fn prelude_double_slash_carries_required_phrases() {
240        let p = render_prelude(CommentStyle::DoubleSlash, "calc.yml");
241        assert!(p.starts_with("// Generated by WeaveFFI "));
242        assert!(p.contains(" from calc.yml\n"));
243        assert!(p.contains("// DO NOT EDIT"));
244        assert!(p.contains("// To regenerate: weaveffi generate calc.yml -o <out>"));
245        assert!(p.ends_with("\n\n"));
246    }
247
248    #[test]
249    fn prelude_hash_uses_hash_marker() {
250        let p = render_prelude(CommentStyle::Hash, "calc.yml");
251        assert!(p.starts_with("# Generated by WeaveFFI "));
252        assert!(p.contains("# DO NOT EDIT"));
253        assert!(p.contains("# To regenerate: weaveffi generate calc.yml -o <out>"));
254    }
255
256    #[test]
257    fn prelude_xml_wraps_lines_in_brackets() {
258        let p = render_prelude(CommentStyle::Xml, "calc.yml");
259        assert!(p.starts_with("<!-- Generated by WeaveFFI "));
260        assert!(p.contains("from calc.yml -->"));
261        assert!(p.contains("<!-- DO NOT EDIT"));
262        assert!(p.contains("Your changes will be overwritten. -->"));
263    }
264
265    #[test]
266    fn trailer_has_correct_marker_per_style() {
267        assert_eq!(
268            render_trailer(CommentStyle::DoubleSlash, "lib.rs"),
269            "// END lib.rs\n"
270        );
271        assert_eq!(
272            render_trailer(CommentStyle::Hash, "build.toml"),
273            "# END build.toml\n"
274        );
275        assert_eq!(
276            render_trailer(CommentStyle::Xml, "package.csproj"),
277            "<!-- END package.csproj -->\n"
278        );
279    }
280
281    #[test]
282    fn json_prelude_contains_required_phrases_in_first_lines() {
283        let p = render_json_prelude("calc.yml");
284        let lines: Vec<&str> = p.lines().collect();
285        assert!(lines[0].contains("Generated by WeaveFFI"));
286        assert!(lines[0].contains("from calc.yml"));
287        assert!(lines[1].contains("DO NOT EDIT"));
288        assert!(lines[2].contains("To regenerate"));
289        for line in &lines {
290            assert!(line.starts_with("  "));
291            assert!(line.ends_with(','));
292        }
293    }
294}