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