22 KiB
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 deep‑dive).
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 single‑purpose 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
orcamodule, not the slicer internals. - A security boundary — file access by plugin code is filtered by an audit hook with a write allow‑list (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 per‑type bases underpluginTypes/. These turn a discovered plugin into a live object the app can call.
Core components
| Component | Responsibility |
|---|---|
PluginManager |
Top‑level singleton orchestrator. Owns the catalog, loader, and cloud service; exposes initialize(), discover_plugins(), install/update/delete, and shutdown(). |
PluginCatalog |
Manifest‑only 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 on‑load/unload + on‑capability‑load/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 + stderr‑to‑log 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 per‑call audit scope. |
pluginTypes/* |
Per‑type C++ capability bases + trampolines (GCodePluginCapability, ScriptPluginCapability, PrinterAgentPluginCapability) that define each type's entry method and dispatch. |
PluginAuditManager |
Singleton CPython audit hook: filesystem policy (write allow‑list), 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 / side‑loaded |
data_dir()/orca_plugins/_subscribed/<user_id>/ |
cloud‑subscribed (per logged‑in 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 load‑time 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 re‑stamped on key migration (update_loaded_plugin_key). See the audit doc.- A module must mark exactly one package class with
@orca.plugin(a subclass oforca.base), and that class'sregister_capabilities()must register at least one valid capability viaorca.register_capability(...), or the load fails. Each capability must resolveget_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 per‑type 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 no‑op — which is how the system guarantees that absent/disabled
capabilities never change existing behavior.
| Capability type | Entry method | Invoked by |
|---|---|---|
post-processing (G‑code) |
execute(ctx) |
PostProcessor during G‑code export, resolving the preset's plugin refs |
script |
execute() |
the Plugins dialog → Run action |
printer-connection |
agent methods | NetworkAgentFactory, registered through a loader on‑capability‑load callback wired in GUI_App |
The on‑load / on‑unload callbacks (PluginLoader::subscribe_on_load_callback /
subscribe_on_unload_callback) and the per‑capability 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
printer‑agent 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, soPluginManager::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 non‑main thread acquires the GIL through the
PythonGILStateRAII 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_uuidis present for plugins subscribed from OrcaCloud and empty for local‑only 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
pluginsarray is an internal manifest (coStrings,comDevelopmode — not a user‑edited field). Fields that hold a capability name are flaggedsupport_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_ref→PluginCapabilityRef{ name, capability_name, uuid }); thepluginsoption is defined inPrintConfig.cppand is tracked on process, printer, and filament presets. Seetests/libslic3r/test_config.cppandtests/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 side‑loading a .py or .whl directly.
Per‑plugin and per‑capability enablement is persisted in a per‑plugin .install_state.json
sidecar (written by PluginManager).
Security and observability
- Security — all C++→Python calls cross a trampoline that opens a per‑call audit context;
the
PluginAuditManageraudit hook then filters sensitive operations (today: a filesystem write allow‑list rooted atdata_dir(), plus scoped roots such as the current G‑code folder). This is groundwork, not a hardened sandbox — readplugin_audit_hook.mdfor exactly what is and isn't enforced. - Observability — Python
sys.stderr(plugin tracebacks, including from plugin‑spawned threads) is teed todata_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 inplugin_development.md.
Related documents
plugin_development.md— authoring Python plugins; adding a new C++ plugin type; testing and debugging.plugin_audit_hook.md— the audit hook: modes, allow‑list, extending the policy.
Key files
| File | Role |
|---|---|
src/slic3r/plugin/PluginManager.{hpp,cpp} |
top‑level 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, audit‑key stamping |
src/slic3r/plugin/PluginDescriptor.hpp |
the per‑plugin 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, audit‑hook + 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/* |
per‑type 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 G‑code capabilities |
src/slic3r/GUI/PluginPickerDialog.{hpp,cpp} |
pick a capability as a setting value |
src/slic3r/GUI/Plater.cpp |
the missing‑plugins resolution dialog on slice (reslice) |
src/slic3r/GUI/GUI_App.cpp |
startup wiring (init, discovery, on‑load / on‑capability‑load callbacks) and shutdown |
src/slic3r/GUI/PluginsDialog.cpp |
the Plugins dialog (capability tree, tabs, Run, Browse plugins) |