Files
OrcaSlicer/src/slic3r/plugin/PythonPluginBridge.cpp

402 lines
18 KiB
C++

#include "PythonPluginBridge.hpp"
#include <boost/log/trivial.hpp>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <pybind11/embed.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "PythonInterpreter.hpp"
#include "PluginHostApi.hpp"
#include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp"
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
namespace py = pybind11;
namespace Slic3r {
namespace {
// Python plugin discovery is a two-step capture:
// 1) PluginLoader sets an active plugin key and imports the Python module.
// 2) Python decorators/API calls enter these pybind callbacks without receiving the
// C++ PluginDescriptor, so the callbacks use the active key to attach Python classes
// to the plugin currently being loaded.
//
// The pending maps hold Python class objects, not plugin instances. Instances are created
// only after the package class has had a chance to register every capability.
thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package;
struct PluginInstanceHandle
{
// The C++ plugin interface points into a Python object. Keep both alive through one
// shared control block; CapturedCapability later exposes an aliasing shared_ptr to plugin.
std::shared_ptr<PluginCapabilityInterface> plugin;
py::object keep_alive;
~PluginInstanceHandle()
{
if (keep_alive) {
if (Py_IsInitialized()) {
// Dropping a py::object decrefs the Python object, so reacquire the GIL.
PythonGILState gil;
keep_alive = py::object();
} else {
// During interpreter shutdown it is no longer safe to decref Python objects.
// release() forgets the wrapper ownership without touching Python runtime state.
(void) keep_alive.release();
}
}
}
};
} // namespace
PythonPluginBridge& PythonPluginBridge::instance()
{
static PythonPluginBridge bridge;
return bridge;
}
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(info) << "Beginning Python plugin capture for key " << plugin_key;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
// Start from a clean slot in case a previous failed load left pending Python classes
// for this same entry path.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
}
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin.
g_active_plugin_key = plugin_key;
}
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
// Phase 1: run the package class's register_capabilities() while the active key is
// still set. That method is expected to call orca.register_capability() once per
// capability class, and register_capability() needs g_active_plugin_key to know which
// pending bucket to append to.
{
auto clear_active_key = [&plugin_key]() {
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
};
auto discard_pending_for_key = [&plugin_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
};
try {
// The @orca.plugin decorator records the package class during module import.
// Move it into a local py::object and remove it from the pending map so the
// registry no longer owns it once finalization starts.
py::object package_cls;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_package.find(plugin_key);
if (it != g_pending_package.end()) {
package_cls = it->second;
g_pending_package.erase(it);
}
}
if (!package_cls) {
error = "Plugin did not register a package class; decorate it with @orca.plugin";
BOOST_LOG_TRIVIAL(error) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
return {};
}
// The package instance is only a registration coordinator. It is not returned
// to the rest of the plugin system; only the capability classes it registers
// are kept.
py::object package = package_cls();
package.attr("register_capabilities")();
} catch (py::error_already_set& err) {
log_python_exception_keep(err);
error = err.what();
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised Python exception for key " << plugin_key
<< " error=" << error;
discard_pending_for_key();
clear_active_key();
return {};
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised exception for key " << plugin_key
<< " error=" << error;
discard_pending_for_key();
clear_active_key();
return {};
}
}
// Phase 2: move the capability classes that register_capabilities() appended into a
// local vector. From this point the pending registry no longer owns these py::objects.
std::vector<py::object> classes;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_capabilities.find(plugin_key);
if (it != g_pending_capabilities.end()) {
classes = std::move(it->second);
g_pending_capabilities.erase(it);
}
}
// Registration is complete. Later register_capability() calls should fail instead of
// accidentally attaching themselves to this plugin.
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size());
// Phase 3: instantiate each registered capability class and convert it to the common
// C++ interface used by the rest of OrcaSlicer.
for (auto& cls : classes) {
try {
py::object instance = cls();
if (!py::isinstance<PluginCapabilityInterface>(instance)) {
error = "Registered capability must inherit from a PluginCapability base";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
<< " error=" << error;
return {};
}
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
if (!capability_iface) {
error = "Failed to cast Python capability to PluginCapabilityInterface";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
<< " error=" << error;
return {};
}
// This is a registered capability, not the transient orca.base package.
// get_name() is required on capabilities and is cached for preset lookup.
std::string name = capability_iface->get_name();
// Capability names feed ';'-delimited config/preset serialization and drive
// dispatch, so unlike display names they cannot be silently rewritten — a ';'
// here is a hard error that rejects the whole plugin capture.
if (name.find(';') != std::string::npos) {
error = "Capability name must not contain ';': " + name;
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
<< " error=" << error;
return {};
}
auto handle = std::make_shared<PluginInstanceHandle>();
handle->keep_alive = instance;
handle->plugin = std::move(capability_iface);
CapturedCapability captured;
// Return a shared_ptr<PluginCapabilityInterface> while keeping PluginInstanceHandle
// as the owner, so the Python instance stays alive as long as the C++ interface does.
captured.instance = std::shared_ptr<PluginCapabilityInterface>(handle, handle->plugin.get());
captured.name = std::move(name);
capabilities.emplace_back(std::move(captured));
} catch (py::error_already_set& err) {
// Direct Python call (cls() / get_name() above), not a trampoline override —
// log the traceback here. GIL is held for the duration of finalize_plugin_capture.
log_python_exception_keep(err);
error = err.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
<< " error=" << error;
return {};
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
<< " error=" << error;
return {};
}
}
BOOST_LOG_TRIVIAL(info) << "Instantiated " << capabilities.size() << " Python capability instance(s) for key " << plugin_key;
return capabilities;
}
void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(warning) << "Cancelling Python plugin capture for key " << plugin_key;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
// Import or dependency setup failed before finalization. Drop anything the module
// may already have registered under this key.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
}
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
}
void PythonPluginBridge::clear_pending_captures()
{
if (!Py_IsInitialized()) {
std::lock_guard<std::mutex> lock(g_registry_mutex);
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size()
<< " pending Python plugin capture(s) without Python interpreter";
// py::object destruction would decref Python objects. If the interpreter is already
// gone, release the wrappers instead and intentionally skip decref.
for (auto& [plugin_key, plugins] : g_pending_capabilities) {
(void) plugin_key;
for (py::object& plugin : plugins)
(void) plugin.release();
}
g_pending_capabilities.clear();
for (auto& [plugin_key, pkg] : g_pending_package) {
(void) plugin_key;
(void) pkg.release();
}
g_pending_package.clear();
g_active_plugin_key.clear();
return;
}
// Normal shutdown path: hold the GIL and let py::object destructors decref cleanly.
PythonGILState gil;
std::lock_guard<std::mutex> lock(g_registry_mutex);
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear();
g_pending_package.clear();
g_active_plugin_key.clear();
}
void bind_python_api(pybind11::module_& m)
{
m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PostProcessing", PluginCapabilityType::PostProcessing)
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation)
.value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
.value("Success", PluginResult::Success)
.value("Skipped", PluginResult::Skipped)
.value("RecoverableError", PluginResult::RecoverableError)
.value("FatalError", PluginResult::FatalError)
.export_values();
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
.def(py::init<>())
.def_readwrite("orca_version", &PluginContext::orca_version);
py::class_<ExecutionResult>(m, "ExecutionResult", "Structured execution outcome")
.def(py::init<>())
.def(py::init<PluginResult, std::string, std::string>())
.def_readwrite("status", &ExecutionResult::status)
.def_readwrite("message", &ExecutionResult::message)
.def_readwrite("data", &ExecutionResult::data)
.def_static("success", &ExecutionResult::success, py::arg("message") = std::string(), py::arg("data") = std::string())
.def_static("skipped", &ExecutionResult::skipped, py::arg("message") = std::string())
.def_static("failure", &ExecutionResult::failure, py::arg("status"), py::arg("message"), py::arg("data") = std::string());
py::class_<PluginCapabilityInterface, PyPluginInterfaceTrampoline, std::shared_ptr<PluginCapabilityInterface>>(m, "PythonPluginBase")
.def(py::init<>())
.def("get_name", &PluginCapabilityInterface::get_name)
.def("get_type", &PluginCapabilityInterface::get_type)
.def("on_load", &PluginCapabilityInterface::on_load)
.def("on_unload", &PluginCapabilityInterface::on_unload);
// Expose the package marker base as orca.base. @orca.plugin later verifies that the
// decorated class derives from this exact pybind-registered C++ type.
py::class_<PyPluginPackage, PyPluginPackageTrampoline>(m, "base")
.def(py::init<>())
.def("register_capabilities", &PyPluginPackage::register_capabilities);
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here
GCodePluginCapability::RegisterBindings(m, pluginTypes);
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
PluginHostApi::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
m.def(
"register_capability",
[](py::object plugin_cls) {
if (g_active_plugin_key.empty()) {
throw py::value_error("register_capability() called outside plugin discovery context");
}
// Store capability classes only, not instances. Finalization instantiates them
// after the package has registered the full set for this plugin.
py::handle base = py::type::of<PluginCapabilityInterface>();
const int is_subclass = PyObject_IsSubclass(plugin_cls.ptr(), base.ptr());
if (is_subclass != 1) {
if (is_subclass < 0)
PyErr_Clear();
throw py::value_error("Registered class must inherit from a PluginCapability base");
}
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities[g_active_plugin_key].push_back(std::move(plugin_cls));
BOOST_LOG_TRIVIAL(debug) << "Registered Python plugin capability class for key " << g_active_plugin_key;
},
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty())
throw py::value_error("@orca.plugin used outside plugin discovery context");
if (!PyType_Check(cls.ptr()))
throw py::value_error("@orca.plugin must decorate a class");
// The decorator is only a marker/capture hook. It records the package class now;
// finalize_plugin_capture() instantiates it later and calls register_capabilities().
py::handle base = py::type::of<PyPluginPackage>();
const int is_subclass = PyObject_IsSubclass(cls.ptr(), base.ptr());
if (is_subclass != 1) {
if (is_subclass < 0)
PyErr_Clear();
throw py::value_error("@orca.plugin must decorate a subclass of orca.base");
}
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& slot = g_pending_package[g_active_plugin_key];
if (slot)
throw py::value_error("multiple @orca.plugin classes registered; exactly one is allowed per plugin");
slot = cls;
}
return cls; // decorator returns the class unchanged
}, R"pbdoc(Mark the single plugin package class (the orca.base subclass) for this file.)pbdoc");
}
} // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE
PYBIND11_MODULE(orca, m) { Slic3r::bind_python_api(m); }
#else
PYBIND11_EMBEDDED_MODULE(orca, m) { Slic3r::bind_python_api(m); }
#endif