# External Extensibility

## ABI-unstable, rebuild-enforced

Rust has no stable ABI. We accept this reality and design with it.

The primary extensibility mechanism is **Cargo dependency**:

```toml
# External module crate
[dependencies]
reovim-client-driver = "0.X"   # trait contracts
```

The user adds the module to their platform binary and rebuilds:

```toml
# reovim-tui/Cargo.toml
[dependencies]
reovim-common-client = "0.X"
my-custom-module = "0.1"
```

```rust
// reovim-tui/src/main.rs
fn register_modules(registry: &mut ModuleRegistry) {
    registry.register(Box::new(statusline::StatuslineModule::new()));
    registry.register(Box::new(line_numbers::LineNumbersModule::new()));
    // External module — same API, same trait, no FFI
    registry.register(Box::new(my_custom_module::MyModule::new()));
}
```

No FFI boundary. No ABI compatibility matrix. Pure Rust trait objects.

## Why this is the right trade-off

What you get:
- Full type safety across the module boundary
- No ABI version mismatches, no `unsafe` in the extensibility mechanism
- Modules work on every platform (TUI, Web/WASM, Mobile)
- Cargo handles version compatibility
- Module authors publish to crates.io like any Rust library

What you give up:
- Hot-swapping modules without restarting the editor
- Distributing as binary .so to users without a Rust toolchain

The second trade-off is acceptable:
- Package managers can provide pre-built binaries with custom module sets
- Development-time hot-reload via same-toolchain `.so` is available (see below)

## No God Crate

There is no "defaults" meta-crate that aggregates all modules.

The platform binary explicitly registers modules via `register_modules()`.
Each binary chooses its own module set. No magic, no link-time tricks,
works on every target including WASM.

Different binaries include different module sets. The TUI binary might
include 15 modules. A minimal Web binary might include 5.

## Dynamic loading (secondary, development only)

For development iteration, same-toolchain `.so` loading is available:

```rust
// Generated by declare_client_module!
#[no_mangle]
pub extern "C" fn reovim_client_module_create() -> *mut dyn ClientModule;

#[no_mangle]
pub extern "C" fn reovim_client_module_destroy(ptr: *mut dyn ClientModule);

// Toolchain hash for same-toolchain validation
#[no_mangle]
pub static REOVIM_TOOLCHAIN_HASH: &str = env!("REOVIM_TOOLCHAIN_HASH");
```

The loader checks `REOVIM_TOOLCHAIN_HASH`. Mismatch = reject with clear error.

**Note on fat-pointer FFI**: `*mut dyn ClientModule` is a fat pointer (data +
vtable). Its layout is NOT stable across Rust versions, even with the same
toolchain hash for different compilation units. This is known unsound for
cross-compilation-unit use. For development convenience with identical toolchains
and compiler invocations, it works in practice. This is NOT the primary
extensibility story — it is a development shortcut. Production modules are
compiled-in via Cargo.

### Discovery paths (dynamic only)

```
$REOVIM_MODULE_PATH          (highest priority, colon-separated)
~/.config/reovim/modules/
$XDG_DATA_HOME/reovim/modules/
/usr/lib/reovim/modules/
/usr/local/lib/reovim/modules/
```

## Server-side pairing

Most features need both a server module (behavior) and a client module
(presentation). They pair by `kind` string from `shared/extension-kinds/`.

```
Server module (diagnostics):
    pushes ExtensionUpdatedPayload { kind: "diagnostics", data: "..." }

Client module (diagnostics):
    fn kind() -> "diagnostics"     // matches server kind
    fn on_notification(data)       // parses the JSON
```

External authors ship both crates — server module + client module —
connected by a shared kind string and JSON schema.

## Performance Model

### Two-phase contract

**Phase 1 — Data collection (on state change):**
Triggered by `on_*` events. Modules update caches. Allocation permitted.
O(changes), not O(frames).

**Phase 2 — Render (every frame):**
CORE reads cached data via getter methods. Guidelines:
- Minimize allocation — prefer pre-built cached structures
- No I/O, no blocking, no locks
- O(visible_lines) at worst

"Minimize allocation" is a guideline, not a hard constraint. Rust has no
`#[no_alloc]`. Small allocations are acceptable when the alternative is
impractical. The real constraints are **no I/O, no blocking, no expensive
computation**.

Modules that receive render-time bounds (completion popups, overlays) may
format content within `chrome_render` — Phase 1 caching applies to content
independent of bounds.

### Cache invalidation

Modules own their caches. The `on_*` events ARE the invalidation signals.
Module's responsibility, not CORE's. Same model as Wayland client buffers.

### The tick exception

`tick()` runs every frame. Must be O(1) — check a timer, flip a bool.
Return true if visual output changed and re-render is needed.

## Error Handling

**Module panic during render:**
CORE catches via `catch_unwind`, marks module as faulted, calls `exit()`,
excludes from future cycles. Requires `panic = unwind` in the binary profile.

**Faulted module cleanup:**
CORE removes the module's registry entries to prevent stale data. Any
`Arc`-held resources clean up when last reference drops.

**Module load failure:**
Loader logs error, skips module. Client starts with whatever loaded.

**Capability mismatch:**
Module stays dormant — returns false from `has_*()`. No error.
