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,GetRibbonDispatch { cmd: String }InteractiveEvent { command_id, event }Shutdown
Plugin → host:
PushInfo/PushOutput/PushErrorAddEntity(SerializedEntity)→HandleBumpGeometry,PushUndo,SetDirtyReadRecord,WriteRecord,RemoveRecordStartInteractive,PollInteractiveDocumentSnapshot→SerializedDocument
SerializedEntity, SerializedRecord, and SerializedDocument are acadrust types with Serialize / Deserialize derived.
4.2 Plugin-side runtime
OpenCADStudio --ocs-plugin-runner <socket_name> <cdylib_path>:
- Loads the cdylib, validates
ocs_plugin_api_version, callsocs_plugin_register. - Connects to the host socket and answers
GetManifest/GetRibbon. - 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
src/plugin/external.rs— replacelibloading-basedLoadedPluginwithPluginProcess::spawn.src/plugin/registry.rs— usePluginProcessfor ribbon collection and command dispatch.src/app/plugin_host.rs— add an IPC request bridge that maps incoming messages toHostSessioncalls.
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
- Add dependencies to
Cargo.toml. - Implement framed transport and protocol messages.
- Implement runner logic in
runner.rs. - Implement
PluginHostApiproxy. - Implement host-side server and
PluginManager. - Add owned ribbon conversions.
- Wire the three host call sites and add
--ocs-plugin-runnerdispatch insrc/main.rs. - Bump
API_VERSIONand updatedocs/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
LoadedPluginis replaced byPluginProcess.
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.