Plugin System Overview¶
This page describes the architecture of OrcaSlicer's Python plugin system. Python plugins run inside an embedded CPython interpreter, so users can extend OrcaSlicer without recompiling the application or patching the C++ core.
The other plugin development pages cover the implementation details:
- Plugin Development - how to write a Python plugin and how to add a new plugin type in C++.
- API Reference - the embedded
orcamodule, registration, host access, UI helpers, and each capability module. - Plugin Audit Hook - the CPython audit hook that constrains what plugin code may do.
Note
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 Overview¶
┌──────────────────────────────────────────────┐
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)
There are two broad layers:
- Orchestration (C++, no Python):
PluginManager,PluginCatalog,CloudPluginService,PluginLoader,PluginDescriptor. These discover, install, and manage plugins as data. - Execution (the C++ to 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++ to 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 file (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.
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.
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¶
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. This 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. For example, 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.
Threading and 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:
{
"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++ to 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; read Plugin Audit Hook for 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 in Plugin Development.
Related Documents¶
- Plugin Development - authoring Python plugins; adding a new C++ plugin type; testing and debugging.
- API Reference - the embedded
orcamodule, registration, host access, UI helpers, and each capability module. - Plugin Audit Hook - 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++ to 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) |