Files
OrcaSlicer/docs/plugins/plugin_system.md
2026-07-02 17:49:36 +08:00

21 KiB
Raw Blame History

Plugin System Overview

OrcaSlicer can be extended at runtime with Python plugins that execute inside an embedded CPython interpreter — no recompilation, no patching the C++ core. This document is the architectural overview: what the pieces are, how they fit together, and the lifecycle of a plugin from discovery to teardown.

It is the map; the other two plugin docs are the detail:

  • plugin_development.md — how to write a Python plugin and how to add a new plugin type in C++ (the authoring/extension guide).
  • plugin_audit_hook.md — the CPython audit hook that constrains what plugin code may do (the security deepdive).

All paths below are under src/slic3r/plugin/ unless stated otherwise.


What the system provides

  • Extensibility without rebuilding — users drop a plugin into a folder (or subscribe to one from the cloud) and OrcaSlicer loads it.
  • Capabilities, not singlepurpose plugins — one plugin is a package that registers one or more capabilities, each a typed unit of functionality (e.g. post-processing, script, printer-connection). Each capability type has a fixed C++ entry point and is invoked at a specific place in the app; a plugin's "types" are simply the set of capability types it registers.
  • Presets remember the plugins they use — when a preset references a plugin capability, the full reference is stored in the preset and can be restored from OrcaCloud on another machine (see Plugin references in presets).
  • A single, narrow API surface — plugins see only the embedded orca module, not the slicer internals.
  • A security boundary — file access by plugin code is filtered by an audit hook with a write allowlist (groundwork; see the audit doc for current scope).
  • Isolation of failure — a misbehaving plugin reports an error and is unloaded rather than taking down the app; tracebacks are persisted to a log file.

Architecture at a glance

                         ┌──────────────────────────────────────────────┐
        app startup ───► │  PluginManager  (singleton orchestrator)       │
   (GUI_App::OnInit)     │   owns: CloudPluginService, PluginCatalog,     │
                         │         PluginLoader                           │
                         └───────┬───────────────┬───────────────┬───────┘
                                 │               │               │
                 discover (scan) │     install/  │   load/unload │
                                 ▼      download  ▼               ▼
                    ┌──────────────────┐  ┌───────────────┐  ┌────────────────────┐
                    │  PluginCatalog   │  │ CloudPlugin   │  │   PluginLoader     │
                    │  manifest-only   │  │ Service       │  │  threaded loads,   │
                    │  inventory of    │  │ (cloud fetch/ │  │  deps (uv), audit  │
                    │  PluginDescriptor│  │  download)    │  │  key, capabilities │
                    └──────────────────┘  └───────────────┘  └─────────┬──────────┘
                                                                       │ instantiates via
                                                                       ▼
   ┌─────────────────────────────────────────────────────────────────────────────────┐
   │                       Embedded CPython  (PythonInterpreter, singleton)            │
   │   PythonPluginBridge → `orca` module + @orca.plugin/register_capability + capture │
   │   PyPluginTrampoline → C++↔Python call boundary (traceback logging + audit scope) │
   │   PluginAuditManager → CPython audit hook (filesystem policy)                     │
   │   pluginTypes/* (gcode, script, printerAgent) → typed capability bases + tramps   │
   └─────────────────────────────────────────────────────────────────────────────────┘
                                                                       │ get_plugin_capability_* + dynamic_pointer_cast
                                                                       ▼
   workflow call sites:  PostProcessor (G-code post-processing) ·
                         PluginsDialog "Run" (script) · NetworkAgentFactory (printer agent)

Two broad layers:

  • Orchestration (C++, no Python): PluginManager, PluginCatalog, CloudPluginService, PluginLoader, PluginDescriptor. These discover, install, and manage plugins as data.
  • Execution (the C++↔Python bridge): PythonInterpreter, PythonPluginBridge, PyPluginTrampoline, PluginAuditManager, and the pertype bases under pluginTypes/. These turn a discovered plugin into a live object the app can call.

Core components

Component Responsibility
PluginManager Toplevel singleton orchestrator. Owns the catalog, loader, and cloud service; exposes initialize(), discover_plugins(), install/update/delete, and shutdown().
PluginCatalog Manifestonly inventory. Scans the plugin directories, parses each plugin's metadata into a PluginDescriptor, and splits results into valid vs. invalid. Loads no Python.
CloudPluginService Thin wrapper over the cloud agent: fetch subscribed/owned plugin manifests, download a plugin payload, unsubscribe/delete.
PluginLoader Load/unload lifecycle. Installs dependencies (bundled uv), imports the module, instantiates the package and its capabilities, stamps their audit identity, runs on_load(), and keeps the live capability instances keyed by a PluginCapabilityIdentifier. Provides get_plugin_capabilities_by_type() / get_plugin_capability_by_name() and onload/unload + oncapabilityload/unload callbacks.
PluginDescriptor The canonical record for one plugin: key, paths, capability/display types, version, changelog, dependencies, cloud overlay, and any error/validity state.
PythonInterpreter Singleton RAII wrapper around embedded CPython. Init/finalize, GIL handoff, sys.path, module loading, and installing the audit hook + stderrtolog redirect.
PythonPluginBridge Defines the embedded orca module, the @orca.plugin decorator + orca.base package class + register_capability entry, and captures/instantiates the package and the capability classes it registers.
PyPluginTrampoline The pybind11 override base at the C++↔Python boundary: logs Python tracebacks and opens the percall audit scope.
pluginTypes/* Pertype C++ capability bases + trampolines (GCodePluginCapability, ScriptPluginCapability, PrinterAgentPluginCapability) that define each type's entry method and dispatch.
PluginAuditManager Singleton CPython audit hook: filesystem policy (write allowlist), scoped roots, Loading/Enforcing modes. See the audit doc.

Plugin packaging and discovery

A plugin is a folder under one of two roots, containing a single entry file:

Root Source
data_dir()/orca_plugins/ locally installed / sideloaded
data_dir()/orca_plugins/_subscribed/<user_id>/ cloudsubscribed (per loggedin user)

The entry file is either a single .py (metadata in a PEP 723 comment block) or a .whl wheel (metadata from the wheel's METADATA). The capabilities the plugin registers determine which workflows can run it — there is no separate type declaration in the metadata. Metadata and packaging details are in plugin_development.md.

Discovery vs. loading are separate stages. PluginCatalog scans directories and produces PluginDescriptors — it parses manifests only and never executes plugin code. A catalog entry is just data; a loaded plugin is a live Python instance created later by PluginLoader. Cloud manifests are merged into the catalog as an overlay once a user is logged in.


The plugin lifecycle

1. App startup (GUI_App::OnInit, after network init)
        │
2. PluginManager::initialize()
        │   └─ PythonInterpreter::initialize()  (MAIN THREAD ONLY)
        │        ├─ start embedded CPython, set sys.path / python home
        │        ├─ install the audit hook (global allowed root = data_dir())
        │        ├─ tee sys.stderr → data_dir()/log/python_*.log
        │        └─ release the GIL (PyEval_SaveThread)
        │
3. discover_plugins()  ─► PluginCatalog scans local + cloud roots
        │                   → PluginDescriptor list (valid / invalid)
        │   (cloud login later: fetch_plugins_from_cloud → catalog overlay)
        │
4. PluginLoader::load_plugin()  (worker thread, serialized)
        │   ├─ install dependencies via bundled `uv`; extract bundled .whl deps onto sys.path
        │   ├─ begin capture → import module (runs @orca.plugin, marking the package class)
        │   ├─ finalize capture → instantiate package, call register_capabilities(),
        │   │      then instantiate each registered capability and cache its get_name()
        │   ├─ set_audit_plugin_key(descriptor.plugin_key)   // audit identity
        │   ├─ on_load()  (under the GIL)
        │   └─ store the capabilities; fire on-load + on-capability-load callbacks
        │
5. Use:  a workflow call site resolves a capability (get_plugin_capability_by_name /
        │   get_plugin_capabilities_by_type) + dynamic_pointer_cast<TypeCapability>,
        │   builds the type's context, and calls the entry method (under the GIL).
        │   Each call crosses a trampoline that opens a ScopedPluginAuditContext.
        │
6. Unload / shutdown:  set_shutting_down → unload_plugin / unload_all_plugins
            (the instance's destructor runs on_unload() + Py_DECREF under the GIL)
            → PythonInterpreter::shutdown()

A few loadtime invariants worth knowing:

  • set_audit_plugin_key() is what arms enforcement. Without it the instance has an empty key and its calls run unaudited. It is stamped at load and restamped on key migration (update_loaded_plugin_key). See the audit doc.
  • A module must mark exactly one package class with @orca.plugin (a subclass of orca.base), and that class's register_capabilities() must register at least one valid capability via orca.register_capability(...), or the load fails. Each capability must resolve get_name(), and (type, name) must be unique within the plugin.

Execution model: how the app calls a plugin

Capabilities are reached by type, not by name. There is no pertype instantiation registry: a capability's Python class subclasses a typed C++ base, the package registers it via register_capability, and each workflow call site narrows the stored capability instance (PluginCapabilityInterface) with std::dynamic_pointer_cast<ConcreteType>. If the cast succeeds, the capability is present and is invoked; if not (no such capability installed or enabled), the path is a noop — which is how the system guarantees that absent/disabled capabilities never change existing behavior.

Capability type Entry method Invoked by
post-processing (Gcode) execute(ctx) PostProcessor during Gcode export, resolving the preset's plugin refs
script execute() the Plugins dialog → Run action
printer-connection agent methods NetworkAgentFactory, registered through a loader oncapabilityload callback wired in GUI_App

The onload / onunload callbacks (PluginLoader::subscribe_on_load_callback / subscribe_on_unload_callback) and the percapability variants (subscribe_on_capability_load_callback / subscribe_on_capability_unload_callback) are how subsystems react to plugins and capabilities appearing or disappearing — e.g. the printeragent layer registers/deregisters an agent for each PrinterConnection capability, and the Plugins dialog refreshes. Adding a new type and wiring a call site is covered in plugin_development.md.


Threading and the GIL

  • The interpreter is initialized on the main thread. CPython is started once via PythonInterpreter (singleton). Initializing it off the main thread risks heap corruption, so PluginManager::initialize() does it eagerly and synchronously.
  • After init the GIL is released (PyEval_SaveThread) and reacquired at shutdown, so other threads may take it.
  • Plugin loads run on worker threads, serialized by a static mutex so module imports don't race. Discovery can also run on a background thread (discover_plugins(async=true)), though startup discovery is synchronous.
  • Every touch of Python from a nonmain thread acquires the GIL through the PythonGILState RAII guard (PyGILState_Ensure / Release) — load, execute, and the instance destructor (on_unload + Py_DECREF) all wrap in it.

Cloud subscriptions

CloudPluginService wraps the cloud agent (OrcaCloudServiceAgent) and is gated on login. It fetches the manifests of subscribed/owned plugins, merges them into the catalog as an overlay, and downloads a plugin's payload (sniffing the file to tell a .whl from a .py) to a temporary file. PluginManager sets the loader's cloud user id, and PluginLoader installs the downloaded payload under orca_plugins/_subscribed/<user_id>/. Logging out unloads cloud plugins. The cloud auth token (orca_refresh_token.sec) is owned by the cloud agent, not by the plugin layer.


Plugin references in presets

When a setting points at a plugin capability (for example post_process_plugin), the value the setting stores is just the capability's name. So that the reference survives being copied to another machine — where the plugin might not be installed — each preset also carries a plugins array that records the full reference for every capability it uses.

Each entry is a single string with three ;separated fields:

<plugin_name>;<cloud_uuid>;<capability_name>
{
  "plugins": [
    "Sample Plugin;1f998ea9-0183-4cc5-957f-4eef659ba4e6;G-code Benchmark (.py)",
    "master_plugin;;header-stamp"
  ],
  "post_process_plugin": ["G-code Benchmark (.py)", "header-stamp"]
}
  • The cloud_uuid is present for plugins subscribed from OrcaCloud and empty for localonly plugins (note the adjacent ;;). It is what lets OrcaSlicer offer to restore a missing plugin automatically.
  • Because ; is the field separator, a capability name may not contain ; (the loader rejects such a plugin), and plugin display names have any ; replaced with _ (sanitize_plugin_name).
  • The plugins array is an internal manifest (coStrings, comDevelop mode — not a useredited field). Fields that hold a capability name are flagged support_plugin; on save the array is pruned to only the references still used by such a field, so stale entries drop out.
  • Parsing/serialization lives in Config.cpp (parse_capability_refPluginCapabilityRef{ name, capability_name, uuid }); the plugins option is defined in PrintConfig.cpp and is a process/print preset setting. See tests/libslic3r/test_config.cpp and tests/slic3rutils/test_plugin_capability_identifier.cpp.

Restoring missing plugins

When a slice is started (Plater::reslice), OrcaSlicer resolves the active preset's plugins array against the loaded catalog. Any reference that is not installed is missing, and a dialog appears before slicing continues. Missing references are split by whether they carry a cloud UUID:

  • Missing OrcaCloud plugins (have a UUID) — the dialog offers Install plugins, which subscribes to, installs, loads, and enables each one so it is usable immediately, or Continue without plugins.
  • Missing local plugins (no UUID) — these cannot be fetched automatically, so the dialog offers Open OrcaCloud (a browser search for similarly named plugins on the OrcaCloud plugins explore page) or Continue without plugins.

Choosing Continue without plugins proceeds with the slice; the functionality those plugins would have provided is simply skipped.

The Plugins dialog

The Plugins dialog (PluginsDialog.cpp + resources/web/dialog/PluginsDialog/) presents each installed plugin as an expandable row (Activate · Name · Version · Status). Expanding a plugin shows a capability tree — one row per registered capability with its own enable checkbox, type label, and (for runnable script capabilities) a Run button. The details pane is tabbed:

Tab Shows
Plugin Info thumbnail, source, types, author, version (with an update badge)
Description the plugin's own description, taken from its Python/wheel metadata
Changelog version / date / changes table
Diagnostics load status and any error state

Installing is done from a Browse plugins split dropdown that opens the OrcaCloud plugins hub, with an Install local plugin option for sideloading a .py or .whl directly. Perplugin and percapability enablement is persisted in a perplugin .install_state.json sidecar (written by PluginManager).


Security and observability

  • Security — all C++→Python calls cross a trampoline that opens a percall audit context; the PluginAuditManager audit hook then filters sensitive operations (today: a filesystem write allowlist rooted at data_dir(), plus scoped roots such as the current Gcode folder). This is groundwork, not a hardened sandbox — read plugin_audit_hook.md for exactly what is and isn't enforced.
  • Observability — Python sys.stderr (plugin tracebacks, including from pluginspawned threads) is teed to data_dir()/log/python_*.log; C++side load/discovery messages go to the main session log. How errors surface in the UI (message box vs. the plugin details area) is described in plugin_development.md.

Key files

File Role
src/slic3r/plugin/PluginManager.{hpp,cpp} toplevel orchestrator; startup initialize() / discover_plugins() / shutdown()
src/slic3r/plugin/PluginCatalog.{hpp,cpp} directory scan → PluginDescriptor inventory
src/slic3r/plugin/PluginLoader.{hpp,cpp} threaded load/unload, dependency install, capability registry, auditkey stamping
src/slic3r/plugin/PluginDescriptor.hpp the perplugin record (types, changelog, sanitize_plugin_name)
src/slic3r/plugin/CloudPluginService.{hpp,cpp} cloud fetch / download / subscribe / unsubscribe
src/slic3r/plugin/PythonInterpreter.{hpp,cpp} embedded CPython, GIL handoff, audithook + log install
src/slic3r/plugin/PythonPluginBridge.{hpp,cpp} the orca module, @orca.plugin / register_capability, package + capability capture
src/slic3r/plugin/PyPluginPackage.hpp the package base (orca.base) + register_capabilities
src/slic3r/plugin/PyPluginTrampoline.hpp C++↔Python boundary macros (traceback logging + audit scope)
src/slic3r/plugin/pluginTypes/* pertype capability bases + trampolines
src/slic3r/plugin/PluginAuditManager.{hpp,cpp} the CPython audit hook and policy
src/libslic3r/Config.cpp parse_capability_ref, the plugins array (de)serialization
src/libslic3r/PrintConfig.cpp the plugins / post_process_plugin option definitions
src/slic3r/GUI/PostProcessor.cpp resolves preset plugin refs and runs Gcode capabilities
src/slic3r/GUI/PluginPickerDialog.{hpp,cpp} pick a capability as a setting value
src/slic3r/GUI/Plater.cpp the missingplugins resolution dialog on slice (reslice)
src/slic3r/GUI/GUI_App.cpp startup wiring (init, discovery, onload / oncapabilityload callbacks) and shutdown
src/slic3r/GUI/PluginsDialog.cpp the Plugins dialog (capability tree, tabs, Run, Browse plugins)