Skip to main content

alef_adapters/
sync_function.rs

1use alef_core::config::{AdapterConfig, AlefConfig, Language};
2
3/// Generate just the function body (what goes inside `{ ... }`) for a sync function adapter.
4pub fn generate_body(adapter: &AdapterConfig, language: Language, config: &AlefConfig) -> anyhow::Result<String> {
5    let body = match language {
6        Language::Python => gen_python_body(adapter, config),
7        Language::Node => gen_node_body(adapter, config),
8        Language::Ruby => gen_ruby_body(adapter, config),
9        Language::Php => gen_php_body(adapter, config),
10        Language::Elixir => gen_elixir_body(adapter, config),
11        Language::Wasm => gen_wasm_body(adapter, config),
12        Language::Ffi => gen_ffi_body(adapter, config),
13        Language::Go => gen_go_body(adapter, config),
14        Language::Java => gen_java_body(adapter, config),
15        Language::Csharp => gen_csharp_body(adapter, config),
16        Language::R => gen_r_body(adapter, config),
17        Language::Rust => anyhow::bail!("Rust does not need generated binding adapters"),
18    };
19    Ok(body)
20}
21
22/// Build the call arguments with `.into()` conversion.
23fn call_args(adapter: &AdapterConfig) -> Vec<String> {
24    adapter
25        .params
26        .iter()
27        .map(|p| {
28            if p.optional {
29                format!("{}.map(Into::into)", p.name)
30            } else {
31                format!("{}.into()", p.name)
32            }
33        })
34        .collect()
35}
36
37// ---------------------------------------------------------------------------
38// Python (PyO3)
39// ---------------------------------------------------------------------------
40
41fn gen_python_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
42    let core_path = &adapter.core_path;
43    let returns = adapter.returns.as_deref().unwrap_or("()");
44    let gil_release = adapter.gil_release;
45
46    let args = call_args(adapter);
47    let call_str = args.join(", ");
48
49    if gil_release {
50        format!(
51            "py.allow_threads(|| {{\n        \
52                 {core_path}({call_str})\n            \
53                 .map({returns}::from)\n            \
54                 .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))\n    \
55             }})"
56        )
57    } else {
58        format!(
59            "{core_path}({call_str})\n        \
60             .map({returns}::from)\n        \
61             .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))"
62        )
63    }
64}
65
66// ---------------------------------------------------------------------------
67// Node (NAPI)
68// ---------------------------------------------------------------------------
69
70fn gen_node_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
71    let core_path = &adapter.core_path;
72    let returns = adapter.returns.as_deref().unwrap_or("()");
73
74    let args = call_args(adapter);
75    let call_str = args.join(", ");
76
77    format!(
78        "{core_path}({call_str})\n        \
79         .map({returns}::from)\n        \
80         .map_err(|e| napi::Error::from_reason(e.to_string()))"
81    )
82}
83
84// ---------------------------------------------------------------------------
85// Ruby (Magnus)
86// ---------------------------------------------------------------------------
87
88fn gen_ruby_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
89    let core_path = &adapter.core_path;
90    let returns = adapter.returns.as_deref().unwrap_or("()");
91
92    let args = call_args(adapter);
93    let call_str = args.join(", ");
94
95    format!(
96        "{core_path}({call_str})\n        \
97         .map({returns}::from)\n        \
98         .map_err(|e| magnus::Error::new(magnus::exception::runtime_error(), e.to_string()))"
99    )
100}
101
102// ---------------------------------------------------------------------------
103// PHP (ext-php-rs)
104// ---------------------------------------------------------------------------
105
106fn gen_php_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
107    let core_path = &adapter.core_path;
108    let returns = adapter.returns.as_deref().unwrap_or("()");
109
110    let args = call_args(adapter);
111    let call_str = args.join(", ");
112
113    format!(
114        "{core_path}({call_str})\n        \
115         .map({returns}::from)\n        \
116         .map_err(|e| PhpException::default(e.to_string()))"
117    )
118}
119
120// ---------------------------------------------------------------------------
121// Elixir (Rustler)
122// ---------------------------------------------------------------------------
123
124fn gen_elixir_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
125    let core_path = &adapter.core_path;
126    let returns = adapter.returns.as_deref().unwrap_or("()");
127
128    let args = call_args(adapter);
129    let call_str = args.join(", ");
130
131    format!(
132        "{core_path}({call_str})\n        \
133         .map({returns}::from)\n        \
134         .map_err(|e| e.to_string())"
135    )
136}
137
138// ---------------------------------------------------------------------------
139// WASM (wasm-bindgen)
140// ---------------------------------------------------------------------------
141
142fn gen_wasm_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
143    let core_path = &adapter.core_path;
144    let returns = adapter.returns.as_deref().unwrap_or("JsValue");
145
146    let args = call_args(adapter);
147    let call_str = args.join(", ");
148
149    format!(
150        "{core_path}({call_str})\n        \
151         .map({returns}::from)\n        \
152         .map_err(|e| JsValue::from_str(&e.to_string()))"
153    )
154}
155
156// ---------------------------------------------------------------------------
157// FFI (C ABI)
158// ---------------------------------------------------------------------------
159
160fn gen_ffi_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
161    let core_path = &adapter.core_path;
162    let prefix = config.ffi_prefix();
163    let name = &adapter.name;
164    let _ = prefix;
165    let _ = name;
166
167    let conversions: Vec<String> = adapter
168        .params
169        .iter()
170        .filter_map(|p| {
171            if p.ty == "String" || p.ty == "&str" {
172                Some(format!(
173                    "let {name} = unsafe {{ std::ffi::CStr::from_ptr({name}) }}\n        \
174                     .to_str()\n        \
175                     .unwrap_or_default()\n        \
176                     .to_owned();",
177                    name = p.name
178                ))
179            } else {
180                None
181            }
182        })
183        .collect();
184
185    let call_args_list: Vec<String> = adapter.params.iter().map(|p| p.name.clone()).collect();
186    let call_str = call_args_list.join(", ");
187    let conversion_block = if conversions.is_empty() {
188        String::new()
189    } else {
190        format!("{}\n", conversions.join("\n"))
191    };
192
193    format!(
194        "{conversion_block}\
195         match {core_path}({call_str}) {{\n        \
196             Ok(result) => {{\n            \
197                 let json = serde_json::to_string(&result).unwrap_or_default();\n            \
198                 std::ffi::CString::new(json).unwrap_or_default().into_raw()\n        \
199             }}\n        \
200             Err(e) => {{\n            \
201                 update_last_error(e);\n            \
202                 std::ptr::null_mut()\n        \
203             }}\n    \
204         }}"
205    )
206}
207
208// ---------------------------------------------------------------------------
209// Go (wraps C FFI)
210// ---------------------------------------------------------------------------
211
212fn gen_go_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
213    let name = &adapter.name;
214    let prefix = config.ffi_prefix();
215    let returns = adapter.returns.as_deref().unwrap_or("string");
216
217    let go_name = to_pascal_case(name);
218
219    let c_call_args: Vec<String> = adapter
220        .params
221        .iter()
222        .map(|p| {
223            if p.ty == "String" || p.ty == "&str" {
224                format!("C.CString({})", p.name)
225            } else {
226                format!("C.{}({})", rust_type_to_c_go(&p.ty), p.name)
227            }
228        })
229        .collect();
230
231    let call_str = c_call_args.join(", ");
232    let _ = go_name;
233
234    format!(
235        "result := C.{prefix}_{name}({call_str})\n    \
236         if result == nil {{\n        \
237             return nil, fmt.Errorf(\"%s\", lastError())\n    \
238         }}\n    \
239         defer C.free(unsafe.Pointer(result))\n    \
240         var out {returns}\n    \
241         if err := json.Unmarshal([]byte(C.GoString(result)), &out); err != nil {{\n        \
242             return nil, err\n    \
243         }}\n    \
244         return &out, nil"
245    )
246}
247
248// ---------------------------------------------------------------------------
249// Java (Panama FFI)
250// ---------------------------------------------------------------------------
251
252fn gen_java_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
253    let name = &adapter.name;
254    let prefix = config.ffi_prefix();
255
256    let arg_pass = if adapter.params.is_empty() {
257        String::new()
258    } else {
259        format!(
260            ", {}",
261            adapter
262                .params
263                .iter()
264                .map(|p| {
265                    if p.ty == "String" || p.ty == "&str" {
266                        format!("arena.allocateFrom({})", p.name)
267                    } else {
268                        p.name.clone()
269                    }
270                })
271                .collect::<Vec<_>>()
272                .join(", ")
273        )
274    };
275
276    format!(
277        "try (var arena = Arena.ofConfined()) {{\n\
278         \x20           var result = (MemorySegment) {prefix}_{name}.invokeExact(arena{arg_pass});\n\
279         \x20           if (result.equals(MemorySegment.NULL)) {{\n\
280         \x20               throw new RuntimeException(lastError());\n\
281         \x20           }}\n\
282         \x20           return result.getString(0);\n\
283         \x20       }}"
284    )
285}
286
287// ---------------------------------------------------------------------------
288// C# (P/Invoke)
289// ---------------------------------------------------------------------------
290
291fn gen_csharp_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
292    let name = &adapter.name;
293    let prefix = config.ffi_prefix();
294
295    let call_args_list: Vec<String> = adapter.params.iter().map(|p| p.name.clone()).collect();
296    let call_str = call_args_list.join(", ");
297
298    format!(
299        "var ptr = {prefix}_{name}_native({call_str});\n\
300         \x20       if (ptr == IntPtr.Zero)\n\
301         \x20           throw new InvalidOperationException(GetLastError());\n\
302         \x20       try {{ return Marshal.PtrToStringUTF8(ptr)!; }}\n\
303         \x20       finally {{ FreeString(ptr); }}"
304    )
305}
306
307// ---------------------------------------------------------------------------
308// R (extendr)
309// ---------------------------------------------------------------------------
310
311fn gen_r_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
312    let core_path = &adapter.core_path;
313    let returns = adapter.returns.as_deref().unwrap_or("Robj");
314
315    let args = call_args(adapter);
316    let call_str = args.join(", ");
317
318    format!(
319        "{core_path}({call_str})\n        \
320         .map({returns}::from)\n        \
321         .map_err(|e| extendr_api::Error::Other(e.to_string()))"
322    )
323}
324
325// ---------------------------------------------------------------------------
326// Helpers
327// ---------------------------------------------------------------------------
328
329fn to_pascal_case(s: &str) -> String {
330    s.split('_')
331        .map(|part| {
332            let mut chars = part.chars();
333            match chars.next() {
334                None => String::new(),
335                Some(first) => first.to_uppercase().to_string() + chars.as_str(),
336            }
337        })
338        .collect()
339}
340
341fn rust_type_to_c_go(ty: &str) -> &str {
342    match ty {
343        "bool" => "int",
344        "i32" => "int",
345        "i64" => "longlong",
346        "u32" => "uint",
347        "u64" => "ulonglong",
348        "f32" => "float",
349        "f64" => "double",
350        _ => "int",
351    }
352}