| new file mode 100644 |
| index 0000000..5d8927c |
| --- /dev/null |
| +++ b/core/src/abi.rs |
| @@ -0,0 +1,141 @@ |
| +//! The C ABI the Go host calls through wazero. No wasm-bindgen: wazero |
| +//! provides no JS glue, so everything crosses as integers and linear-memory |
| +//! offsets. |
| +//! |
| +//! Handles are indices into a slab rather than raw pointers, so a stale |
| +//! handle from the host is a null lookup instead of a wild dereference. |
| + |
| +use crate::{apply_update, text_content, CoreError}; |
| +use std::cell::RefCell; |
| +use std::collections::HashMap; |
| +use yrs::{Doc, Options}; |
| + |
| +thread_local! { |
| + static DOCS: RefCell<HashMap<u32, Doc>> = RefCell::new(HashMap::new()); |
| + static NEXT: RefCell<u32> = const { RefCell::new(1) }; |
| +} |
| + |
| +pub const ERR_NO_DOC: i32 = -1; |
| +pub const ERR_DECODE: i32 = -2; |
| +pub const ERR_APPLY: i32 = -3; |
| +pub const ERR_BUFFER_TOO_SMALL: i32 = -4; |
| + |
| +pub fn doc_new(guid: &str, client_id: u64) -> u32 { |
| + let id = NEXT.with(|n| { |
| + let mut n = n.borrow_mut(); |
| + let id = *n; |
| + *n += 1; |
| + id |
| + }); |
| + let doc = Doc::with_options(Options::with_guid_and_client_id(guid.into(), client_id)); |
| + DOCS.with(|d| d.borrow_mut().insert(id, doc)); |
| + id |
| +} |
| + |
| +pub fn doc_free(handle: u32) { |
| + DOCS.with(|d| d.borrow_mut().remove(&handle)); |
| +} |
| + |
| +pub fn apply_slice(handle: u32, update: &[u8]) -> i32 { |
| + DOCS.with(|d| match d.borrow().get(&handle) { |
| + None => ERR_NO_DOC, |
| + Some(doc) => match apply_update(doc, update) { |
| + Ok(()) => 0, |
| + Err(CoreError::Decode(_)) => ERR_DECODE, |
| + Err(CoreError::Apply(_)) => ERR_APPLY, |
| + }, |
| + }) |
| +} |
| + |
| +pub fn text_string(handle: u32) -> String { |
| + DOCS.with(|d| match d.borrow().get(&handle) { |
| + None => String::new(), |
| + Some(doc) => text_content(doc), |
| + }) |
| +} |
| + |
| +// --- wasm exports --- |
| + |
| +// Amended by pre-flight ruling R2. The obvious version — Vec::with_capacity |
| +// then Vec::from_raw_parts(ptr, 0, len) — is undefined behaviour: with_capacity |
| +// guarantees AT LEAST len, so reconstructing with capacity `len` is wrong |
| +// whenever the allocator rounded up. std::alloc with an identical Layout on |
| +// both sides has no such gap. |
| + |
| +fn layout_for(len: u32) -> std::alloc::Layout { |
| + // align 1: these are byte buffers the host writes into. |
| + std::alloc::Layout::from_size_align(len.max(1) as usize, 1) |
| + .expect("byte layout is always valid") |
| +} |
| + |
| +#[no_mangle] |
| +pub extern "C" fn docs_core_alloc(len: u32) -> u32 { |
| + unsafe { std::alloc::alloc(layout_for(len)) as u32 } |
| +} |
| + |
| +/// # Safety |
| +/// |
| +/// `ptr` must be either 0 (a no-op) or a value previously returned by |
| +/// `docs_core_alloc`, not yet freed, and `len` must be exactly the `len` |
| +/// that allocation was made with — `layout_for(len)` must reproduce the |
| +/// same `Layout` used to allocate it, per `std::alloc::dealloc`'s contract. |
| +#[no_mangle] |
| +pub unsafe extern "C" fn docs_core_free(ptr: u32, len: u32) { |
| + if ptr != 0 { |
| + std::alloc::dealloc(ptr as *mut u8, layout_for(len)); |
| + } |
| +} |
| + |
| +/// # Safety |
| +/// |
| +/// `guid_ptr` and `guid_len` must describe a byte range that lies entirely |
| +/// within this module's linear memory and is not mutated for the duration |
| +/// of this call. |
| +#[no_mangle] |
| +pub unsafe extern "C" fn docs_core_doc_new( |
| + guid_ptr: u32, |
| + guid_len: u32, |
| + client_id: u64, |
| +) -> u32 { |
| + let bytes = std::slice::from_raw_parts(guid_ptr as *const u8, guid_len as usize); |
| + match std::str::from_utf8(bytes) { |
| + Ok(guid) => doc_new(guid, client_id), |
| + Err(_) => 0, // invalid UTF-8 GUID is a null handle, not a trap |
| + } |
| +} |
| + |
| +#[no_mangle] |
| +pub extern "C" fn docs_core_doc_free(handle: u32) { |
| + doc_free(handle) |
| +} |
| + |
| +/// # Safety |
| +/// |
| +/// `ptr` and `len` must describe a byte range that lies entirely within |
| +/// this module's linear memory and is not mutated for the duration of this |
| +/// call. |
| +#[no_mangle] |
| +pub unsafe extern "C" fn docs_core_apply(handle: u32, ptr: u32, len: u32) -> i32 { |
| + let bytes = std::slice::from_raw_parts(ptr as *const u8, len as usize); |
| + apply_slice(handle, bytes) |
| +} |
| + |
| +#[no_mangle] |
| +pub extern "C" fn docs_core_text_len(handle: u32) -> u32 { |
| + text_string(handle).len() as u32 |
| +} |
| + |
| +/// # Safety |
| +/// |
| +/// `out_ptr` and `out_len` must describe a byte range that lies entirely |
| +/// within this module's linear memory and is valid for writes for the |
| +/// duration of this call. |
| +#[no_mangle] |
| +pub unsafe extern "C" fn docs_core_text_read(handle: u32, out_ptr: u32, out_len: u32) -> i32 { |
| + let s = text_string(handle); |
| + if s.len() > out_len as usize { |
| + return ERR_BUFFER_TOO_SMALL; |
| + } |
| + std::ptr::copy_nonoverlapping(s.as_ptr(), out_ptr as *mut u8, s.len()); |
| + s.len() as i32 |
| +} |