diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..2cf0198 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,70 @@ +name: Deploy Documentation + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + name: Build Rustdoc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ + libxkbcommon-dev libssl-dev libgtk-3-dev libatk1.0-dev \ + libglib2.0-dev libpango1.0-dev libgdk-pixbuf-2.0-dev \ + libasound2-dev + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Build documentation + run: | + cargo doc --no-deps \ + -p oxide-docs \ + -p oxide-sdk \ + -p oxide-browser + env: + RUSTDOCFLAGS: >- + --html-in-header oxide-docs/rustdoc-header.html + -Z unstable-options + --default-theme dark + + - name: Create landing page + run: cp oxide-docs/index.html target/doc/index.html + + - name: Add .nojekyll + run: touch target/doc/.nojekyll + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: target/doc + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Cargo.toml b/Cargo.toml index 5c64718..933b10c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "oxide-browser", "oxide-sdk", + "oxide-docs", "examples/hello-oxide", "examples/audio-player", "examples/timer-demo", diff --git a/oxide-browser/Cargo.toml b/oxide-browser/Cargo.toml index 536955a..d5695f5 100644 --- a/oxide-browser/Cargo.toml +++ b/oxide-browser/Cargo.toml @@ -4,6 +4,10 @@ version = "0.2.0" edition = "2021" description = "A binary-first browser that fetches and runs .wasm modules in a secure sandbox" +[lib] +name = "oxide_browser" +path = "src/lib.rs" + [[bin]] name = "oxide" path = "src/main.rs" diff --git a/oxide-browser/src/bookmarks.rs b/oxide-browser/src/bookmarks.rs index e665bd3..04a28c5 100644 --- a/oxide-browser/src/bookmarks.rs +++ b/oxide-browser/src/bookmarks.rs @@ -1,13 +1,30 @@ +//! Persistent bookmarks for the Oxide browser. +//! +//! Entries are stored in a [`sled`] embedded database under a dedicated [`sled::Tree`] +//! named `"bookmarks"`. Each record is keyed by URL; values hold the serialized +//! title, favorite flag, and creation time. For UI code that may run on multiple +//! threads, use [`SharedBookmarkStore`] and initialize the store when the +//! database is available. + use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{Context, Result}; +/// A saved bookmark: canonical URL, display title, favorite flag, and creation time. +/// +/// The URL is the primary key in [`BookmarkStore`]. New bookmarks from [`BookmarkStore::add`] +/// start with [`Bookmark::is_favorite`] set to `false` and [`Bookmark::created_at_ms`] set +/// to the current time in milliseconds since the UNIX epoch. #[derive(Clone, Debug)] pub struct Bookmark { + /// Canonical bookmark URL; also the sled key for this entry. pub url: String, + /// User-visible title (may differ from the page title at save time). pub title: String, + /// When `true`, this bookmark is included in favorite-only listings. pub is_favorite: bool, + /// Creation instant as milliseconds since [`UNIX_EPOCH`]. pub created_at_ms: u64, } @@ -46,13 +63,17 @@ impl Bookmark { } } -/// Persistent bookmark storage backed by a sled tree. +/// Persistent bookmark storage backed by a [`sled::Tree`] in an open [`sled::Db`]. +/// +/// The tree name is `"bookmarks"`. Keys are URL byte strings; values are an internal +/// binary encoding of title, favorite bit, and timestamp (see [`Bookmark`]). #[derive(Clone)] pub struct BookmarkStore { tree: sled::Tree, } impl BookmarkStore { + /// Opens the bookmarks tree in `db`, creating it if it does not exist. pub fn open(db: &sled::Db) -> Result { let tree = db .open_tree("bookmarks") @@ -60,6 +81,9 @@ impl BookmarkStore { Ok(Self { tree }) } + /// Inserts a new bookmark for `url` with the given `title`, or overwrites the existing entry. + /// + /// The bookmark is stored as not favorited with a fresh [`Bookmark::created_at_ms`]. pub fn add(&self, url: &str, title: &str) -> Result<()> { let bm = Bookmark { url: url.to_string(), @@ -73,6 +97,7 @@ impl BookmarkStore { Ok(()) } + /// Removes the bookmark for `url`, if present. pub fn remove(&self, url: &str) -> Result<()> { self.tree .remove(url.as_bytes()) @@ -80,10 +105,15 @@ impl BookmarkStore { Ok(()) } + /// Returns whether a bookmark exists for `url`. pub fn contains(&self, url: &str) -> bool { self.tree.contains_key(url.as_bytes()).unwrap_or(false) } + /// Flips the favorite flag for the bookmark at `url` and returns the new value. + /// + /// If the URL is missing or the stored value cannot be decoded, returns `Ok(false)` without + /// changing storage. pub fn toggle_favorite(&self, url: &str) -> Result { if let Some(data) = self .tree @@ -102,6 +132,9 @@ impl BookmarkStore { Ok(false) } + /// Returns whether the bookmark at `url` is marked as a favorite. + /// + /// Missing or corrupt entries are treated as not favorited. #[allow(dead_code)] pub fn is_favorite(&self, url: &str) -> bool { self.tree @@ -113,6 +146,7 @@ impl BookmarkStore { .unwrap_or(false) } + /// Returns every bookmark, ordered by [`Bookmark::created_at_ms`] descending (newest first). pub fn list_all(&self) -> Vec { let mut bookmarks = Vec::new(); for (key, val) in self.tree.iter().flatten() { @@ -126,6 +160,7 @@ impl BookmarkStore { bookmarks } + /// Returns only bookmarks with [`Bookmark::is_favorite`] set, in the same order as [`Self::list_all`]. #[allow(dead_code)] pub fn list_favorites(&self) -> Vec { self.list_all() @@ -135,8 +170,13 @@ impl BookmarkStore { } } +/// Thread-safe handle to an optional [`BookmarkStore`]: [`Arc`] wrapped [`Mutex`] of [`Option`]. +/// +/// Use `None` before the sled database is opened; replace with `Some(store)` after +/// [`BookmarkStore::open`]. Lock the mutex when reading or updating bookmarks from worker threads. pub type SharedBookmarkStore = Arc>>; +/// Creates a shared bookmark store initialized to `None` (no database opened yet). pub fn new_shared() -> SharedBookmarkStore { Arc::new(Mutex::new(None)) } diff --git a/oxide-browser/src/capabilities.rs b/oxide-browser/src/capabilities.rs index a665872..a6b125e 100644 --- a/oxide-browser/src/capabilities.rs +++ b/oxide-browser/src/capabilities.rs @@ -1,3 +1,15 @@ +//! Host capabilities and shared state for WebAssembly guests. +//! +//! This module defines [`HostState`] and the data structures the host and guest share +//! (console, canvas, timers, input, widgets, navigation, and more). +//! [`register_host_functions`] attaches the **`oxide`** Wasm import module to a Wasmtime +//! [`Linker`]: every host function that guest modules may call—`api_log`, `api_canvas_*`, +//! `api_storage_*`, `api_navigate`, audio and UI APIs, etc.—is registered there under the +//! import module name `oxide`. +//! +//! Guest code imports these symbols from `oxide`; implementations run on the host and +//! read or mutate the [`HostState`] held in the Wasmtime store attached to the linker. + use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -18,8 +30,11 @@ struct AudioChannel { looping: bool, } -/// Audio playback engine backed by rodio. -/// Supports multiple simultaneous channels for layered audio (e.g. music + SFX). +/// Multi-channel audio playback engine backed by [rodio](https://crates.io/crates/rodio). +/// +/// Each logical channel has its own [`rodio::Player`] so guests can play overlapping +/// sounds (for example music on one channel and effects on another). The default channel +/// used by the single-channel `api_audio_*` imports is `0`. pub struct AudioEngine { _device_sink: rodio::stream::MixerDeviceSink, channels: HashMap, @@ -79,19 +94,37 @@ impl AudioEngine { } } +/// All shared state between the browser host and a guest Wasm module (and dynamically loaded children). +/// +/// Most fields are behind [`Arc`] and [`Mutex`] so the same state can be shared across +/// threads and nested module loads. Host code sets fields like [`HostState::memory`] and +/// [`HostState::current_url`] before or during execution; guest imports mutate the rest +/// through the registered `oxide` functions. #[derive(Clone)] pub struct HostState { + /// Console log lines shown in the host UI, appended by [`console_log`] and `api_*` helpers. pub console: Arc>>, + /// Raster canvas: queued draw commands and decoded images for the current frame. pub canvas: Arc>, + /// In-memory key/value session storage (string keys and values), similar to `localStorage` in scope. pub storage: Arc>>, + /// Pending one-shot and interval timers; the host drains these and invokes `on_timer` on the guest. pub timers: Arc>>, + /// Monotonic counter used to assign unique [`TimerEntry::id`] values for `api_set_timeout` / `api_set_interval`. pub timer_next_id: Arc>, + /// Last text written to or read from the clipboard via the guest API (when permitted). pub clipboard: Arc>, + /// When `false`, `api_clipboard_read` / `api_clipboard_write` are blocked and log a warning. pub clipboard_allowed: Arc>, + /// Optional embedded [`sled`] database for persistent per-origin key/value bytes (`api_kv_store_*`). pub kv_db: Option>, + /// The guest’s exported linear memory, used to read/write pointers passed to host imports. pub memory: Option, + /// Engine and limits used by `api_load_module` to fetch and instantiate child Wasm modules. pub module_loader: Option>, + /// Session history stack for `api_push_state`, `api_replace_state`, and back/forward navigation. pub navigation: Arc>, + /// Hit-test regions registered by the guest for link clicks in the canvas area. pub hyperlinks: Arc>>, /// Set by guest `api_navigate` — consumed by the UI after module returns. pub pending_navigation: Arc>>, @@ -113,44 +146,60 @@ pub struct HostState { pub audio: Arc>>, } +/// A single console log line: local time, severity, and message text. #[derive(Clone, Debug)] pub struct ConsoleEntry { + /// Time of day when the entry was recorded (`chrono` local format, e.g. `14:03:22.123`). pub timestamp: String, + /// Severity bucket for styling in the host console. pub level: ConsoleLevel, + /// UTF-8 message body. pub message: String, } +/// Severity level for [`ConsoleEntry`] and [`console_log`]. #[derive(Clone, Debug)] pub enum ConsoleLevel { + /// Informational message (maps to `api_log`). Log, + /// Warning (maps to `api_warn`). Warn, + /// Error (maps to `api_error`). Error, } +/// Current canvas snapshot for one frame: command list, dimensions, image atlas, and invalidation generation. #[derive(Clone, Debug)] pub struct CanvasState { + /// Ordered draw operations accumulated since the last clear (or start of frame). pub commands: Vec, + /// Canvas width in pixels. pub width: u32, + /// Canvas height in pixels. pub height: u32, + /// Decoded images indexed by position in this vector; [`DrawCommand::Image`] references them by `image_id`. pub images: Vec, + /// Bumped when the canvas is cleared so the host can detect a full redraw. pub generation: u64, } +/// An image decoded to RGBA8 pixels for compositing in the host canvas renderer. #[derive(Clone, Debug)] pub struct DecodedImage { + /// Width in pixels. pub width: u32, + /// Height in pixels. pub height: u32, + /// Raw RGBA bytes, row-major (`width * height * 4` elements when full frame). pub pixels: Vec, } +/// One canvas drawing operation produced by guest `api_canvas_*` imports and consumed by the host renderer. #[derive(Clone, Debug)] pub enum DrawCommand { - Clear { - r: u8, - g: u8, - b: u8, - a: u8, - }, + /// Fill the entire canvas with a solid RGBA color and reset the command list (see `api_canvas_clear`). + Clear { r: u8, g: u8, b: u8, a: u8 }, + /// Axis-aligned filled rectangle in canvas coordinates with RGBA fill. Rect { x: f32, y: f32, @@ -161,6 +210,7 @@ pub enum DrawCommand { b: u8, a: u8, }, + /// Filled circle centered at `(cx, cy)` with the given radius and RGBA fill. Circle { cx: f32, cy: f32, @@ -170,6 +220,7 @@ pub enum DrawCommand { b: u8, a: u8, }, + /// Text baseline position `(x, y)`, font size in pixels, RGB color, and string payload. Text { x: f32, y: f32, @@ -179,6 +230,7 @@ pub enum DrawCommand { b: u8, text: String, }, + /// Line from `(x1, y1)` to `(x2, y2)` with RGB stroke color and stroke width in pixels. Line { x1: f32, y1: f32, @@ -189,6 +241,7 @@ pub enum DrawCommand { b: u8, thickness: f32, }, + /// Draw [`DecodedImage`] `image_id` from `images` into the axis-aligned rectangle `(x, y, w, h)`. Image { x: f32, y: f32, @@ -198,16 +251,25 @@ pub enum DrawCommand { }, } +/// A scheduled timer: either a one-shot `setTimeout` or repeating `setInterval`. #[derive(Clone, Debug)] pub struct TimerEntry { + /// Host-assigned id returned by `api_set_timeout` / `api_set_interval` for `api_clear_timer`. pub id: u32, + /// Absolute time when this entry should fire next. pub fire_at: Instant, + /// `None` for a one-shot timer; `Some(duration)` for an interval (rescheduled after each fire). pub interval: Option, + /// Guest-defined id passed to the exported `on_timer` callback when this timer fires. pub callback_id: u32, } -/// Drain all expired timers, returning their callback IDs. -/// One-shot timers are removed; interval timers are rescheduled. +/// Remove due timers from `timers`, collect each fired entry’s [`TimerEntry::callback_id`], and return them. +/// +/// Compares each [`TimerEntry::fire_at`] against `Instant::now()`. **One-shot** entries +/// (`interval` is `None`) are removed from the vector after firing. **Interval** entries +/// are kept and their `fire_at` is advanced by `interval` so they fire again later. The +/// host typically calls the guest’s `on_timer` once per id in the returned vector. pub fn drain_expired_timers(timers: &Arc>>) -> Vec { let now = Instant::now(); let mut guard = timers.lock().unwrap(); @@ -229,35 +291,57 @@ pub fn drain_expired_timers(timers: &Arc>>) -> Vec { fired } -/// A clickable rectangular region on the canvas that acts as a hyperlink. +/// A clickable axis-aligned rectangle on the canvas that navigates to a URL when hit-tested. +/// +/// Populated by `api_register_hyperlink` and cleared with `api_clear_hyperlinks`. Coordinates +/// are in the same space as canvas drawing (the host maps pointer position into this space). #[derive(Clone, Debug)] pub struct Hyperlink { + /// Left edge of the hit region in canvas coordinates. pub x: f32, + /// Top edge of the hit region in canvas coordinates. pub y: f32, + /// Width of the hit region. pub w: f32, + /// Height of the hit region. pub h: f32, + /// Target URL (already resolved relative to the current page URL when registered). pub url: String, } -/// Per-frame input state captured from egui and exposed to the guest. +/// Per-frame input snapshot from the host (egui) for guest polling via `api_mouse_*`, `api_key_*`, etc. #[derive(Clone, Debug, Default)] pub struct InputState { + /// Pointer horizontal position in window/content coordinates before canvas offset subtraction in APIs. pub mouse_x: f32, + /// Pointer vertical position in window/content coordinates before canvas offset subtraction in APIs. pub mouse_y: f32, + /// Mouse buttons currently held: index 0 = primary, 1 = secondary, 2 = middle. pub mouse_buttons_down: [bool; 3], + /// Mouse buttons that transitioned to pressed this frame (same indexing as `mouse_buttons_down`). pub mouse_buttons_clicked: [bool; 3], + /// Key codes currently held (host-defined `u32` values, polled by `api_key_down`). pub keys_down: Vec, + /// Key codes that registered a press this frame (`api_key_pressed`). pub keys_pressed: Vec, + /// Shift modifier held this frame. pub modifiers_shift: bool, + /// Control modifier held this frame. pub modifiers_ctrl: bool, + /// Alt modifier held this frame. pub modifiers_alt: bool, + /// Horizontal scroll delta for this frame. pub scroll_x: f32, + /// Vertical scroll delta for this frame. pub scroll_y: f32, } -/// A widget the guest wants rendered this frame. +/// UI control the guest requested for the current frame; the host egui layer renders these after canvas content. +/// +/// Commands are queued during `on_frame`; stable `id` values tie widgets to [`WidgetValue`] state and click tracking. #[derive(Clone, Debug)] pub enum WidgetCommand { + /// Clickable button with label; `api_ui_button` returns whether this `id` was clicked this pass. Button { id: u32, x: f32, @@ -266,12 +350,14 @@ pub enum WidgetCommand { h: f32, label: String, }, + /// Toggle with label; checked state lives in [`WidgetValue::Bool`] for this `id`. Checkbox { id: u32, x: f32, y: f32, label: String, }, + /// Horizontal slider between `min` and `max`; value stored in [`WidgetValue::Float`]. Slider { id: u32, x: f32, @@ -280,19 +366,18 @@ pub enum WidgetCommand { min: f32, max: f32, }, - TextInput { - id: u32, - x: f32, - y: f32, - w: f32, - }, + /// Single-line text field; current text stored in [`WidgetValue::Text`]. + TextInput { id: u32, x: f32, y: f32, w: f32 }, } -/// Persistent widget state maintained by the host across frames. +/// Persistent control state for interactive widgets, keyed by widget `id` across frames. #[derive(Clone, Debug)] pub enum WidgetValue { + /// Checkbox on/off. Bool(bool), + /// Slider current value. Float(f32), + /// Text field contents. Text(String), } @@ -382,6 +467,9 @@ fn write_guest_bytes( Ok(()) } +/// Append a [`ConsoleEntry`] with the current local timestamp to the shared console buffer. +/// +/// Used by `api_log` / `api_warn` / `api_error` and by other host helpers that surface messages to the UI. pub fn console_log(console: &Arc>>, level: ConsoleLevel, message: String) { console.lock().unwrap().push(ConsoleEntry { timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(), @@ -390,7 +478,16 @@ pub fn console_log(console: &Arc>>, level: ConsoleLevel, }); } -/// Register all host-provided capabilities onto the linker. +/// Register every `oxide` import on `linker` so guest modules can link against them. +/// +/// This wires dozens of functions (console, canvas, storage, clipboard, timers, HTTP, +/// dynamic module loading, crypto helpers, navigation, hyperlinks, input, audio, UI +/// widgets, etc.) under the Wasm import module name **`oxide`**. Each closure captures +/// [`Caller`] to read [`HostState`] from the store: guest pointers are resolved through +/// [`HostState::memory`], and shared handles (`Arc>`) are updated in place. +/// +/// Call this once when building the linker for a main or child instance; the dynamic loader +/// path also invokes it when instantiating a child module (see the `api_load_module` import). pub fn register_host_functions(linker: &mut Linker) -> Result<()> { // ── Console ────────────────────────────────────────────────────── diff --git a/oxide-browser/src/engine.rs b/oxide-browser/src/engine.rs index 60ce3b1..48bd68a 100644 --- a/oxide-browser/src/engine.rs +++ b/oxide-browser/src/engine.rs @@ -1,26 +1,60 @@ +//! WebAssembly engine configuration for Oxide. +//! +//! This module configures [Wasmtime](https://wasmtime.dev/) for running guest modules in a +//! sandboxed environment: bounded linear memory, instruction fuel metering, and a +//! [`SandboxPolicy`] that gates host capabilities (filesystem, environment variables, network +//! sockets)—all denied unless explicitly enabled. +//! +//! Default [`SandboxPolicy`] limits: **16 MiB** linear memory (256 × 64 KiB pages) and **~500M** +//! Wasm instructions of fuel per [`Store`] before the guest is halted. +//! +//! [`WasmEngine`] owns a shared [`Engine`] plus policy and is the main entry point for creating +//! stores, bounded memory, and compiled modules. [`ModuleLoader`] is a lighter bundle of engine +//! plus limits for scenarios such as loading child or dynamically linked modules. + use anyhow::{Context, Result}; use wasmtime::*; const MAX_MEMORY_PAGES: u32 = 256; // 256 * 64KB = 16MB const FUEL_LIMIT: u64 = 500_000_000; // ~500M instructions before forced halt +/// Policy describing what resources a Wasm guest may use and the hard limits applied at runtime. +/// +/// Limits (`max_memory_pages`, `fuel_limit`) are enforced when building stores and memory via +/// [`WasmEngine`]. Capability flags (`allow_*`) express intent for host integrations; by default +/// all are `false` so filesystem, environment, and network access are denied unless the embedding +/// layer opts in. #[allow(dead_code)] #[derive(Clone)] pub struct SandboxPolicy { + /// Maximum number of 64 KiB Wasm memory pages the guest may grow to (default: 256 → 16 MiB). pub max_memory_pages: u32, + /// Maximum Wasm “fuel” (instruction budget) for a single [`Store`] before execution stops. pub fuel_limit: u64, + /// When `true`, the embedding may expose filesystem-backed host APIs to the guest. pub allow_filesystem: bool, + /// When `true`, the embedding may expose environment variable access to the guest. pub allow_env_vars: bool, + /// When `true`, the embedding may expose network socket APIs to the guest. pub allow_network_sockets: bool, } +/// Minimal engine + limit bundle for loading additional Wasm modules (e.g. dynamic imports). +/// +/// Holds a shared [`Engine`] alongside the same memory and fuel caps as [`SandboxPolicy`] so +/// child modules can be compiled consistently without carrying the full policy struct. pub struct ModuleLoader { + /// Wasmtime engine instance shared with the parent embedding. pub engine: Engine, + /// Upper bound on guest linear memory, in 64 KiB pages. pub max_memory_pages: u32, + /// Instruction budget (fuel) aligned with the parent sandbox. pub fuel_limit: u64, } impl Default for SandboxPolicy { + /// Returns the default policy: 256 memory pages (16 MiB cap), ~500M instruction fuel, all + /// `allow_*` flags `false`. fn default() -> Self { Self { max_memory_pages: MAX_MEMORY_PAGES, @@ -32,12 +66,20 @@ impl Default for SandboxPolicy { } } +/// Sandbox-aware wrapper around a Wasmtime [`Engine`]. +/// +/// Configures the engine for fuel-metered execution and compiles/instantiates modules according +/// to the associated [`SandboxPolicy`]. pub struct WasmEngine { engine: Engine, policy: SandboxPolicy, } impl WasmEngine { + /// Builds a [`WasmEngine`] with fuel metering enabled and Cranelift optimizations for speed. + /// + /// The returned engine is ready to compile modules; per-guest limits come from `policy` when + /// calling [`create_store`](Self::create_store) and [`create_bounded_memory`](Self::create_bounded_memory). pub fn new(policy: SandboxPolicy) -> Result { let mut config = Config::new(); config.consume_fuel(true); @@ -47,15 +89,18 @@ impl WasmEngine { Ok(Self { engine, policy }) } + /// Returns the underlying Wasmtime [`Engine`] for compilation and linking. pub fn engine(&self) -> &Engine { &self.engine } + /// Returns the active [`SandboxPolicy`] (memory cap, fuel, and capability flags). #[allow(dead_code)] pub fn policy(&self) -> &SandboxPolicy { &self.policy } + /// Creates a new [`Store`] with `data` as host state and sets fuel to `policy.fuel_limit`. pub fn create_store(&self, data: T) -> Result> { let mut store = Store::new(&self.engine, data); store @@ -64,11 +109,13 @@ impl WasmEngine { Ok(store) } + /// Allocates a new linear [`Memory`] with minimum 1 page and maximum `policy.max_memory_pages`. pub fn create_bounded_memory(&self, store: &mut Store) -> Result { let mem_type = MemoryType::new(1, Some(self.policy.max_memory_pages)); Memory::new(store, mem_type).context("failed to create bounded linear memory") } + /// Compiles raw Wasm bytes into a [`Module`] using this engine’s configuration. pub fn compile_module(&self, wasm_bytes: &[u8]) -> Result { Module::new(&self.engine, wasm_bytes).context("failed to compile wasm module") } diff --git a/oxide-browser/src/lib.rs b/oxide-browser/src/lib.rs new file mode 100644 index 0000000..4132a8d --- /dev/null +++ b/oxide-browser/src/lib.rs @@ -0,0 +1,70 @@ +//! # Oxide Browser — Host Runtime +//! +//! `oxide-browser` is the native desktop host application for the +//! [Oxide browser](https://github.com/nicklabh/oxide), a **binary-first browser** +//! that fetches and executes `.wasm` (WebAssembly) modules instead of +//! HTML/JavaScript. +//! +//! ## Architecture +//! +//! ```text +//! ┌──────────────────────────────────────────────────┐ +//! │ Oxide Browser │ +//! │ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │ +//! │ │ URL Bar │ │ Canvas │ │ Console │ │ +//! │ └────┬─────┘ └──────┬─────┘ └──────┬───────┘ │ +//! │ │ │ │ │ +//! │ ┌────▼───────────────▼───────────────▼───────┐ │ +//! │ │ Host Runtime │ │ +//! │ │ wasmtime engine + sandbox policy │ │ +//! │ │ fuel limit: 500M │ memory: 16MB max │ │ +//! │ └────────────────────┬───────────────────────┘ │ +//! │ │ │ +//! │ ┌────────────────────▼───────────────────────┐ │ +//! │ │ Capability Provider │ │ +//! │ │ "oxide" import module │ │ +//! │ │ canvas, console, storage, clipboard, │ │ +//! │ │ fetch, images, crypto, base64, protobuf, │ │ +//! │ │ dynamic module loading, audio, timers, │ │ +//! │ │ navigation, widgets, input, hyperlinks │ │ +//! │ └────────────────────┬───────────────────────┘ │ +//! │ │ │ +//! │ ┌────────────────────▼───────────────────────┐ │ +//! │ │ Guest .wasm Module │ │ +//! │ │ exports: start_app(), on_frame(dt_ms) │ │ +//! │ │ imports: oxide::* │ │ +//! │ └────────────────────────────────────────────┘ │ +//! └──────────────────────────────────────────────────┘ +//! ``` +//! +//! ## Modules +//! +//! | Module | Purpose | +//! |--------|---------| +//! | [`engine`] | Wasmtime engine configuration, sandbox policy, memory bounds | +//! | [`runtime`] | Module fetching, compilation, execution lifecycle | +//! | [`capabilities`] | All host-imported functions exposed to guest wasm modules | +//! | [`navigation`] | Browser history stack with back/forward traversal | +//! | [`bookmarks`] | Persistent bookmark storage backed by sled | +//! | [`url`] | WHATWG-compliant URL parsing with Oxide-specific schemes | +//! | [`ui`] | egui/eframe desktop UI (toolbar, canvas, console, tabs) | +//! +//! ## Security Model +//! +//! Every guest `.wasm` module runs in a strict sandbox: +//! +//! - **No filesystem access** — guests cannot read or write host files +//! - **No environment variables** — guests cannot inspect the host environment +//! - **No raw sockets** — all network access is mediated through `fetch` +//! - **Bounded memory** — 16 MB (256 pages) hard limit +//! - **Fuel metering** — 500M instruction budget prevents infinite loops +//! - **Capability-based I/O** — only explicitly provided `oxide::*` functions +//! are available to the guest + +pub mod bookmarks; +pub mod capabilities; +pub mod engine; +pub mod navigation; +pub mod runtime; +pub mod ui; +pub mod url; diff --git a/oxide-browser/src/main.rs b/oxide-browser/src/main.rs index 603b588..8714c08 100644 --- a/oxide-browser/src/main.rs +++ b/oxide-browser/src/main.rs @@ -1,17 +1,9 @@ -mod bookmarks; -mod capabilities; -mod engine; -mod navigation; -mod runtime; -mod ui; -mod url; - use anyhow::Result; fn main() -> Result<()> { tracing_subscriber::fmt::init(); - let host = runtime::BrowserHost::new()?; + let host = oxide_browser::runtime::BrowserHost::new()?; let host_state = host.host_state.clone(); let status = host.status.clone(); @@ -26,7 +18,11 @@ fn main() -> Result<()> { eframe::run_native( "Oxide Browser", native_options, - Box::new(move |_cc| Ok(Box::new(ui::OxideApp::new(host_state, status)))), + Box::new(move |_cc| { + Ok(Box::new(oxide_browser::ui::OxideApp::new( + host_state, status, + ))) + }), ) .map_err(|e| anyhow::anyhow!("eframe error: {e}"))?; diff --git a/oxide-browser/src/runtime.rs b/oxide-browser/src/runtime.rs index 51f41c8..87a5c91 100644 --- a/oxide-browser/src/runtime.rs +++ b/oxide-browser/src/runtime.rs @@ -1,3 +1,11 @@ +//! Guest WebAssembly lifecycle for the Oxide browser. +//! +//! This module coordinates fetching `.wasm` binaries (HTTP/HTTPS and `file://`), compiling them +//! with Wasmtime, applying the sandbox policy, and linking the `oxide` import module (memory, +//! host capabilities). After `start_app()` runs, interactive guests may export `on_frame(dt_ms: +//! u32)` for a per-frame render loop; optional `on_timer(callback_id: u32)` callbacks run when +//! timers expire, immediately before each frame. + use std::sync::{Arc, Mutex}; use anyhow::{Context, Result}; @@ -8,17 +16,25 @@ use crate::capabilities::{drain_expired_timers, register_host_functions, HostSta use crate::engine::{ModuleLoader, SandboxPolicy, WasmEngine}; use crate::url::OxideUrl; +/// Current lifecycle state of a browser tab, reflected in the UI and shared across threads. #[derive(Clone, Debug, PartialEq)] pub enum PageStatus { + /// No navigation in progress; ready for a new load. Idle, + /// A URL is being resolved and the `.wasm` module fetched or read (the string is the URL or path being loaded). Loading(String), + /// Guest code is active after a successful load; the string identifies the page (URL or a local placeholder). Running(String), + /// Load, compile, or `start_app` failed; the string is a human-readable error message. Error(String), } const FRAME_FUEL_LIMIT: u64 = 50_000_000; -/// A wasm instance kept alive across frames for interactive apps that export `on_frame`. +/// A Wasmtime [`Store`] and typed guest exports kept alive across frames for interactive apps. +/// +/// Constructed when the module exports `on_frame`. The store holds [`HostState`] (canvas, console, +/// timers, etc.) for the lifetime of the tab. pub struct LiveModule { store: Store, on_frame_fn: TypedFunc, @@ -26,6 +42,11 @@ pub struct LiveModule { } impl LiveModule { + /// Advances one frame: drains expired timers and invokes `on_timer(callback_id)` for each, + /// then calls `on_frame(dt_ms)` with the elapsed time in milliseconds. + /// + /// Timer and frame work each run with a bounded fuel budget; exhaustion is surfaced as an + /// error or logged to the guest console. pub fn tick(&mut self, dt_ms: u32) -> Result<()> { // Fire any expired timers before the frame update. if let Some(ref on_timer) = self.on_timer_fn { @@ -60,13 +81,22 @@ impl LiveModule { } } +/// Main host-side entry point: Wasmtime engine, shared tab status, and guest-facing host state. +/// +/// Use [`BrowserHost::new`] on the UI thread, then [`BrowserHost::fetch_and_run`] or +/// [`BrowserHost::run_bytes`] to load modules. [`BrowserHost::recreate`] builds a second host +/// that shares [`HostState`] and [`PageStatus`] for background workers. pub struct BrowserHost { wasm_engine: WasmEngine, + /// Latest [`PageStatus`] for this tab, safe to share with worker threads via [`Arc`] and [`Mutex`]. pub status: Arc>, + /// Sandbox resources and host imports: module loader, KV/bookmarks, canvas, timers, console, etc. pub host_state: HostState, } impl BrowserHost { + /// Creates a new host with default [`SandboxPolicy`], a persistent KV store under the platform + /// data directory, and an initialized [`BookmarkStore`]. pub fn new() -> Result { let policy = SandboxPolicy::default(); let wasm_engine = WasmEngine::new(policy.clone())?; @@ -103,7 +133,10 @@ impl BrowserHost { }) } - /// Recreate a BrowserHost sharing existing state (used by background worker threads). + /// Re-creates a [`BrowserHost`] that shares the given [`HostState`] and [`PageStatus`]. + /// + /// Used when worker threads need their own [`WasmEngine`] / Wasmtime instance while keeping + /// bookmarks, KV, canvas handles, and tab status in sync with the main host. pub fn recreate(mut host_state: HostState, status: Arc>) -> Self { let policy = SandboxPolicy::default(); let wasm_engine = WasmEngine::new(policy.clone()).expect("failed to create engine"); @@ -123,9 +156,12 @@ impl BrowserHost { } } - /// Fetch a .wasm binary from a URL, compile it, and run its `start_app` - /// entry point. Supports http(s) and file:// URLs via WHATWG parsing. - /// Returns a `LiveModule` if the guest exports `on_frame`. + /// Fetches a `.wasm` from `url`, compiles it, links the `oxide` imports, runs `start_app()`, + /// and returns a [`LiveModule`] if the guest exports `on_frame`. + /// + /// Updates [`PageStatus`] to [`PageStatus::Loading`] then [`PageStatus::Running`] on success. + /// Supports `http`/`https` (network fetch) and `file://` (local read) via [`OxideUrl`] parsing; + /// other schemes error. pub async fn fetch_and_run(&mut self, url: &str) -> Result> { *self.status.lock().unwrap() = PageStatus::Loading(url.to_string()); self.host_state.canvas.lock().unwrap().commands.clear(); @@ -154,8 +190,11 @@ impl BrowserHost { self.run_module(&wasm_bytes) } - /// Load a .wasm binary from raw bytes (useful for local files). - /// Returns a `LiveModule` if the guest exports `on_frame`. + /// Compiles and runs `wasm_bytes` like [`fetch_and_run`](Self::fetch_and_run), but without a + /// network fetch—useful for in-memory or locally read modules. + /// + /// Sets [`PageStatus::Running`] with a `"(local)"` label. Returns `Some` with a [`LiveModule`] + /// when `on_frame` is exported, otherwise [`None`]. pub fn run_bytes(&mut self, wasm_bytes: &[u8]) -> Result> { self.host_state.canvas.lock().unwrap().commands.clear(); self.host_state.console.lock().unwrap().clear(); diff --git a/oxide-browser/src/ui.rs b/oxide-browser/src/ui.rs index f331809..037610b 100644 --- a/oxide-browser/src/ui.rs +++ b/oxide-browser/src/ui.rs @@ -1,3 +1,13 @@ +//! Desktop GUI for Oxide using [egui](https://github.com/emilk/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe). +//! +//! This module implements a multi-tab browser shell: toolbar, canvas renderer, console panel, +//! bookmarks sidebar, and an about dialog. The canvas draws guest WebAssembly output—rectangles, +//! circles, text, lines, and images—via the host’s draw command stream. Interactive widgets +//! (buttons, checkboxes, sliders, text inputs) are issued by the guest and rendered each frame. +//! Hyperlinks drawn on the canvas are hit-tested so clicks trigger navigation. +//! +//! **Shortcuts:** Cmd+T new tab, Cmd+W close tab, Ctrl+Tab next tab, Cmd+D toggle bookmark for the +//! current page, Cmd+B toggle the bookmarks panel. use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -838,6 +848,8 @@ impl TabState { // ── Application ───────────────────────────────────────────────────────────── +/// Main [`eframe::App`] implementation: owns tab state, bookmarks, and browser chrome (navigation, +/// panels, and dialogs) for the Oxide desktop shell. pub struct OxideApp { tabs: Vec, active_tab: usize, diff --git a/oxide-docs/Cargo.toml b/oxide-docs/Cargo.toml new file mode 100644 index 0000000..03e58ae --- /dev/null +++ b/oxide-docs/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oxide-docs" +version = "0.2.0" +edition = "2021" +description = "Documentation hub for the Oxide browser project — a binary-first browser for WebAssembly" +license = "Apache-2.0" +authors = ["Nikhil Ranjan "] +repository = "https://github.com/nicklabh/oxide" +homepage = "https://docs.oxide.foundation" +documentation = "https://docs.oxide.foundation" + +[lib] +name = "oxide_docs" +path = "src/lib.rs" + +[dependencies] +oxide-sdk = { path = "../oxide-sdk" } +oxide-browser = { path = "../oxide-browser" } diff --git a/oxide-docs/README.md b/oxide-docs/README.md new file mode 100644 index 0000000..3fa3d23 --- /dev/null +++ b/oxide-docs/README.md @@ -0,0 +1,188 @@ +# oxide-docs — Oxide Documentation Hub + +This crate generates the API documentation for the Oxide browser project, +served at **** via GitHub Pages. + +## Building Docs Locally + +```bash +# From the workspace root +cargo doc --no-deps -p oxide-docs -p oxide-sdk -p oxide-browser --open +``` + +The generated docs will be at `target/doc/` and your browser will open to the +`oxide_docs` landing page. + +To build with the custom theme (matching the production site): + +```bash +RUSTDOCFLAGS="--html-in-header oxide-docs/rustdoc-header.html" \ + cargo doc --no-deps -p oxide-docs -p oxide-sdk -p oxide-browser +``` + +## Deployment + +Documentation is automatically deployed to GitHub Pages on every push to +`main` via the `.github/workflows/docs.yml` workflow. The workflow: + +1. Builds rustdoc for `oxide-docs`, `oxide-sdk`, and `oxide-browser` +2. Copies the custom landing page (`oxide-docs/index.html`) to `target/doc/` +3. Deploys to GitHub Pages at + +## How to Add Documentation for New Capabilities + +When you add a new host capability (a new `api_*` function), follow these +steps to ensure it appears in the documentation: + +### Step 1 — Implement the host function + +In `oxide-browser/src/capabilities.rs`, add the `linker.func_wrap(...)` call +inside `register_host_functions()`: + +```rust +linker.func_wrap( + "oxide", + "api_my_feature", + |caller: Caller<'_, HostState>, ptr: u32, len: u32| { + let mem = caller.data().memory.expect("memory not set"); + let value = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default(); + // ... implementation ... + }, +)?; +``` + +### Step 2 — Add the SDK wrapper + +In `oxide-sdk/src/lib.rs`, add the raw FFI import and a safe wrapper: + +```rust +// Inside the `extern "C"` block: +#[link_name = "api_my_feature"] +fn _api_my_feature(ptr: u32, len: u32); + +// Below the extern block, add the safe wrapper with a doc comment: + +/// Brief description of what my_feature does. +/// +/// Longer explanation if needed, including parameter meanings, +/// return values, and usage examples. +/// +/// # Example +/// +/// ```rust,ignore +/// use oxide_sdk::*; +/// +/// my_feature("hello"); +/// ``` +pub fn my_feature(value: &str) { + unsafe { _api_my_feature(value.as_ptr() as u32, value.len() as u32) } +} +``` + +**Documentation requirements for SDK functions:** + +- `///` doc comment on every public function +- Brief one-line summary +- Parameter descriptions if not obvious +- Return value semantics (especially for `Result` and `Option`) +- An `# Example` section with `rust,ignore` code block +- Cross-references using `[`backtick links`]` to related functions + +### Step 3 — Update the docs hub + +In `oxide-docs/src/lib.rs`, add the new function to the appropriate API +category table: + +```rust +//! | [`oxide_sdk::my_feature`] | Brief description | +``` + +If the new capability introduces a whole new category (e.g., "WebRTC", +"GPU"), add a new section heading: + +```rust +//! ## WebRTC +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::rtc_connect`] | Establish a peer connection | +//! | [`oxide_sdk::rtc_send`] | Send data over the connection | +``` + +### Step 4 — Update the SDK crate-level table + +In `oxide-sdk/src/lib.rs`, add the new function to the `## API Categories` +table in the crate-level doc comment: + +```rust +//! | **WebRTC** | [`rtc_connect`], [`rtc_send`], [`rtc_close`] | +``` + +### Step 5 — Add host-side doc comments + +If you added new types to `capabilities.rs` (structs, enums, etc.), add +`///` doc comments: + +```rust +/// State for the WebRTC subsystem. +/// +/// Manages peer connections and data channels for real-time communication +/// between guest modules. +pub struct RtcState { + /// Active peer connections keyed by connection ID. + pub connections: HashMap, +} +``` + +### Step 6 — Verify docs build + +```bash +cargo doc --no-deps -p oxide-docs -p oxide-sdk -p oxide-browser 2>&1 | grep -i warning +``` + +Fix any broken links or missing references before merging. + +### Step 7 — Update DOCS.md + +Add the new API to the `DOCS.md` developer documentation with a usage +example and API reference table entry. + +## Doc Comment Style Guide + +### Module-level comments (`//!`) + +Every `.rs` file with public items should have a `//!` module-level doc +comment explaining: + +- What the module does (one sentence) +- Key types and functions it provides +- How it fits into the overall architecture + +### Item-level comments (`///`) + +Every public item (`pub fn`, `pub struct`, `pub enum`, `pub type`) must +have a `///` doc comment with: + +1. **Summary line** — One sentence describing the item +2. **Details** (if needed) — Parameters, return values, behavior notes +3. **Example** (for SDK functions) — `rust,ignore` code block +4. **Cross-references** — Link to related items with `[`backticks`]` + +### Don't + +- Don't add comments that just repeat the function name +- Don't document private items unless the logic is non-obvious +- Don't use `# Safety` sections unless there's actual `unsafe` code + exposed to consumers + +## File Structure + +``` +oxide-docs/ +├── Cargo.toml # Depends on oxide-sdk and oxide-browser +├── README.md # This file +├── index.html # Custom landing page for docs.oxide.foundation +├── rustdoc-header.html # Custom CSS injected into rustdoc pages +└── src/ + └── lib.rs # Re-exports + comprehensive API reference prose +``` diff --git a/oxide-docs/index.html b/oxide-docs/index.html new file mode 100644 index 0000000..fa73d62 --- /dev/null +++ b/oxide-docs/index.html @@ -0,0 +1,153 @@ + + + + + + Oxide Documentation + + + + +
+

Oxide Docs

+

+ API documentation for the Oxide browser — a binary-first, + decentralized browser that fetches and executes WebAssembly modules + instead of HTML/JavaScript. +

+
+ + + + + + diff --git a/oxide-docs/rustdoc-header.html b/oxide-docs/rustdoc-header.html new file mode 100644 index 0000000..42719be --- /dev/null +++ b/oxide-docs/rustdoc-header.html @@ -0,0 +1,10 @@ + diff --git a/oxide-docs/src/lib.rs b/oxide-docs/src/lib.rs new file mode 100644 index 0000000..b690919 --- /dev/null +++ b/oxide-docs/src/lib.rs @@ -0,0 +1,318 @@ +//! # Oxide — The Binary-First WebAssembly Browser +//! +//! +//! +//! Oxide is a **binary-first browser** that fetches and executes `.wasm` +//! (WebAssembly) modules instead of HTML/JavaScript. Guest applications run +//! in a secure sandbox with zero access to the host filesystem, environment +//! variables, or raw network sockets. The browser exposes a rich set of +//! **capability APIs** that guest modules import to interact with the host. +//! +//! ## Crate Map +//! +//! | Crate | Purpose | Audience | +//! |-------|---------|----------| +//! | [`oxide_sdk`] | Guest SDK — safe Rust wrappers for the `"oxide"` host imports | App developers | +//! | [`oxide_browser`] | Host runtime — Wasmtime engine, sandbox, egui UI | Browser contributors | +//! +//! --- +//! +//! # Quick Start — Building a Guest App +//! +//! ```toml +//! # Cargo.toml +//! [package] +//! name = "my-oxide-app" +//! version = "0.1.0" +//! edition = "2021" +//! +//! [lib] +//! crate-type = ["cdylib"] +//! +//! [dependencies] +//! oxide-sdk = "0.2" +//! ``` +//! +//! ```rust,ignore +//! // src/lib.rs +//! use oxide_sdk::*; +//! +//! #[no_mangle] +//! pub extern "C" fn start_app() { +//! log("Hello from Oxide!"); +//! canvas_clear(30, 30, 46, 255); +//! canvas_text(20.0, 40.0, 28.0, 255, 255, 255, "Welcome to Oxide"); +//! } +//! ``` +//! +//! Build and run: +//! +//! ```bash +//! cargo build --target wasm32-unknown-unknown --release +//! # Open Oxide browser → navigate to your .wasm file +//! ``` +//! +//! --- +//! +//! # Architecture Overview +//! +//! ```text +//! ┌─────────────────────────────────────────────────────┐ +//! │ Oxide Browser │ +//! │ │ +//! │ ┌──────────┐ ┌────────────┐ ┌──────────────────┐ │ +//! │ │ URL Bar │ │ Canvas │ │ Console │ │ +//! │ └────┬─────┘ └──────┬─────┘ └──────┬───────────┘ │ +//! │ │ │ │ │ +//! │ ┌────▼───────────────▼───────────────▼──────────┐ │ +//! │ │ Host Runtime (oxide-browser) │ │ +//! │ │ Wasmtime engine · sandbox policy │ │ +//! │ │ fuel: 500M instructions · memory: 16 MB │ │ +//! │ └────────────────────┬──────────────────────────┘ │ +//! │ │ │ +//! │ ┌────────────────────▼──────────────────────────┐ │ +//! │ │ Capability Provider │ │ +//! │ │ "oxide" wasm import module │ │ +//! │ │ canvas · console · storage · clipboard │ │ +//! │ │ fetch · audio · timers · crypto · navigation │ │ +//! │ │ widgets · input · hyperlinks · protobuf │ │ +//! │ └────────────────────┬──────────────────────────┘ │ +//! │ │ │ +//! │ ┌────────────────────▼──────────────────────────┐ │ +//! │ │ Guest .wasm Module (oxide-sdk) │ │ +//! │ │ exports: start_app(), on_frame(dt_ms) │ │ +//! │ │ imports: oxide::* │ │ +//! │ └───────────────────────────────────────────────┘ │ +//! └─────────────────────────────────────────────────────┘ +//! ``` +//! +//! --- +//! +//! # Guest SDK API Reference +//! +//! The [`oxide_sdk`] crate provides the full guest-side API. All functions +//! are available via `use oxide_sdk::*;`. +//! +//! ## Canvas Drawing +//! +//! The canvas is the main rendering surface. Coordinates start at `(0, 0)` +//! in the top-left corner. +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::canvas_clear`] | Clear canvas with a solid RGBA color | +//! | [`oxide_sdk::canvas_rect`] | Draw a filled rectangle | +//! | [`oxide_sdk::canvas_circle`] | Draw a filled circle | +//! | [`oxide_sdk::canvas_text`] | Draw text at a position | +//! | [`oxide_sdk::canvas_line`] | Draw a line between two points | +//! | [`oxide_sdk::canvas_dimensions`] | Get canvas `(width, height)` in pixels | +//! | [`oxide_sdk::canvas_image`] | Draw an encoded image (PNG, JPEG, GIF, WebP) | +//! +//! ```rust,ignore +//! use oxide_sdk::*; +//! +//! canvas_clear(30, 30, 46, 255); +//! canvas_rect(10.0, 10.0, 200.0, 100.0, 80, 120, 200, 255); +//! canvas_circle(300.0, 200.0, 50.0, 200, 100, 150, 255); +//! canvas_text(20.0, 30.0, 24.0, 255, 255, 255, "Hello!"); +//! canvas_line(0.0, 0.0, 400.0, 300.0, 255, 200, 0, 2.0); +//! +//! let (w, h) = canvas_dimensions(); +//! log(&format!("Canvas: {}x{}", w, h)); +//! ``` +//! +//! ## Console Logging +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::log`] | Print an informational message | +//! | [`oxide_sdk::warn`] | Print a warning (yellow) | +//! | [`oxide_sdk::error`] | Print an error (red) | +//! +//! ## HTTP Networking +//! +//! All network access is mediated by the host — the guest never opens raw +//! sockets. **Protocol Buffers** is the native wire format. +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::fetch`] | Full HTTP request with method, headers, body | +//! | [`oxide_sdk::fetch_get`] | HTTP GET shorthand | +//! | [`oxide_sdk::fetch_post`] | HTTP POST with content-type and body | +//! | [`oxide_sdk::fetch_post_proto`] | HTTP POST with protobuf body | +//! | [`oxide_sdk::fetch_put`] | HTTP PUT | +//! | [`oxide_sdk::fetch_delete`] | HTTP DELETE | +//! +//! ```rust,ignore +//! use oxide_sdk::*; +//! +//! let resp = fetch_get("https://api.example.com/data").unwrap(); +//! log(&format!("Status: {}, Body: {}", resp.status, resp.text())); +//! ``` +//! +//! ## Protobuf — Native Data Format +//! +//! The [`oxide_sdk::proto`] module provides a zero-dependency protobuf +//! encoder/decoder compatible with the Protocol Buffers wire format. +//! +//! ```rust,ignore +//! use oxide_sdk::proto::{ProtoEncoder, ProtoDecoder}; +//! +//! let msg = ProtoEncoder::new() +//! .string(1, "alice") +//! .uint64(2, 42) +//! .bool(3, true) +//! .finish(); +//! +//! let mut decoder = ProtoDecoder::new(&msg); +//! while let Some(field) = decoder.next() { +//! match field.number { +//! 1 => log(&format!("name = {}", field.as_str())), +//! 2 => log(&format!("age = {}", field.as_u64())), +//! _ => {} +//! } +//! } +//! ``` +//! +//! ## Storage +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::storage_set`] | Store a key-value pair (session-scoped) | +//! | [`oxide_sdk::storage_get`] | Retrieve a value by key | +//! | [`oxide_sdk::storage_remove`] | Delete a key | +//! | [`oxide_sdk::kv_store_set`] | Persistent on-disk KV store | +//! | [`oxide_sdk::kv_store_get`] | Read from persistent KV store | +//! | [`oxide_sdk::kv_store_delete`] | Delete from persistent KV store | +//! +//! ## Audio +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::audio_play`] | Play audio from encoded bytes (WAV, MP3, OGG, FLAC) | +//! | [`oxide_sdk::audio_play_url`] | Fetch audio from a URL and play it | +//! | [`oxide_sdk::audio_pause`] / [`oxide_sdk::audio_resume`] / [`oxide_sdk::audio_stop`] | Playback control | +//! | [`oxide_sdk::audio_set_volume`] / [`oxide_sdk::audio_get_volume`] | Volume control (0.0 – 2.0) | +//! | [`oxide_sdk::audio_is_playing`] | Check playback state | +//! | [`oxide_sdk::audio_position`] / [`oxide_sdk::audio_seek`] / [`oxide_sdk::audio_duration`] | Seek and position | +//! | [`oxide_sdk::audio_set_loop`] | Enable/disable looping | +//! | [`oxide_sdk::audio_channel_play`] | Multi-channel simultaneous playback | +//! +//! ## Timers +//! +//! Timer callbacks fire via the guest-exported `on_timer(callback_id)` function. +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::set_timeout`] | One-shot timer after a delay | +//! | [`oxide_sdk::set_interval`] | Repeating timer at an interval | +//! | [`oxide_sdk::clear_timer`] | Cancel a timer | +//! | [`oxide_sdk::time_now_ms`] | Current time (ms since UNIX epoch) | +//! +//! ## Navigation & History +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::navigate`] | Navigate to a new URL | +//! | [`oxide_sdk::push_state`] | Push history entry (like `pushState()`) | +//! | [`oxide_sdk::replace_state`] | Replace current history entry | +//! | [`oxide_sdk::get_url`] | Get current page URL | +//! | [`oxide_sdk::history_back`] / [`oxide_sdk::history_forward`] | Navigate history | +//! +//! ## Input Polling +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::mouse_position`] | Mouse `(x, y)` in canvas coordinates | +//! | [`oxide_sdk::mouse_button_down`] / [`oxide_sdk::mouse_button_clicked`] | Mouse button state | +//! | [`oxide_sdk::key_down`] / [`oxide_sdk::key_pressed`] | Keyboard state | +//! | [`oxide_sdk::scroll_delta`] | Scroll wheel delta | +//! | [`oxide_sdk::shift_held`] / [`oxide_sdk::ctrl_held`] / [`oxide_sdk::alt_held`] | Modifier keys | +//! +//! ## Interactive Widgets +//! +//! Widgets are rendered during the `on_frame()` loop: +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::ui_button`] | Clickable button, returns `true` when clicked | +//! | [`oxide_sdk::ui_checkbox`] | Checkbox, returns current checked state | +//! | [`oxide_sdk::ui_slider`] | Slider, returns current value | +//! | [`oxide_sdk::ui_text_input`] | Text input field, returns current text | +//! +//! ## Crypto & Encoding +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::hash_sha256`] | SHA-256 hash (32-byte array) | +//! | [`oxide_sdk::hash_sha256_hex`] | SHA-256 hash (hex string) | +//! | [`oxide_sdk::base64_encode`] / [`oxide_sdk::base64_decode`] | Base64 encoding/decoding | +//! +//! ## Other APIs +//! +//! | Function | Description | +//! |----------|-------------| +//! | [`oxide_sdk::clipboard_write`] / [`oxide_sdk::clipboard_read`] | System clipboard access | +//! | [`oxide_sdk::random_u64`] / [`oxide_sdk::random_f64`] | Cryptographic random numbers | +//! | [`oxide_sdk::notify`] | Send a notification | +//! | [`oxide_sdk::upload_file`] | Open native file picker | +//! | [`oxide_sdk::get_location`] | Mock geolocation | +//! | [`oxide_sdk::load_module`] | Dynamically load another `.wasm` module | +//! | [`oxide_sdk::register_hyperlink`] / [`oxide_sdk::clear_hyperlinks`] | Canvas hyperlinks | +//! | [`oxide_sdk::url_resolve`] / [`oxide_sdk::url_encode`] / [`oxide_sdk::url_decode`] | URL utilities | +//! +//! --- +//! +//! # Browser Internals +//! +//! The [`oxide_browser`] crate contains the host-side implementation. +//! Key modules for contributors: +//! +//! - **[`oxide_browser::engine`]** — Wasmtime engine setup, [`oxide_browser::engine::SandboxPolicy`], +//! fuel metering, bounded linear memory +//! - **[`oxide_browser::runtime`]** — [`oxide_browser::runtime::BrowserHost`] orchestrates module +//! fetching, compilation, and execution. [`oxide_browser::runtime::LiveModule`] keeps interactive +//! apps alive across frames. +//! - **[`oxide_browser::capabilities`]** — The `"oxide"` import module: every host function the +//! guest can call is registered here via `register_host_functions()`. Also contains shared state +//! types ([`oxide_browser::capabilities::HostState`], [`oxide_browser::capabilities::CanvasState`], +//! [`oxide_browser::capabilities::InputState`], etc.). +//! - **[`oxide_browser::navigation`]** — [`oxide_browser::navigation::NavigationStack`] implements +//! browser-style back/forward history with opaque state. +//! - **[`oxide_browser::bookmarks`]** — [`oxide_browser::bookmarks::BookmarkStore`] provides +//! persistent bookmark storage backed by sled. +//! - **[`oxide_browser::url`]** — [`oxide_browser::url::OxideUrl`] wraps WHATWG URL parsing with +//! support for `http`, `https`, `file`, and `oxide://` schemes. +//! - **[`oxide_browser::ui`]** — [`oxide_browser::ui::OxideApp`] is the egui/eframe application +//! with tabbed browsing, toolbar, canvas rendering, console panel, and bookmarks sidebar. +//! +//! --- +//! +//! # Guest Module Contract +//! +//! Every `.wasm` module loaded by Oxide must: +//! +//! 1. **Export `start_app`** — `extern "C" fn()` entry point called on load +//! 2. **Optionally export `on_frame`** — `extern "C" fn(dt_ms: u32)` for +//! interactive apps with a render loop +//! 3. **Optionally export `on_timer`** — `extern "C" fn(callback_id: u32)` +//! to receive timer callbacks +//! 4. **Import from `"oxide"`** — all host APIs live under this namespace +//! 5. **Compile as `cdylib`** — `crate-type = ["cdylib"]` in `Cargo.toml` +//! 6. **Target `wasm32-unknown-unknown`** — no WASI, pure capability-based +//! +//! --- +//! +//! # Security Model +//! +//! | Constraint | Value | Purpose | +//! |-----------|-------|---------| +//! | Filesystem access | None | Guest cannot read/write host files | +//! | Environment variables | None | Guest cannot inspect host env | +//! | Raw network sockets | None | All networking is mediated via `fetch` | +//! | Memory limit | 16 MB (256 pages) | Prevents memory exhaustion | +//! | Fuel limit | 500M instructions | Prevents infinite loops / DoS | +//! | No WASI | — | Zero implicit system access | + +pub use oxide_browser; +pub use oxide_sdk; diff --git a/oxide-sdk/src/lib.rs b/oxide-sdk/src/lib.rs index 68a0852..75c7d17 100644 --- a/oxide-sdk/src/lib.rs +++ b/oxide-sdk/src/lib.rs @@ -1,11 +1,24 @@ //! # Oxide SDK //! //! Guest-side SDK for building WebAssembly applications that run inside the -//! Oxide browser. This crate provides safe Rust wrappers around the raw -//! host-imported functions exposed by the `"oxide"` module. +//! [Oxide browser](https://github.com/niklabh/oxide). This crate provides +//! safe Rust wrappers around the raw host-imported functions exposed by the +//! `"oxide"` wasm import module. //! //! ## Quick Start //! +//! Add `oxide-sdk` to your `Cargo.toml` and set `crate-type = ["cdylib"]`: +//! +//! ```toml +//! [lib] +//! crate-type = ["cdylib"] +//! +//! [dependencies] +//! oxide-sdk = "0.2" +//! ``` +//! +//! Then write your app: +//! //! ```rust,ignore //! use oxide_sdk::*; //! @@ -16,6 +29,54 @@ //! canvas_text(20.0, 40.0, 28.0, 255, 255, 255, "Welcome to Oxide"); //! } //! ``` +//! +//! Build with `cargo build --target wasm32-unknown-unknown --release`. +//! +//! ## Interactive Apps +//! +//! For apps that need a render loop, export `on_frame`: +//! +//! ```rust,ignore +//! use oxide_sdk::*; +//! +//! #[no_mangle] +//! pub extern "C" fn start_app() { +//! log("Interactive app started"); +//! } +//! +//! #[no_mangle] +//! pub extern "C" fn on_frame(_dt_ms: u32) { +//! canvas_clear(30, 30, 46, 255); +//! let (mx, my) = mouse_position(); +//! canvas_circle(mx, my, 20.0, 255, 100, 100, 255); +//! +//! if ui_button(1, 20.0, 20.0, 100.0, 30.0, "Click me!") { +//! log("Button was clicked!"); +//! } +//! } +//! ``` +//! +//! ## API Categories +//! +//! | Category | Functions | +//! |----------|-----------| +//! | **Canvas** | [`canvas_clear`], [`canvas_rect`], [`canvas_circle`], [`canvas_text`], [`canvas_line`], [`canvas_image`], [`canvas_dimensions`] | +//! | **Console** | [`log`], [`warn`], [`error`] | +//! | **HTTP** | [`fetch`], [`fetch_get`], [`fetch_post`], [`fetch_post_proto`], [`fetch_put`], [`fetch_delete`] | +//! | **Protobuf** | [`proto::ProtoEncoder`], [`proto::ProtoDecoder`] | +//! | **Storage** | [`storage_set`], [`storage_get`], [`storage_remove`], [`kv_store_set`], [`kv_store_get`], [`kv_store_delete`] | +//! | **Audio** | [`audio_play`], [`audio_play_url`], [`audio_pause`], [`audio_resume`], [`audio_stop`], [`audio_set_volume`], [`audio_channel_play`] | +//! | **Timers** | [`set_timeout`], [`set_interval`], [`clear_timer`], [`time_now_ms`] | +//! | **Navigation** | [`navigate`], [`push_state`], [`replace_state`], [`get_url`], [`history_back`], [`history_forward`] | +//! | **Input** | [`mouse_position`], [`mouse_button_down`], [`key_down`], [`key_pressed`], [`scroll_delta`] | +//! | **Widgets** | [`ui_button`], [`ui_checkbox`], [`ui_slider`], [`ui_text_input`] | +//! | **Crypto** | [`hash_sha256`], [`hash_sha256_hex`], [`base64_encode`], [`base64_decode`] | +//! | **Other** | [`clipboard_write`], [`clipboard_read`], [`random_u64`], [`random_f64`], [`notify`], [`upload_file`], [`load_module`] | +//! +//! ## Full API Documentation +//! +//! See for the complete API +//! reference, or browse the individual function documentation below. pub mod proto;