supabase/
ffi.rs

1//! Enhanced C FFI (Foreign Function Interface) bindings
2//!
3//! This module provides comprehensive C-compatible functions for all Supabase features,
4//! allowing the library to be used from other programming languages like C, C++, Python, Go, etc.
5//!
6//! # Features
7//!
8//! - Full API coverage: Auth, Database, Storage, Functions, Realtime
9//! - Async-to-sync bridge for FFI consumers
10//! - Safe memory management with leak prevention
11//! - Comprehensive error handling with detailed context
12//! - Thread-safe operations
13//!
14//! # Safety
15//!
16//! All FFI functions are marked as `unsafe` and require careful handling of
17//! memory management and string encoding.
18//!
19//! # Usage
20//!
21//! ```c
22//! #include "include/supabase.h"
23//!
24//! int main() {
25//!     SupabaseClient* client = supabase_client_new("https://example.supabase.co", "your-key");
26//!
27//!     char result[1024];
28//!     SupabaseError error = supabase_auth_sign_in(client, "email@example.com", "password", result, sizeof(result));
29//!
30//!     if (error == SUPABASE_SUCCESS) {
31//!         printf("Success: %s\n", result);
32//!     }
33//!
34//!     supabase_client_free(client);
35//!     return 0;
36//! }
37//! ```
38
39use std::ffi::{CStr, CString};
40use std::os::raw::c_char;
41use std::ptr;
42use std::sync::Mutex;
43
44use crate::{Client, Error};
45
46/// Thread-safe error storage for FFI
47static ERROR_STORAGE: Mutex<Option<String>> = Mutex::new(None);
48
49/// Opaque handle to a Supabase client with runtime
50pub struct SupabaseClient {
51    client: Client,
52    runtime: tokio::runtime::Runtime,
53}
54
55/// Enhanced C-compatible error codes
56#[repr(C)]
57#[derive(Debug, Clone, Copy)]
58pub enum SupabaseError {
59    Success = 0,
60    InvalidInput = 1,
61    NetworkError = 2,
62    AuthError = 3,
63    DatabaseError = 4,
64    StorageError = 5,
65    FunctionsError = 6,
66    RealtimeError = 7,
67    RuntimeError = 8,
68    UnknownError = 99,
69}
70
71impl From<Error> for SupabaseError {
72    fn from(err: Error) -> Self {
73        // Store detailed error message
74        if let Ok(mut storage) = ERROR_STORAGE.lock() {
75            *storage = Some(format!("{}", err));
76        }
77
78        match err {
79            Error::InvalidInput { .. } => SupabaseError::InvalidInput,
80            Error::Network { .. } => SupabaseError::NetworkError,
81            Error::Auth { .. } => SupabaseError::AuthError,
82            Error::Database { .. } => SupabaseError::DatabaseError,
83            Error::Storage { .. } => SupabaseError::StorageError,
84            Error::Functions { .. } => SupabaseError::FunctionsError,
85            Error::Realtime { .. } => SupabaseError::RealtimeError,
86            Error::Platform { .. } | Error::Crypto { .. } => SupabaseError::RuntimeError,
87            _ => SupabaseError::UnknownError,
88        }
89    }
90}
91
92/// Create a new Supabase client with async runtime
93///
94/// # Safety
95///
96/// `url` and `key` must be valid C strings
97/// Returns NULL on error
98#[no_mangle]
99pub unsafe extern "C" fn supabase_client_new(
100    url: *const c_char,
101    key: *const c_char,
102) -> *mut SupabaseClient {
103    if url.is_null() || key.is_null() {
104        return ptr::null_mut();
105    }
106
107    let url_str = match CStr::from_ptr(url).to_str() {
108        Ok(s) => s,
109        Err(_) => return ptr::null_mut(),
110    };
111
112    let key_str = match CStr::from_ptr(key).to_str() {
113        Ok(s) => s,
114        Err(_) => return ptr::null_mut(),
115    };
116
117    // Create tokio runtime for async operations
118    let runtime = match tokio::runtime::Runtime::new() {
119        Ok(rt) => rt,
120        Err(_) => return ptr::null_mut(),
121    };
122
123    match Client::new(url_str, key_str) {
124        Ok(client) => Box::into_raw(Box::new(SupabaseClient { client, runtime })),
125        Err(_) => ptr::null_mut(),
126    }
127}
128
129/// Free a Supabase client
130///
131/// # Safety
132///
133/// `client` must be a valid pointer returned by `supabase_client_new`
134#[no_mangle]
135pub unsafe extern "C" fn supabase_client_free(client: *mut SupabaseClient) {
136    if !client.is_null() {
137        let _ = Box::from_raw(client);
138    }
139}
140
141/// Sign in with email and password
142///
143/// # Safety
144///
145/// All parameters must be valid pointers
146/// `result` buffer should be at least 1024 bytes
147#[no_mangle]
148pub unsafe extern "C" fn supabase_auth_sign_in(
149    client: *mut SupabaseClient,
150    email: *const c_char,
151    password: *const c_char,
152    result: *mut c_char,
153    result_len: usize,
154) -> SupabaseError {
155    if client.is_null() || email.is_null() || password.is_null() || result.is_null() {
156        return SupabaseError::InvalidInput;
157    }
158
159    let client_ref = &(*client);
160
161    let email_str = match CStr::from_ptr(email).to_str() {
162        Ok(s) => s,
163        Err(_) => return SupabaseError::InvalidInput,
164    };
165
166    let password_str = match CStr::from_ptr(password).to_str() {
167        Ok(s) => s,
168        Err(_) => return SupabaseError::InvalidInput,
169    };
170
171    // Execute async operation in runtime
172    let auth_result = client_ref.runtime.block_on(async {
173        client_ref
174            .client
175            .auth()
176            .sign_in_with_email_and_password(email_str, password_str)
177            .await
178    });
179
180    match auth_result {
181        Ok(session) => {
182            let response = match serde_json::to_string(&session) {
183                Ok(json) => json,
184                Err(_) => return SupabaseError::UnknownError,
185            };
186
187            write_string_to_buffer(&response, result, result_len)
188        }
189        Err(err) => err.into(),
190    }
191}
192
193/// Sign up with email and password
194///
195/// # Safety
196///
197/// All parameters must be valid pointers
198#[no_mangle]
199pub unsafe extern "C" fn supabase_auth_sign_up(
200    client: *mut SupabaseClient,
201    email: *const c_char,
202    password: *const c_char,
203    result: *mut c_char,
204    result_len: usize,
205) -> SupabaseError {
206    if client.is_null() || email.is_null() || password.is_null() || result.is_null() {
207        return SupabaseError::InvalidInput;
208    }
209
210    let client_ref = &(*client);
211
212    let email_str = match CStr::from_ptr(email).to_str() {
213        Ok(s) => s,
214        Err(_) => return SupabaseError::InvalidInput,
215    };
216
217    let password_str = match CStr::from_ptr(password).to_str() {
218        Ok(s) => s,
219        Err(_) => return SupabaseError::InvalidInput,
220    };
221
222    let auth_result = client_ref.runtime.block_on(async {
223        client_ref
224            .client
225            .auth()
226            .sign_up_with_email_and_password(email_str, password_str)
227            .await
228    });
229
230    match auth_result {
231        Ok(session) => {
232            let response = match serde_json::to_string(&session) {
233                Ok(json) => json,
234                Err(_) => return SupabaseError::UnknownError,
235            };
236
237            write_string_to_buffer(&response, result, result_len)
238        }
239        Err(err) => err.into(),
240    }
241}
242
243/// Execute a database select query
244///
245/// # Safety
246///
247/// All parameters must be valid pointers
248#[no_mangle]
249pub unsafe extern "C" fn supabase_database_select(
250    client: *mut SupabaseClient,
251    table: *const c_char,
252    columns: *const c_char,
253    result: *mut c_char,
254    result_len: usize,
255) -> SupabaseError {
256    if client.is_null() || table.is_null() || result.is_null() {
257        return SupabaseError::InvalidInput;
258    }
259
260    let client_ref = &(*client);
261
262    let table_str = match CStr::from_ptr(table).to_str() {
263        Ok(s) => s,
264        Err(_) => return SupabaseError::InvalidInput,
265    };
266
267    let columns_str = if columns.is_null() {
268        "*"
269    } else {
270        match CStr::from_ptr(columns).to_str() {
271            Ok(s) => s,
272            Err(_) => return SupabaseError::InvalidInput,
273        }
274    };
275
276    let db_result = client_ref.runtime.block_on(async {
277        let result: Result<Vec<serde_json::Value>, Error> = client_ref
278            .client
279            .database()
280            .from(table_str)
281            .select(columns_str)
282            .execute()
283            .await;
284        result.map(|data| serde_json::to_string(&data).unwrap_or_default())
285    });
286
287    match db_result {
288        Ok(data) => write_string_to_buffer(&data, result, result_len),
289        Err(err) => err.into(),
290    }
291}
292
293/// Execute a database insert operation
294///
295/// # Safety
296///
297/// All parameters must be valid pointers
298/// `json_data` must be valid JSON string
299#[no_mangle]
300pub unsafe extern "C" fn supabase_database_insert(
301    client: *mut SupabaseClient,
302    table: *const c_char,
303    json_data: *const c_char,
304    result: *mut c_char,
305    result_len: usize,
306) -> SupabaseError {
307    if client.is_null() || table.is_null() || json_data.is_null() || result.is_null() {
308        return SupabaseError::InvalidInput;
309    }
310
311    let client_ref = &(*client);
312
313    let table_str = match CStr::from_ptr(table).to_str() {
314        Ok(s) => s,
315        Err(_) => return SupabaseError::InvalidInput,
316    };
317
318    let json_str = match CStr::from_ptr(json_data).to_str() {
319        Ok(s) => s,
320        Err(_) => return SupabaseError::InvalidInput,
321    };
322
323    let json_value: serde_json::Value = match serde_json::from_str(json_str) {
324        Ok(v) => v,
325        Err(_) => return SupabaseError::InvalidInput,
326    };
327
328    let db_result = client_ref.runtime.block_on(async {
329        let result = client_ref
330            .client
331            .database()
332            .insert(table_str)
333            .values(json_value)?
334            .execute::<serde_json::Value>()
335            .await;
336
337        match result {
338            Ok(data) => Ok(serde_json::to_string(&data).unwrap_or_default()),
339            Err(err) => Err(err),
340        }
341    });
342
343    match db_result {
344        Ok(data) => write_string_to_buffer(&data, result, result_len),
345        Err(err) => err.into(),
346    }
347}
348
349/// List storage buckets
350///
351/// # Safety
352///
353/// All parameters must be valid pointers
354#[no_mangle]
355pub unsafe extern "C" fn supabase_storage_list_buckets(
356    client: *mut SupabaseClient,
357    result: *mut c_char,
358    result_len: usize,
359) -> SupabaseError {
360    if client.is_null() || result.is_null() {
361        return SupabaseError::InvalidInput;
362    }
363
364    let client_ref = &(*client);
365
366    let storage_result = client_ref
367        .runtime
368        .block_on(async { client_ref.client.storage().list_buckets().await });
369
370    match storage_result {
371        Ok(buckets) => {
372            let response = match serde_json::to_string(&buckets) {
373                Ok(json) => json,
374                Err(_) => return SupabaseError::UnknownError,
375            };
376            write_string_to_buffer(&response, result, result_len)
377        }
378        Err(err) => err.into(),
379    }
380}
381
382/// Invoke an edge function
383///
384/// # Safety
385///
386/// All parameters must be valid pointers
387#[no_mangle]
388pub unsafe extern "C" fn supabase_functions_invoke(
389    client: *mut SupabaseClient,
390    function_name: *const c_char,
391    json_payload: *const c_char,
392    result: *mut c_char,
393    result_len: usize,
394) -> SupabaseError {
395    if client.is_null() || function_name.is_null() || result.is_null() {
396        return SupabaseError::InvalidInput;
397    }
398
399    let client_ref = &(*client);
400
401    let function_str = match CStr::from_ptr(function_name).to_str() {
402        Ok(s) => s,
403        Err(_) => return SupabaseError::InvalidInput,
404    };
405
406    let payload = if json_payload.is_null() {
407        None
408    } else {
409        match CStr::from_ptr(json_payload).to_str() {
410            Ok(s) => match serde_json::from_str::<serde_json::Value>(s) {
411                Ok(v) => Some(v),
412                Err(_) => return SupabaseError::InvalidInput,
413            },
414            Err(_) => return SupabaseError::InvalidInput,
415        }
416    };
417
418    let function_result = client_ref.runtime.block_on(async {
419        client_ref
420            .client
421            .functions()
422            .invoke(function_str, payload)
423            .await
424    });
425
426    match function_result {
427        Ok(response) => {
428            let response_str = match response {
429                serde_json::Value::String(s) => s,
430                other => serde_json::to_string(&other).unwrap_or_default(),
431            };
432            write_string_to_buffer(&response_str, result, result_len)
433        }
434        Err(err) => err.into(),
435    }
436}
437
438/// Get the last error message
439///
440/// # Safety
441///
442/// `buffer` must be a valid pointer with at least `buffer_len` bytes
443#[no_mangle]
444pub unsafe extern "C" fn supabase_get_last_error(
445    buffer: *mut c_char,
446    buffer_len: usize,
447) -> SupabaseError {
448    if buffer.is_null() || buffer_len == 0 {
449        return SupabaseError::InvalidInput;
450    }
451
452    let error_msg = if let Ok(storage) = ERROR_STORAGE.lock() {
453        storage
454            .as_ref()
455            .cloned()
456            .unwrap_or_else(|| "No error".to_string())
457    } else {
458        "Failed to access error storage".to_string()
459    };
460
461    write_string_to_buffer(&error_msg, buffer, buffer_len)
462}
463
464/// Helper function to write string to C buffer
465unsafe fn write_string_to_buffer(
466    data: &str,
467    buffer: *mut c_char,
468    buffer_len: usize,
469) -> SupabaseError {
470    let data_cstring = match CString::new(data) {
471        Ok(s) => s,
472        Err(_) => return SupabaseError::UnknownError,
473    };
474
475    let data_bytes = data_cstring.as_bytes_with_nul();
476    if data_bytes.len() > buffer_len {
477        return SupabaseError::InvalidInput;
478    }
479
480    ptr::copy_nonoverlapping(data_bytes.as_ptr(), buffer as *mut u8, data_bytes.len());
481
482    SupabaseError::Success
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use std::ffi::CString;
489
490    #[test]
491    fn test_client_creation() {
492        let url = CString::new("http://localhost:54321").unwrap();
493        let key = CString::new("test-key").unwrap();
494
495        unsafe {
496            let client = supabase_client_new(url.as_ptr(), key.as_ptr());
497            assert!(!client.is_null());
498
499            supabase_client_free(client);
500        }
501    }
502
503    #[test]
504    fn test_error_handling() {
505        unsafe {
506            let client = supabase_client_new(ptr::null(), ptr::null());
507            assert!(client.is_null());
508        }
509    }
510
511    #[test]
512    fn test_error_storage() {
513        let mut buffer = [0u8; 256];
514        unsafe {
515            let result = supabase_get_last_error(buffer.as_mut_ptr() as *mut c_char, buffer.len());
516            assert_eq!(result as i32, SupabaseError::Success as i32);
517        }
518    }
519}