Files
OrcaSlicer/docs/plugins/plugin_system.md
2026-07-02 19:34:04 +08:00

22 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 tracked on process, printer, and filament presets. See tests/libslic3r/test_config.cpp and tests/slic3rutils/test_plugin_capability_identifier.cpp.

Restoring missing plugins

When you prepare to slice, OrcaSlicer resolves the active process, printer, and filament presets' plugins arrays against the loaded catalog (Plater::refresh_missing_plugin_block). Any reference it cannot satisfy is surfaced as a non-closable notification, and while any remain the Slice button stays blocked — there is no "slice anyway" path; you resolve the reference (or change the setting that pulls it in). References are sorted into four buckets:

  • Missing OrcaCloud plugins (ref carries a UUID) — notification action Install Plugins, which subscribes to, installs, loads, and enables each one so it becomes usable immediately.
  • Missing local plugins (no UUID) — cannot be fetched automatically; the action Find on OrcaCloud just opens a browser search on the OrcaCloud plugins page. It is a suggestion only: it neither closes the notification nor unblocks slicing.
  • Inactive plugins — the package is installed locally but the referenced capability is not active (plugin not loaded, or capability disabled). Action Activate Now loads/enables it locally, with no download.
  • Broken references — the plugin is installed and loaded but no longer provides the referenced capability (renamed/removed/outdated). Activation cannot fix this, so it is informational, with Find on OrcaCloud to look for an update.

The bucketing lives in PluginResolver (get_missing_cloud_plugins, get_missing_local_plugins, get_inactive_plugins, get_broken_plugins); the notifications and the slice block are driven from Plater.cpp (refresh_missing_plugin_block).

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)