Jamie Pine 49c3ff9f5e feat: Introduce Spacedrive SDK for building WASM extensions
- Added a new SDK for developing Spacedrive WASM extensions, featuring a type-safe API and comprehensive documentation.
- Implemented core components including job context management, FFI bindings, and error handling mechanisms.
- Created macros for simplifying extension and job definitions, enhancing developer experience.
- Included detailed README and status documentation to guide users in utilizing the SDK effectively.
- Updated Cargo configurations for the SDK and its macros, ensuring proper dependency management and build processes.
2025-10-09 04:00:12 -07:00
..

Spacedrive Extension SDK

Beautiful, type-safe API for building Spacedrive WASM extensions.

Installation

Add to your extension's Cargo.toml:

[dependencies]
spacedrive-sdk = { path = "../spacedrive-sdk" }

Quick Start

use spacedrive_sdk::prelude::*;
use spacedrive_sdk::ExtensionContext;

#[no_mangle]
pub extern "C" fn plugin_init() -> i32 {
    spacedrive_sdk::ffi::log_info("Extension started!");
    0 // Success
}

fn process_receipt(ctx: &ExtensionContext, pdf_data: &[u8]) -> Result<Uuid> {
    // Create entry
    let entry = ctx.vdfs().create_entry(CreateEntry {
        name: "Receipt: Starbucks".into(),
        path: "receipts/1.pdf".into(),
        entry_type: "FinancialDocument".into(),
        metadata: None,
    })?;

    // Run OCR
    let ocr_result = ctx.ai().ocr(pdf_data, OcrOptions::default())?;

    // Store result
    ctx.vdfs().write_sidecar(entry.id, "ocr.txt", ocr_result.text.as_bytes())?;

    // Classify with AI
    let receipt = ctx.ai().classify_text(
        &ocr_result.text,
        "Extract vendor, amount, date from this receipt. Return JSON."
    )?;

    ctx.vdfs().write_sidecar(entry.id, "receipt.json",
        serde_json::to_vec(&receipt)?.as_slice()
    )?;

    Ok(entry.id)
}

No Unsafe, No FFI, Just Clean Rust

Before (raw C bindings):

#[link(wasm_import_module = "spacedrive")]
extern "C" {
    fn spacedrive_call(
        method_ptr: *const u8,
        method_len: usize,
        library_id_ptr: u32,
        payload_ptr: *const u8,
        payload_len: usize
    ) -> u32;
}

// Then manually:
// - Serialize to JSON
// - Get pointer to string
// - Call unsafe function
// - Read result from returned pointer
// - Deserialize JSON
// - Handle errors

After (with SDK):

let entry = ctx.vdfs().create_entry(CreateEntry { ... })?;
let ocr = ctx.ai().ocr(&pdf_data, OcrOptions::default())?;

That's it! Clean, type-safe, ergonomic.

API Reference

VDFS Operations

// Create entries
let entry = ctx.vdfs().create_entry(CreateEntry {
    name: "My File".into(),
    path: "path/to/file".into(),
    entry_type: "Document".into(),
    metadata: None,
})?;

// Write sidecars (store metadata/analysis)
ctx.vdfs().write_sidecar(entry.id, "metadata.json", data)?;

// Read sidecars
let data = ctx.vdfs().read_sidecar(entry.id, "metadata.json")?;

// Update metadata
ctx.vdfs().update_metadata(entry.id, json!({ "category": "Important" }))?;

AI Operations

// OCR
let ocr_result = ctx.ai().ocr(&pdf_bytes, OcrOptions {
    language: "eng".into(),
    engine: OcrEngine::Tesseract,
    preprocessing: true,
})?;

// Text classification
let result = ctx.ai().classify_text(
    &text,
    "Extract structured data from this receipt"
)?;

// Embeddings
let embedding = ctx.ai().embed("search query text")?;

Credential Management

// Store OAuth token
ctx.credentials().store("gmail_oauth", Credential::oauth2(
    access_token,
    Some(refresh_token),
    3600, // expires_in_seconds
    vec!["https://www.googleapis.com/auth/gmail.readonly".into()]
))?;

// Get credential (auto-refreshes if OAuth)
let cred = ctx.credentials().get("gmail_oauth")?;

// Delete credential
ctx.credentials().delete("old_credential")?;

Job System

// Dispatch background job
let job_id = ctx.jobs().dispatch("email_scan", json!({
    "provider": "gmail"
}))?;

// Check status
match ctx.jobs().get_status(job_id)? {
    JobStatus::Running { progress } => {
        ctx.log(&format!("Job {}% complete", progress * 100.0));
    }
    JobStatus::Completed => {
        ctx.log("Job done!");
    }
    _ => {}
}

// Cancel job
ctx.jobs().cancel(job_id)?;

Building Your Extension

# Build for release (optimized for size)
cargo build --target wasm32-unknown-unknown --release

# Output: target/wasm32-unknown-unknown/release/your_extension.wasm

Required Exports

Your extension must export these functions:

#[no_mangle]
pub extern "C" fn plugin_init() -> i32 {
    spacedrive_sdk::ffi::log_info("Extension starting!");
    0 // Return 0 for success
}

#[no_mangle]
pub extern "C" fn plugin_cleanup() -> i32 {
    spacedrive_sdk::ffi::log_info("Extension cleanup");
    0 // Return 0 for success
}

The SDK automatically provides wasm_alloc and wasm_free - you don't need to implement them!

manifest.json

{
  "id": "my-extension",
  "name": "My Extension",
  "version": "0.1.0",
  "description": "What my extension does",
  "author": "Your Name",
  "wasm_file": "my_extension.wasm",
  "permissions": {
    "methods": ["vdfs.", "ai.ocr", "credentials."],
    "libraries": ["*"],
    "rate_limits": {
      "requests_per_minute": 1000
    },
    "max_memory_mb": 512
  }
}

Error Handling

All operations return Result<T, Error>:

use spacedrive_sdk::prelude::*;

fn my_operation(ctx: &ExtensionContext) -> Result<()> {
    let entry = ctx.vdfs().create_entry(...)?;  // ? operator works!
    ctx.vdfs().write_sidecar(entry.id, "data.json", data)?;
    Ok(())
}

Logging

ctx.log("Info message");
ctx.log_error("Error message");

// Or directly:
spacedrive_sdk::ffi::log_info("Message");
spacedrive_sdk::ffi::log_debug("Debug");
spacedrive_sdk::ffi::log_warn("Warning");
spacedrive_sdk::ffi::log_error("Error");

Examples

See extensions/test-extension/ for a complete working example.


Beautiful API. Zero unsafe code. Just Rust. 🦀