standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! The guest runtime a `no_std` component needs and the UI world does not
//! provide: an allocator over linear memory, a panic handler that traps,
//! the canonical ABI's `cabi_realloc`, and the few C symbols compiled Rust
//! calls (`memcmp`, `bcmp`, `fmodf`, `fmod`), which `std` takes from
//! wasi-libc on wasip2.
//! Compiled only for `wasm32` without the `std` feature; the allocator and
//! panic handler also need the default `runtime` feature.
//!
//! The allocator bumps for new memory and reuses freed blocks: a surface
//! allocates a whole new region on every resize and frees the old one, so
//! a bump allocator that never reused memory would run a stage into the
//! host's memory limit after a few terminal resizes.

#[cfg(feature = "runtime")]
mod heap {

    use core::alloc::{GlobalAlloc, Layout};
    use core::cell::UnsafeCell;

    /// Freed blocks kept for reuse; past this the smallest is forgotten.
    const FREE_SLOTS: usize = 64;
    const PAGE: usize = 65_536;

    #[derive(Clone, Copy)]
    struct Block {
        start: usize,
        len: usize,
    }

    struct Heap {
        /// The bump pointer and the end of grown memory.
        next: usize,
        end: usize,
        /// Free blocks, unordered, never adjacent to each other or to
        /// `next` (they are merged when freed).
        free: [Block; FREE_SLOTS],
        count: usize,
    }

    impl Heap {
        fn take_free(&mut self, layout: Layout) -> Option<usize> {
            for index in 0..self.count {
                let block = self.free[index];
                let start = (block.start + layout.align() - 1) & !(layout.align() - 1);
                let stop = start + layout.size();
                if stop > block.start + block.len {
                    continue;
                }
                // Keep what is left on either side of the allocation.
                self.remove(index);
                self.release(Block {
                    start: block.start,
                    len: start - block.start,
                });
                self.release(Block {
                    start: stop,
                    len: block.start + block.len - stop,
                });
                return Some(start);
            }
            None
        }

        fn bump(&mut self, layout: Layout) -> Option<usize> {
            if self.end == 0 {
                let previous = core::arch::wasm32::memory_grow(0, 1);
                if previous == usize::MAX {
                    return None;
                }
                self.next = previous * PAGE;
                self.end = self.next + PAGE;
            }
            let start = (self.next + layout.align() - 1) & !(layout.align() - 1);
            let stop = start + layout.size();
            if stop > self.end {
                let pages = (stop - self.end).div_ceil(PAGE);
                if core::arch::wasm32::memory_grow(0, pages) == usize::MAX {
                    return None;
                }
                self.end += pages * PAGE;
            }
            let gap = Block {
                start: self.next,
                len: start - self.next,
            };
            self.next = stop;
            self.release(gap);
            Some(start)
        }

        fn remove(&mut self, index: usize) {
            self.count -= 1;
            self.free[index] = self.free[self.count];
        }

        /// Returns a block to the heap: merged with free neighbours, given
        /// back to the bump pointer when it ends there, else listed.
        fn release(&mut self, mut block: Block) {
            if block.len == 0 {
                return;
            }
            let mut index = 0;
            while index < self.count {
                let other = self.free[index];
                if other.start + other.len == block.start {
                    block = Block {
                        start: other.start,
                        len: other.len + block.len,
                    };
                    self.remove(index);
                    index = 0;
                } else if block.start + block.len == other.start {
                    block.len += other.len;
                    self.remove(index);
                    index = 0;
                } else {
                    index += 1;
                }
            }
            if block.start + block.len == self.next {
                self.next = block.start;
                return;
            }
            if self.count == FREE_SLOTS {
                let smallest = (0..self.count)
                    .min_by_key(|index| self.free[*index].len)
                    .unwrap_or(0);
                if self.free[smallest].len >= block.len {
                    return;
                }
                self.remove(smallest);
            }
            self.free[self.count] = block;
            self.count += 1;
        }
    }

    struct Allocator {
        heap: UnsafeCell<Heap>,
    }

    // SAFETY: a component instance runs on one thread.
    unsafe impl Sync for Allocator {}

    unsafe impl GlobalAlloc for Allocator {
        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
            // SAFETY: single-threaded; the pointer is this static's cell.
            let heap = unsafe { &mut *self.heap.get() };
            heap.take_free(layout)
                .or_else(|| heap.bump(layout))
                .map_or(core::ptr::null_mut(), |start| start as *mut u8)
        }

        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
            // SAFETY: single-threaded; the pointer is this static's cell.
            let heap = unsafe { &mut *self.heap.get() };
            heap.release(Block {
                start: ptr as usize,
                len: layout.size(),
            });
        }
    }

    #[global_allocator]
    static ALLOCATOR: Allocator = Allocator {
        heap: UnsafeCell::new(Heap {
            next: 0,
            end: 0,
            free: [Block { start: 0, len: 0 }; FREE_SLOTS],
            count: 0,
        }),
    };

    #[panic_handler]
    fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {
        core::arch::wasm32::unreachable()
    }
}

/// The canonical ABI allocator the host uses to lower strings and lists
/// into the guest, over the global allocator.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cabi_realloc(
    old_ptr: *mut u8,
    old_len: usize,
    align: usize,
    new_len: usize,
) -> *mut u8 {
    use alloc::alloc::{Layout, alloc, dealloc};
    let Ok(layout) = Layout::from_size_align(new_len.max(1), align.max(1)) else {
        core::arch::wasm32::unreachable()
    };
    // SAFETY: a fresh allocation; the old bytes are copied before the old
    // block is released. The canonical ABI passes the old block's alignment
    // with `align`.
    unsafe {
        let new_ptr = alloc(layout);
        if new_ptr.is_null() {
            core::arch::wasm32::unreachable()
        }
        if !old_ptr.is_null() && old_len > 0 {
            core::ptr::copy_nonoverlapping(old_ptr, new_ptr, old_len.min(new_len));
            dealloc(
                old_ptr,
                Layout::from_size_align_unchecked(old_len, align.max(1)),
            );
        }
        new_ptr
    }
}

/// `memcmp` and `bcmp`, which `std` takes from wasi-libc on wasip2 and a
/// `no_std` component must supply (slice and string comparisons call them;
/// the other memory functions lower to bulk-memory instructions).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn memcmp(
    left: *const core::ffi::c_void,
    right: *const core::ffi::c_void,
    len: usize,
) -> i32 {
    let left = left.cast::<u8>();
    let right = right.cast::<u8>();
    let mut index = 0;
    while index < len {
        // SAFETY: the caller passes two readable ranges of `len` bytes.
        let (a, b) = unsafe { (*left.add(index), *right.add(index)) };
        if a != b {
            return i32::from(a) - i32::from(b);
        }
        index += 1;
    }
    0
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn bcmp(
    left: *const core::ffi::c_void,
    right: *const core::ffi::c_void,
    len: usize,
) -> i32 {
    // SAFETY: as `memcmp`.
    unsafe { memcmp(left, right, len) }
}

/// `fmodf` and `fmod`: the `%` operator on floats calls them, and on
/// wasip2 they come from wasi-libc, which a `no_std` component lacks.
#[unsafe(no_mangle)]
pub extern "C" fn fmodf(x: f32, y: f32) -> f32 {
    libm::fmodf(x, y)
}

#[unsafe(no_mangle)]
pub extern "C" fn fmod(x: f64, y: f64) -> f64 {
    libm::fmod(x, y)
}