//! The C ABI the Go host calls through wazero, following `core/src/abi.rs`: //! integers and linear-memory offsets only, every host pointer bounds- //! checked before a slice is built over it, and the two results of the //! last `slides_pdf_render` call held in module memory behind `_len`/`_read` //! pairs. //! //! There are no handles: a render is one call in, one PDF out, and the //! host instantiates a fresh module per export job (a trap or a timeout //! discards the instance, as in the CRDT host). `OUT` and `ERR` are //! therefore module-global rather than per-handle. //! //! # Inputs //! //! `svgs_ptr/len` and `fonts_ptr/len` each describe a sequence of //! length-prefixed frames (4-byte big-endian length, then the bytes): //! one frame per SVG page, one frame per TTF. The host writes them into //! buffers from `slides_pdf_alloc`. //! //! # Results //! //! `slides_pdf_render` returns 0 on success (PDF bytes in `out`, `err` //! cleared) or a negative code (message in `err`, `out` cleared). use crate::{render, split_frames, RenderError}; use std::cell::RefCell; thread_local! { static OUT: RefCell> = const { RefCell::new(Vec::new()) }; static ERR: RefCell = const { RefCell::new(String::new()) }; } /// The render was refused; the message is in the `err` read pair. pub const ERR_RENDER: i32 = -1; pub const ERR_BUFFER_TOO_SMALL: i32 = -4; /// A `ptr`/`len` pair this module's linear memory cannot satisfy. See /// `in_bounds`. pub const ERR_INVALID_LENGTH: i32 = -5; /// Files the outcome of a render. This is the ONLY place `OUT` and `ERR` /// are borrowed mutably, and it runs after `render` has returned: no /// `RefCell` guard is ever held across a call into usvg or svg2pdf, so a /// trap inside them (which does not unwind under `panic = "abort"`) /// cannot leave a guard behind. The host discards the instance after a /// trap anyway, but the rule is the same one `core/src/abi.rs` keeps. fn set_result(r: Result, RenderError>) -> i32 { match r { Ok(pdf) => { OUT.with(|o| *o.borrow_mut() = pdf); ERR.with(|e| e.borrow_mut().clear()); 0 } Err(e) => { ERR.with(|m| *m.borrow_mut() = e.0); OUT.with(|o| o.borrow_mut().clear()); ERR_RENDER } } } /// Renders the frames and files the result. Native callers (tests) use /// this; the wasm export below unpacks the pointers and calls it. pub fn render_frames(svgs: &[u8], fonts: &[u8]) -> i32 { let r = (|| { let svgs = split_frames(svgs)?; let fonts = split_frames(fonts)?; render(&svgs, &fonts) })(); set_result(r) } fn layout_for(len: u32) -> Option { std::alloc::Layout::from_size_align(len.max(1) as usize, 1).ok() } #[cfg(target_arch = "wasm32")] fn memory_len_bytes() -> u64 { const WASM_PAGE_BYTES: u64 = 65536; core::arch::wasm32::memory_size(0) as u64 * WASM_PAGE_BYTES } #[cfg(not(target_arch = "wasm32"))] fn memory_len_bytes() -> u64 { u64::MAX } /// True if `ptr..ptr+len` is representable as a Rust slice and (on wasm32) /// fully inside this module's currently instantiated linear memory. fn in_bounds(ptr: u32, len: u32) -> bool { if len as u64 > isize::MAX as u64 { return false; } match (ptr as u64).checked_add(len as u64) { Some(end) => end <= memory_len_bytes(), None => false, } } /// Builds the slice for a host `ptr`/`len` pair that `in_bounds` has /// already accepted. A zero length never touches the pointer: the host /// sends `(0, 0)` for "no frames" and `slice::from_raw_parts` over a null /// pointer is undefined even for zero bytes. /// /// # Safety /// /// `in_bounds(ptr, len)` must be true, and the range must not be written /// by anyone for the lifetime of the returned slice. unsafe fn host_slice<'a>(ptr: u32, len: u32) -> &'a [u8] { if len == 0 { return &[]; } std::slice::from_raw_parts(ptr as *const u8, len as usize) } /// Allocates `len` bytes for the host to write into; 0 on failure, which /// the host must check. #[no_mangle] pub extern "C" fn slides_pdf_alloc(len: u32) -> u32 { match layout_for(len) { // SAFETY: `layout_for` never returns a zero-size Layout. Some(layout) => unsafe { std::alloc::alloc(layout) as u32 }, None => 0, } } /// # Safety /// /// `ptr` must be 0 or a value returned by `slides_pdf_alloc` with this /// exact `len`, not yet freed. #[no_mangle] pub unsafe extern "C" fn slides_pdf_free(ptr: u32, len: u32) { if ptr == 0 { return; } if let Some(layout) = layout_for(len) { std::alloc::dealloc(ptr as *mut u8, layout); } } /// # Safety /// /// Both pointer/length pairs must describe byte ranges inside this /// module's linear memory that are not mutated during the call (checked /// by `in_bounds` before any slice is constructed). #[no_mangle] pub unsafe extern "C" fn slides_pdf_render( svgs_ptr: u32, svgs_len: u32, fonts_ptr: u32, fonts_len: u32, ) -> i32 { if !in_bounds(svgs_ptr, svgs_len) || !in_bounds(fonts_ptr, fonts_len) { return ERR_INVALID_LENGTH; } let svgs = host_slice(svgs_ptr, svgs_len); let fonts = host_slice(fonts_ptr, fonts_len); render_frames(svgs, fonts) } fn len_code(bytes: &[u8]) -> i32 { bytes.len().try_into().unwrap_or(i32::MAX) } /// # Safety /// /// `out_ptr`/`out_len` must be a writable range inside linear memory /// (checked by `in_bounds` before any write). unsafe fn read_into(bytes: &[u8], out_ptr: u32, out_len: u32) -> i32 { if bytes.len() > out_len as usize { return ERR_BUFFER_TOO_SMALL; } if !in_bounds(out_ptr, out_len) { return ERR_INVALID_LENGTH; } if !bytes.is_empty() { std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr as *mut u8, bytes.len()); } len_code(bytes) } /// Byte length of the last render's PDF (0 if none or it failed). #[no_mangle] pub extern "C" fn slides_pdf_out_len() -> i32 { OUT.with(|o| len_code(&o.borrow())) } /// # Safety /// /// See `read_into`. #[no_mangle] pub unsafe extern "C" fn slides_pdf_out_read(out_ptr: u32, out_len: u32) -> i32 { OUT.with(|o| read_into(&o.borrow(), out_ptr, out_len)) } /// Byte length of the last render's refusal message (0 if none or it /// succeeded). #[no_mangle] pub extern "C" fn slides_pdf_err_len() -> i32 { ERR.with(|e| len_code(e.borrow().as_bytes())) } /// # Safety /// /// See `read_into`. #[no_mangle] pub unsafe extern "C" fn slides_pdf_err_read(out_ptr: u32, out_len: u32) -> i32 { ERR.with(|e| read_into(e.borrow().as_bytes(), out_ptr, out_len)) }