braid_http_rs 0.1.5

Unified Braid Protocol implementation in Rust, including Braid-HTTP, Antimatter CRDT, and BraidFS.
Documentation
use crate::antimatter::json_crdt::{JsonCrdt, JsonPatch};
use crate::antimatter::messages::{Message, Patch};
use crate::antimatter::AntimatterCrdt;
use crate::core::client::BraidClient;
use crate::core::traits::NativeRuntime;
use crate::core::types::{BraidRequest, Update};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::sync::Arc;

/// Opaque handle to an Antimatter instance.
pub struct BraidAntimatter {
    inner: AntimatterCrdt<JsonCrdt>,
}

/// Callback type for outgoing messages.
/// # Arguments
/// * `instance` - The user-provided context pointer.
/// * `json_ptr` - Pointer to a null-terminated JSON string representing the message.
pub type BraidCallback = extern "C" fn(user_data: *mut c_void, json_ptr: *const c_char);

struct FfiCallbackContext {
    callback: BraidCallback,
    user_data: *mut c_void,
}

unsafe impl Send for FfiCallbackContext {}
unsafe impl Sync for FfiCallbackContext {}

#[no_mangle]
pub extern "C" fn braid_antimatter_new(
    id: *const c_char,
    initial_text: *const c_char,
    callback: BraidCallback,
    user_data: *mut c_void,
) -> *mut BraidAntimatter {
    let id_str = if id.is_null() {
        None
    } else {
        unsafe { CStr::from_ptr(id).to_str().ok().map(|s| s.to_string()) }
    };

    let text_str = if initial_text.is_null() {
        ""
    } else {
        unsafe { CStr::from_ptr(initial_text).to_str().unwrap_or("") }
    };

    let ctx = Arc::new(FfiCallbackContext {
        callback,
        user_data,
    });

    let send_cb = move |msg: Message| {
        if let Ok(json) = serde_json::to_string(&msg) {
            if let Ok(c_str) = CString::new(json) {
                (ctx.callback)(ctx.user_data, c_str.as_ptr());
            }
        }
    };

    let runtime = Arc::new(NativeRuntime);
    let crdt = JsonCrdt::with_content(&id_str.clone().unwrap_or_default(), text_str);
    let inner = AntimatterCrdt::new(id_str, crdt, Arc::new(send_cb), runtime);

    Box::into_raw(Box::new(BraidAntimatter { inner }))
}

#[no_mangle]
pub extern "C" fn braid_antimatter_free(instance: *mut BraidAntimatter) {
    if !instance.is_null() {
        unsafe {
            let _ = Box::from_raw(instance);
        }
    }
}

#[no_mangle]
pub extern "C" fn braid_antimatter_receive(
    instance: *mut BraidAntimatter,
    json_msg: *const c_char,
) -> *mut c_char {
    if instance.is_null() || json_msg.is_null() {
        return std::ptr::null_mut();
    }

    let inst = unsafe { &mut *instance };
    let msg_str = unsafe { CStr::from_ptr(json_msg).to_str().unwrap_or("") };

    let msg: Message = match serde_json::from_str(msg_str) {
        Ok(m) => m,
        Err(_) => return std::ptr::null_mut(),
    };

    let result = match inst.inner.receive(msg) {
        Ok(patches) => patches,
        Err(_) => return std::ptr::null_mut(),
    };

    let json_res = serde_json::to_string(&result).unwrap_or_else(|_| "[]".to_string());
    CString::new(json_res)
        .map(|s| s.into_raw())
        .unwrap_or(std::ptr::null_mut())
}

#[no_mangle]
pub extern "C" fn braid_antimatter_update(
    instance: *mut BraidAntimatter,
    json_patches: *const c_char,
) -> *mut c_char {
    if instance.is_null() || json_patches.is_null() {
        return std::ptr::null_mut();
    }

    let inst = unsafe { &mut *instance };
    let patches_str = unsafe { CStr::from_ptr(json_patches).to_str().unwrap_or("") };

    let patches: Vec<Patch> = match serde_json::from_str(patches_str) {
        Ok(p) => p,
        Err(_) => return std::ptr::null_mut(),
    };

    let version = inst.inner.update(patches);
    CString::new(version)
        .map(|s| s.into_raw())
        .unwrap_or(std::ptr::null_mut())
}

#[no_mangle]
pub extern "C" fn braid_antimatter_get_content(instance: *mut BraidAntimatter) -> *mut c_char {
    if instance.is_null() {
        return std::ptr::null_mut();
    }

    let inst = unsafe { &*instance };
    let content = inst.inner.crdt.get_content();
    CString::new(content)
        .map(|s| s.into_raw())
        .unwrap_or(std::ptr::null_mut())
}

#[no_mangle]
pub extern "C" fn braid_antimatter_subscribe(instance: *mut BraidAntimatter, conn: *const c_char) {
    if instance.is_null() || conn.is_null() {
        return;
    }
    let inst = unsafe { &mut *instance };
    if let Ok(conn_str) = unsafe { CStr::from_ptr(conn).to_str() } {
        inst.inner.subscribe(conn_str.to_string());
    }
}

#[no_mangle]
pub extern "C" fn braid_antimatter_disconnect(
    instance: *mut BraidAntimatter,
    conn: *const c_char,
    create_fissure: bool,
) {
    if instance.is_null() || conn.is_null() {
        return;
    }
    let inst = unsafe { &mut *instance };
    if let Ok(conn_str) = unsafe { CStr::from_ptr(conn).to_str() } {
        inst.inner.disconnect(conn_str.to_string(), create_fissure);
    }
}

#[no_mangle]
pub extern "C" fn braid_antimatter_ackme(instance: *mut BraidAntimatter) -> *mut c_char {
    if instance.is_null() {
        return std::ptr::null_mut();
    }
    let inst = unsafe { &mut *instance };
    let ackme_id = inst.inner.ackme();
    CString::new(ackme_id)
        .map(|s| s.into_raw())
        .unwrap_or(std::ptr::null_mut())
}

#[no_mangle]
pub extern "C" fn braid_string_free(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe {
            let _ = CString::from_raw(ptr);
        }
    }
}

/// Opaque handle to a BraidClient instance.
pub struct BraidClientFfi {
    inner: BraidClient,
}

#[no_mangle]
pub extern "C" fn braid_client_new() -> *mut BraidClientFfi {
    match BraidClient::new() {
        Ok(inner) => Box::into_raw(Box::new(BraidClientFfi { inner })),
        Err(_) => std::ptr::null_mut(),
    }
}

#[no_mangle]
pub extern "C" fn braid_client_free(client: *mut BraidClientFfi) {
    if !client.is_null() {
        unsafe {
            let _ = Box::from_raw(client);
        }
    }
}

/// Handle for an active subscription.
pub struct BraidSubscriptionFfi {
    cancel_tx: tokio::sync::oneshot::Sender<()>,
}

#[no_mangle]
pub extern "C" fn braid_client_subscribe(
    client: *mut BraidClientFfi,
    url: *const c_char,
    callback: BraidCallback,
    user_data: *mut c_void,
) -> *mut BraidSubscriptionFfi {
    if client.is_null() || url.is_null() {
        return std::ptr::null_mut();
    }

    let cli = unsafe { &*client };
    let url_str = unsafe { CStr::from_ptr(url).to_str().unwrap_or("").to_string() };

    let (cancel_tx, mut cancel_rx) = tokio::sync::oneshot::channel::<()>();
    let ctx = Arc::new(FfiCallbackContext {
        callback,
        user_data,
    });

    let inner_client = cli.inner.clone();

    tokio::spawn(async move {
        let mut sub = match inner_client
            .subscribe(&url_str, BraidRequest::new().subscribe())
            .await
        {
            Ok(s) => s,
            Err(_) => return,
        };

        loop {
            tokio::select! {
                result = sub.next() => {
                    match result {
                        Some(res) => {
                            if let Ok(update) = res {
                                if let Ok(json) = serde_json::to_string(&update) {
                                    if let Ok(c_str) = CString::new(json) {
                                        (ctx.callback)(ctx.user_data, c_str.as_ptr());
                                    }
                                }
                            }
                        }
                        None => break,
                    }
                }
                _ = &mut cancel_rx => break,
            }
        }
    });

    Box::into_raw(Box::new(BraidSubscriptionFfi { cancel_tx }))
}

#[no_mangle]
pub extern "C" fn braid_subscription_free(sub: *mut BraidSubscriptionFfi) {
    if !sub.is_null() {
        unsafe {
            let s = Box::from_raw(sub);
            let _ = s.cancel_tx.send(());
        }
    }
}