Skip to main content

seqc/ffi/
manifest.rs

1//! FFI manifest parsing and type mapping: the TOML-driven schema plus the
2//! embedded manifest lookup (currently just `libedit`).
3
4use serde::Deserialize;
5
6use crate::types::{Effect, StackType, Type};
7
8/// FFI type mapping for C interop
9#[derive(Debug, Clone, Deserialize, PartialEq)]
10#[serde(rename_all = "snake_case")]
11pub enum FfiType {
12    /// C int/long mapped to Seq Int (i64)
13    Int,
14    /// C char* mapped to Seq String
15    String,
16    /// C void* as raw pointer (represented as Int)
17    Ptr,
18    /// C void - no return value
19    Void,
20}
21
22/// Argument passing mode
23#[derive(Debug, Clone, Deserialize, PartialEq)]
24#[serde(rename_all = "snake_case")]
25pub enum PassMode {
26    /// Convert Seq String to null-terminated char*
27    CString,
28    /// Pass raw pointer value
29    Ptr,
30    /// Pass as C integer
31    Int,
32    /// Pass pointer to value (for out parameters)
33    ByRef,
34}
35
36/// Memory ownership annotation for return values
37#[derive(Debug, Clone, Deserialize, PartialEq)]
38#[serde(rename_all = "snake_case")]
39pub enum Ownership {
40    /// C function allocated memory, caller must free
41    CallerFrees,
42    /// Library owns the memory, don't free
43    Static,
44    /// Valid only during call, copy immediately
45    Borrowed,
46}
47
48/// An argument to an FFI function
49#[derive(Debug, Clone, Deserialize)]
50pub struct FfiArg {
51    /// The type of the argument
52    #[serde(rename = "type")]
53    pub arg_type: FfiType,
54    /// How to pass the argument to C
55    #[serde(default = "default_pass_mode")]
56    pub pass: PassMode,
57    /// Fixed value (for parameters like NULL callbacks)
58    pub value: Option<String>,
59}
60
61fn default_pass_mode() -> PassMode {
62    PassMode::CString
63}
64
65/// Return value specification
66#[derive(Debug, Clone, Deserialize)]
67pub struct FfiReturn {
68    /// The type of the return value
69    #[serde(rename = "type")]
70    pub return_type: FfiType,
71    /// Memory ownership
72    #[serde(default = "default_ownership")]
73    pub ownership: Ownership,
74}
75
76fn default_ownership() -> Ownership {
77    Ownership::Borrowed
78}
79
80/// A function binding in an FFI manifest
81#[derive(Debug, Clone, Deserialize)]
82pub struct FfiFunction {
83    /// C function name (e.g., "sqlite3_open")
84    pub c_name: String,
85    /// Seq word name (e.g., "db-open")
86    pub seq_name: String,
87    /// Stack effect annotation (e.g., "( String -- String )")
88    pub stack_effect: String,
89    /// Function arguments
90    #[serde(default)]
91    pub args: Vec<FfiArg>,
92    /// Return value specification
93    #[serde(rename = "return")]
94    pub return_spec: Option<FfiReturn>,
95}
96
97/// A library binding in an FFI manifest
98#[derive(Debug, Clone, Deserialize)]
99pub struct FfiLibrary {
100    /// Library name for reference
101    pub name: String,
102    /// Linker flag (e.g., "sqlite3" for -lsqlite3)
103    pub link: String,
104    /// Function bindings
105    #[serde(rename = "function", default)]
106    pub functions: Vec<FfiFunction>,
107}
108
109/// Top-level FFI manifest structure
110#[derive(Debug, Clone, Deserialize)]
111pub struct FfiManifest {
112    /// Library definitions (usually just one per manifest)
113    #[serde(rename = "library")]
114    pub libraries: Vec<FfiLibrary>,
115}
116
117impl FfiManifest {
118    /// Parse an FFI manifest from TOML content
119    ///
120    /// Validates the manifest after parsing to catch:
121    /// - Empty library names or linker flags
122    /// - Empty function names (c_name or seq_name)
123    /// - Malformed stack effects
124    pub fn parse(content: &str) -> Result<Self, String> {
125        let manifest: Self =
126            toml::from_str(content).map_err(|e| format!("Failed to parse FFI manifest: {}", e))?;
127        manifest.validate()?;
128        Ok(manifest)
129    }
130
131    /// Validate the manifest for common errors
132    pub(super) fn validate(&self) -> Result<(), String> {
133        if self.libraries.is_empty() {
134            return Err("FFI manifest must define at least one library".to_string());
135        }
136
137        for (lib_idx, lib) in self.libraries.iter().enumerate() {
138            // Validate library name
139            if lib.name.trim().is_empty() {
140                return Err(format!("FFI library {} has empty name", lib_idx + 1));
141            }
142
143            // Validate linker flag (security: prevent injection of arbitrary flags)
144            if lib.link.trim().is_empty() {
145                return Err(format!("FFI library '{}' has empty linker flag", lib.name));
146            }
147            // Only allow safe characters in linker flag: alphanumeric, dash, underscore, dot
148            for c in lib.link.chars() {
149                if !c.is_alphanumeric() && c != '-' && c != '_' && c != '.' {
150                    return Err(format!(
151                        "FFI library '{}' has invalid character '{}' in linker flag '{}'. \
152                         Only alphanumeric, dash, underscore, and dot are allowed.",
153                        lib.name, c, lib.link
154                    ));
155                }
156            }
157
158            // Validate each function
159            for (func_idx, func) in lib.functions.iter().enumerate() {
160                // Validate c_name
161                if func.c_name.trim().is_empty() {
162                    return Err(format!(
163                        "FFI function {} in library '{}' has empty c_name",
164                        func_idx + 1,
165                        lib.name
166                    ));
167                }
168
169                // Validate seq_name
170                if func.seq_name.trim().is_empty() {
171                    return Err(format!(
172                        "FFI function '{}' in library '{}' has empty seq_name",
173                        func.c_name, lib.name
174                    ));
175                }
176
177                // Validate stack_effect is not empty
178                if func.stack_effect.trim().is_empty() {
179                    return Err(format!(
180                        "FFI function '{}' has empty stack_effect",
181                        func.seq_name
182                    ));
183                }
184
185                // Validate stack_effect parses correctly
186                if let Err(e) = func.effect() {
187                    return Err(format!(
188                        "FFI function '{}' has malformed stack_effect '{}': {}",
189                        func.seq_name, func.stack_effect, e
190                    ));
191                }
192            }
193        }
194
195        Ok(())
196    }
197
198    /// Get all linker flags needed for this manifest
199    pub fn linker_flags(&self) -> Vec<String> {
200        self.libraries.iter().map(|lib| lib.link.clone()).collect()
201    }
202
203    /// Get all function bindings from this manifest
204    pub fn functions(&self) -> impl Iterator<Item = &FfiFunction> {
205        self.libraries.iter().flat_map(|lib| lib.functions.iter())
206    }
207}
208
209impl FfiFunction {
210    /// Parse the stack effect string into an Effect
211    pub fn effect(&self) -> Result<Effect, String> {
212        parse_stack_effect(&self.stack_effect)
213    }
214}
215
216/// Parse a stack effect string like "( String -- String )" into an Effect
217pub(super) fn parse_stack_effect(s: &str) -> Result<Effect, String> {
218    // Strip parentheses and trim
219    let s = s.trim();
220    let s = s
221        .strip_prefix('(')
222        .ok_or("Stack effect must start with '('")?;
223    let s = s
224        .strip_suffix(')')
225        .ok_or("Stack effect must end with ')'")?;
226    let s = s.trim();
227
228    // Split on "--"
229    let parts: Vec<&str> = s.split("--").collect();
230    if parts.len() != 2 {
231        return Err(format!(
232            "Stack effect must contain exactly one '--', got: {}",
233            s
234        ));
235    }
236
237    let inputs_str = parts[0].trim();
238    let outputs_str = parts[1].trim();
239
240    // Parse input types
241    let mut inputs = StackType::RowVar("a".to_string());
242    for type_name in inputs_str.split_whitespace() {
243        let ty = parse_type_name(type_name)?;
244        inputs = inputs.push(ty);
245    }
246
247    // Parse output types
248    let mut outputs = StackType::RowVar("a".to_string());
249    for type_name in outputs_str.split_whitespace() {
250        let ty = parse_type_name(type_name)?;
251        outputs = outputs.push(ty);
252    }
253
254    Ok(Effect::new(inputs, outputs))
255}
256
257/// Parse a type name string into a Type
258pub(super) fn parse_type_name(name: &str) -> Result<Type, String> {
259    match name {
260        "Int" => Ok(Type::Int),
261        "Float" => Ok(Type::Float),
262        "Bool" => Ok(Type::Bool),
263        "String" => Ok(Type::String),
264        _ => Err(format!("Unknown type '{}' in stack effect", name)),
265    }
266}
267
268// ============================================================================
269// Embedded FFI Manifests
270// ============================================================================
271
272/// Embedded libedit FFI manifest (BSD-licensed)
273pub const LIBEDIT_MANIFEST: &str = include_str!("../../ffi/libedit.toml");
274
275/// Get an embedded FFI manifest by name
276pub fn get_ffi_manifest(name: &str) -> Option<&'static str> {
277    match name {
278        "libedit" => Some(LIBEDIT_MANIFEST),
279        _ => None,
280    }
281}
282
283/// Check if an FFI manifest exists
284pub fn has_ffi_manifest(name: &str) -> bool {
285    get_ffi_manifest(name).is_some()
286}
287
288/// List all available embedded FFI manifests
289pub fn list_ffi_manifests() -> &'static [&'static str] {
290    &["libedit"]
291}