cad-editor/crates/ocs_plugin_api/REPORT.md
2026-06-25 00:01:36 +03:00

8.1 KiB

Out-of-Process Plugin Architecture — ocs_plugin_api

Status: Design proposal
Scope: crates/ocs_plugin_api only (minimal host wiring, no plugin changes, no new crate)


1. Problem

The host currently loads external add-ons as cdylib libraries into its own process via libloading. panic::catch_unwind catches Rust panics, but a plugin can still corrupt host memory, segfault, or deadlock the UI thread. The fix is to run each plugin as a separate OS process and mediate all interaction through IPC.


2. Design

ocs_plugin_api becomes a dual-use library:

  • Plugin side: unchanged API surface (BuiltinPlugin, HostApi, CadModule, export_plugin!).
  • Host side: runtime that spawns plugin processes and handles their IPC requests.

Plugins remain cdylibs. The host spawns itself in runner mode (--ocs-plugin-runner <socket> <cdylib>) to load each cdylib in a child process and bridge to the host over interprocess::local_socket. The runner implementation lives inside ocs_plugin_api as a library module; no separate helper binary is needed, so the runner and host are always the same build and cannot get out of sync at deployment time.


3. Constraints

Constraint Handling
Plugin API unchanged Trait/type signatures preserved. document() / document_mut() keep their signatures but return a local cached copy, so API_VERSION bumps to 3.
Only ocs_plugin_api modified All new code lives here. The host needs only minimal call-site wiring in src/plugin/external.rs, src/plugin/registry.rs, and src/app/plugin_host.rs.
No new crate Runner code lives inside ocs_plugin_api; the host executable serves as the runner process.
Platform-independent interprocess::local_socket uses named pipes on Windows and Unix domain sockets elsewhere; self-spawning works on every host target.

4. Architecture

Host process                          Plugin process
┌─────────────────┐  local socket   ┌─────────────────┐
│ HostSession     │◄───────────────►│ HostApi proxy   │
│ (document, UI)  │  bincode frames │ (sends RPCs)    │
└─────────────────┘                 └─────────────────┘
        ▲                                    │
        │                                    ▼
 PluginManager                          cdylib loaded
 (spawn / kill /                        by host in
  supervise)                             runner mode

4.1 IPC protocol

All messages are length-framed and serialized with bincode.

Host → plugin:

  • GetManifest, GetRibbon
  • Dispatch { cmd: String }
  • InteractiveEvent { command_id, event }
  • Shutdown

Plugin → host:

  • PushInfo / PushOutput / PushError
  • AddEntity(SerializedEntity)Handle
  • BumpGeometry, PushUndo, SetDirty
  • ReadRecord, WriteRecord, RemoveRecord
  • StartInteractive, PollInteractive
  • DocumentSnapshotSerializedDocument

SerializedEntity, SerializedRecord, and SerializedDocument are acadrust types with Serialize / Deserialize derived.

4.2 Plugin-side runtime

OpenCADStudio --ocs-plugin-runner <socket_name> <cdylib_path>:

  1. Loads the cdylib, validates ocs_plugin_api_version, calls ocs_plugin_register.
  2. Connects to the host socket and answers GetManifest / GetRibbon.
  3. Runs a request loop: dispatch commands, forward interactive events.

PluginHostApi implements HostApi by sending RPCs. document() / document_mut() return a local copy fetched from DocumentSnapshot; mutations are not automatically synced back. Plugins use add_entity, write_record, etc. for host-visible changes.

4.3 Host-side runtime

ocs_plugin_api::process provides:

  • PluginProcess::spawn(cdylib_path) — creates a socket, launches the host executable in runner mode, accepts its connection.
  • PluginManager — spawn all discovered plugins, supervise, kill.
  • serve_plugin_connection(stream, &mut dyn HostApi) — host-side request handler.

The host creates a HostSession and passes it to serve_plugin_connection; existing document/undo logic is reused.

4.4 Ribbon

Ribbon types use &'static str, which cannot cross a socket. Define owned equivalents in ocs_plugin_api::ribbon::owned and convert for IPC. The host reconstructs RibbonGroup once per load (e.g., by leaking the owned strings), avoiding changes to ribbon rendering code.

4.5 Failure handling

Failure Behavior
Plugin crash / hang / malformed message Host marks plugin dead, drops its ribbon tab, logs the error, and continues running.
Plugin panics Caught inside the runner; an error response is returned to the host.
Spawn failure Reported through PluginManager and shown in the Plugin Manager.

5. Host Integration Points

  1. src/plugin/external.rs — replace libloading-based LoadedPlugin with PluginProcess::spawn.
  2. src/plugin/registry.rs — use PluginProcess for ribbon collection and command dispatch.
  3. src/app/plugin_host.rs — add an IPC request bridge that maps incoming messages to HostSession calls.

No changes to docs/plugin-template or any other plugin.


6. Crate Changes

6.1 New files inside crates/ocs_plugin_api

src/
  ipc/
    protocol.rs      # HostRequest / PluginRequest / PluginResponse
    transport.rs     # framed read/write over local_socket
    client.rs        # plugin-side IpcClient + PluginHostApi
    server.rs        # host-side serve_plugin_connection
  process.rs         # PluginProcess / PluginManager
  runner.rs          # plugin runner logic invoked by host in runner mode

6.2 Dependencies

Add under the existing host feature:

[dependencies]
interprocess = { version = "2", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
bincode = { version = "1", optional = true }
thiserror = { version = "1", optional = true }
libloading = { version = "0.8", optional = true }

[features]
host = ["dep:acadrust", "dep:interprocess", "dep:serde", "dep:bincode", "dep:thiserror", "dep:libloading"]

7. API Version

Bump API_VERSION to 3 because document() / document_mut() semantics change from direct host references to local cached copies. v2 plugins are refused as usual; plugin authors recompile with ApiVersion::CURRENT.


8. Implementation Plan

  1. Add dependencies to Cargo.toml.
  2. Implement framed transport and protocol messages.
  3. Implement runner logic in runner.rs.
  4. Implement PluginHostApi proxy.
  5. Implement host-side server and PluginManager.
  6. Add owned ribbon conversions.
  7. Wire the three host call sites and add --ocs-plugin-runner dispatch in src/main.rs.
  8. Bump API_VERSION and update docs/plugin-architecture.md.

9. Testing

  • Unit: protocol round-trip, ribbon conversion, proxy request emission.
  • Integration: spawn a test plugin, verify dispatch and interactive command round-trip, kill the process and confirm the host survives.
  • Host: update registry tests once LoadedPlugin is replaced by PluginProcess.

10. Compliance with AGENT.md

Requirement Status
ocs_plugin_api is a library, not a plugin Yes
Plugin API source-compatible Yes; signatures are unchanged. document() semantics change is gated by v3.
Separate processes + failure management Yes
Platform-independent interprocess IPC Yes
Only ocs_plugin_api modified Code: yes. Host needs minimal wiring; unavoidable because OpenCADStudio / HostSession are host-private.
No new crate Yes
Memory / process isolation Yes

11. Summary

ocs_plugin_api absorbs the plugin runtime: the host spawns itself in runner mode to load each cdylib in its own process, and all host/plugin interaction is serialized over interprocess local sockets. Plugin API signatures stay intact, host changes are limited to a few call sites, and no new crate is introduced.