123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- /*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
- #include <PythonProxyBus.h>
- #include <EditorPythonBindings/PythonUtility.h>
- #include <Source/PythonTypeCasters.h>
- #include <EditorPythonBindings/PythonCommon.h>
- #include <Source/PythonSymbolsBus.h>
- #include <pybind11/embed.h>
- #include <AzCore/PlatformDef.h>
- #include <AzCore/RTTI/BehaviorContext.h>
- #include <AzCore/RTTI/AttributeReader.h>
- #include <AzCore/std/optional.h>
- #include <AzFramework/StringFunc/StringFunc.h>
- #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
- namespace EditorPythonBindings
- {
- namespace Internal
- {
- enum class EventType
- {
- Broadcast,
- Event,
- QueueBroadcast,
- QueueEvent
- };
- pybind11::object InvokeEbus(AZ::BehaviorEBus& behaviorEBus, EventType eventType, AZStd::string_view eventName, pybind11::args pythonArgs)
- {
- auto eventIterator = behaviorEBus.m_events.find(eventName);
- AZ_Warning("python", eventIterator != behaviorEBus.m_events.end(), "Event %.*s does not exist in EBus %s", aznumeric_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
- if (eventIterator == behaviorEBus.m_events.end())
- {
- return pybind11::cast<pybind11::none>(Py_None);
- }
- auto& behaviorEBusEventSender = eventIterator->second;
- switch (eventType)
- {
- case EventType::Broadcast:
- {
- AZ_Warning("python", behaviorEBusEventSender.m_broadcast, "EventSender: function %.*s in EBus %s does not support the bus.Broadcast event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
- if (behaviorEBusEventSender.m_broadcast)
- {
- return Call::StaticMethod(behaviorEBusEventSender.m_broadcast, pythonArgs);
- }
- break;
- }
- case EventType::Event:
- {
- AZ_Warning("python", behaviorEBusEventSender.m_event, "EventSender: function %.*s in EBus %s does not support the bus.Event event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
- if (behaviorEBusEventSender.m_event)
- {
- return Call::StaticMethod(behaviorEBusEventSender.m_event, pythonArgs);
- }
- break;
- }
- case EventType::QueueBroadcast:
- {
- AZ_Warning("python", behaviorEBusEventSender.m_queueBroadcast, "EventSender: function %.*s in EBus %s does not support the bus.QueueBroadcast event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
- if (behaviorEBusEventSender.m_queueBroadcast)
- {
- return Call::StaticMethod(behaviorEBusEventSender.m_queueBroadcast, pythonArgs);
- }
- break;
- }
- case EventType::QueueEvent:
- {
- AZ_Warning("python", behaviorEBusEventSender.m_queueEvent, "EventSender: function %.*s in EBus %s does not support the bus.QueueEvent event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
- if (behaviorEBusEventSender.m_queueEvent)
- {
- return Call::StaticMethod(behaviorEBusEventSender.m_queueEvent, pythonArgs);
- }
- break;
- }
- default:
- AZ_Error("python", false, "Unknown EBus call type %d", eventType);
- break;
- }
- return pybind11::cast<pybind11::none>(Py_None);
- }
-
- class PythonProxyNotificationHandler final
- {
- public:
- AZ_CLASS_ALLOCATOR(PythonProxyNotificationHandler, AZ::SystemAllocator);
- PythonProxyNotificationHandler(AZStd::string_view busName)
- {
- AZ::BehaviorContext* behaviorContext(nullptr);
- AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
- if (!behaviorContext)
- {
- AZ_Error("python", false, "A behavior context is required to bind the buses!");
- return;
- }
- auto behaviorEBusEntry = behaviorContext->m_ebuses.find(busName);
- if (behaviorEBusEntry == behaviorContext->m_ebuses.end())
- {
- AZ_Error("python", false, "There is no EBus by the name of %.*s", static_cast<int>(busName.size()), busName.data());
- return;
- }
- AZ_Assert(behaviorEBusEntry->second, "A null EBus:%s is in the Behavior Context!", behaviorEBusEntry->first.c_str());
- m_ebus = behaviorEBusEntry->second;
- }
- ~PythonProxyNotificationHandler()
- {
- Disconnect();
- }
- bool IsConnected() const
- {
- if (m_handler)
- {
- return m_handler->IsConnected();
- }
- return false;
- }
- bool Connect(pybind11::object busId)
- {
- if (!m_ebus)
- {
- AZ_Error("python", false, "EBus not set.");
- return false;
- }
- if (!CreateHandler(*m_ebus))
- {
- AZ_Error("python", false, "Could not create a handler for ebus");
- return false;
- }
- // does the EBus require an address to connect?
- if (m_ebus->m_idParam.m_typeId.IsNull())
- {
- AZ_Warning("python", busId.is_none(), "Connecting to an singleton EBus but was given a non-None busId(%s)", pybind11::cast<AZStd::string>(busId).c_str());
- return m_handler->Connect();
- }
- else if (busId.is_none())
- {
- AZ_Warning("python", busId.is_none(), "Connecting to an EBus that requires an address but was given a None busId");
- return false;
- }
- Convert::StackVariableAllocator stackVariableAllocator;
- AZ::BehaviorArgument busAddress;
- if (!Convert::PythonToBehaviorValueParameter(m_ebus->m_idParam, busId, busAddress, stackVariableAllocator))
- {
- AZ_Warning("python", busId.is_none(), "Could not convert busId(%s) to address type (%s)",
- pybind11::cast<AZStd::string>(busId).c_str(), m_ebus->m_idParam.m_typeId.ToString<AZStd::string>().c_str());
- return false;
- }
- return m_handler->Connect(&busAddress);
- }
- bool Disconnect()
- {
- if (!m_handler)
- {
- return false;
- }
- m_handler->Disconnect();
- if (m_ebus)
- {
- DestroyHandler(*m_ebus);
- }
- return true;
- }
- bool AddCallback(AZStd::string_view eventName, pybind11::function callback)
- {
- if (!PyCallable_Check(callback.ptr()))
- {
- [[maybe_unused]] AZStd::string ebusName(AZStd::string(m_ebus ? m_ebus->m_name : "invalid ebus"));
- AZ_Error("python", false, "The callback for event '%s' on bus '%.*s' needs to be a callable python function.",
- eventName.data(),
- AZ_STRING_ARG(ebusName));
- return false;
- }
- if (!m_handler)
- {
- [[maybe_unused]] AZStd::string ebusName(m_ebus ? m_ebus->m_name : "invalid ebus");
- AZ_Error(
- "python",
- false,
- "No EBus connection detected for event '%s'. Make sure to call to connect() on the %.*s bus, first.",
- eventName.data(),
- AZ_STRING_ARG(ebusName));
- return false;
- }
- const AZ::BehaviorEBusHandler::EventArray& events = m_handler->GetEvents();
- for (int iEvent = 0; iEvent < static_cast<int>(events.size()); ++iEvent)
- {
- const AZ::BehaviorEBusHandler::BusForwarderEvent& e = events[iEvent];
- if (eventName == e.m_name)
- {
- AZStd::string eventNameValue{ eventName };
- AZ_Warning("python", m_callbackMap.end() == m_callbackMap.find(eventNameValue), "Replacing callback for eventName:%s", eventNameValue.c_str());
- m_callbackMap[eventNameValue] = callback;
- return true;
- }
- }
- return false;
- }
- protected:
- void DestroyHandler(const AZ::BehaviorEBus& ebus)
- {
- if (m_handler)
- {
- AZ_Warning("python", ebus.m_destroyHandler, "Ebus (%s) does not have a handler destroyer.", ebus.m_name.c_str());
- if (ebus.m_destroyHandler)
- {
- ebus.m_destroyHandler->Invoke(m_handler);
- }
- }
- m_handler = nullptr;
- m_callbackMap.clear();
- }
- bool CreateHandler(const AZ::BehaviorEBus& ebus)
- {
- DestroyHandler(ebus);
-
- AZ_Warning("python", ebus.m_createHandler, "Ebus (%s) does not have a handler creator.", ebus.m_name.c_str());
- if (!ebus.m_createHandler)
- {
- return false;
- }
-
- if (!ebus.m_createHandler->InvokeResult(m_handler))
- {
- AZ_Warning("python", ebus.m_createHandler, "Ebus (%s) failed to create a handler.", ebus.m_name.c_str());
- return false;
- }
- if (m_handler)
- {
- const AZ::BehaviorEBusHandler::EventArray& events = m_handler->GetEvents();
- for (int iEvent = 0; iEvent < static_cast<int>(events.size()); ++iEvent)
- {
- m_handler->InstallGenericHook(iEvent, &PythonProxyNotificationHandler::OnEventGenericHook, this);
- }
- }
- return true;
- }
- static void OnEventGenericHook(void* userData, const char* eventName, int eventIndex, AZ::BehaviorArgument* result, int numParameters, AZ::BehaviorArgument* parameters)
- {
- auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
- if (!editorPythonEventsInterface)
- {
- return;
- }
- // find the callback for the event
- auto* handler = reinterpret_cast<PythonProxyNotificationHandler*>(userData);
- const auto& callbackEntry = handler->m_callbackMap.find(eventName);
- if (callbackEntry == handler->m_callbackMap.end())
- {
- return;
- }
- // This function can reach from multiple threads, which means OnEventGenericHook
- // will require to acquire the Python GIL, make sure it tries to lock it using TryExecuteWithLock.
- [[maybe_unused]] const bool executed = editorPythonEventsInterface->TryExecuteWithLock(
- [handler, eventName, callback = callbackEntry->second, eventIndex, result, numParameters, parameters]()
- {
- handler->OnEventGenericHook(eventName, callback, eventIndex, result, numParameters, parameters);
- });
- AZ_Error("python", executed,
- "Ebus(%s) event(%s) could not be executed because it could not acquire the Python GIL. "
- "This occurs when there is already another thread executing python, which has the GIL locked, "
- "making it not possible for this thread to callback python at the same time. "
- "This is a limitation of python interpreter. Python scripts executions and event callbacks "
- "from EBuses need be designed to avoid this scenario.",
- handler->m_ebus->m_name.c_str(), eventName);
- }
- void OnEventGenericHook([[maybe_unused]] const char* eventName, pybind11::function callback, [[maybe_unused]] int eventIndex, AZ::BehaviorArgument* result, int numParameters, AZ::BehaviorArgument* parameters)
- {
- // build the parameters to send to callback
- Convert::StackVariableAllocator stackVariableAllocator;
- pybind11::tuple pythonParamters(numParameters);
- for (int index = 0; index < numParameters; ++index)
- {
- AZ::BehaviorArgument& behaviorValueParameter{ *(parameters + index) };
- pythonParamters[index] = Convert::BehaviorValueParameterToPython(behaviorValueParameter, stackVariableAllocator);
-
- if (pythonParamters[index].is_none())
- {
- AZ_Warning("python", false, "Ebus(%s) event(%s) failed to convert parameter at index(%d)", m_ebus->m_name.c_str(), eventName, index);
- return;
- }
- }
- try
- {
- pybind11::object pyResult = callback(pythonParamters);
- // store the result
- if (result && pyResult.is_none() == false)
- {
- // reset/prepare the stack allocator
- m_stackVariableAllocator = {};
- // Reset the result parameter
- m_resultParam = {};
- const AZ::u32 traits = result->m_traits;
- if (Convert::PythonToBehaviorValueParameter(*result, pyResult, m_resultParam, m_stackVariableAllocator))
- {
- // Setting result parameter into the output parameter will not fix its pointers
- // to use output parameter's internal memory, because of this, result parameter
- // needs to be a member so its memory is still valid when accessed in BehaviorEBusHandler::CallResult.
- result->Set(m_resultParam);
- result->m_value = m_resultParam.GetValueAddress();
- if ((traits & AZ::BehaviorParameter::TR_POINTER) == AZ::BehaviorParameter::TR_POINTER)
- {
- result->m_value = &result->m_value;
- }
- }
- }
- }
- catch ([[maybe_unused]] const std::exception& e)
- {
- AZ_Error("python", false, "Python callback threw an exception %s", e.what());
- }
- }
- private:
- const AZ::BehaviorEBus* m_ebus = nullptr;
- AZ::BehaviorEBusHandler* m_handler = nullptr;
- AZStd::unordered_map<AZStd::string, pybind11::function> m_callbackMap;
- Convert::StackVariableAllocator m_stackVariableAllocator;
- AZ::BehaviorArgument m_resultParam;
- };
- }
- namespace PythonProxyBusManagement
- {
- void CreateSubmodule(pybind11::module baseModule)
- {
- AZ::BehaviorContext* behaviorContext(nullptr);
- AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
- if (!behaviorContext)
- {
- AZ_Error("python", false, "A behavior context is required to bind the buses!");
- return;
- }
- auto busModule = baseModule.def_submodule("bus");
- Module::PackageMapType modulePackageMap;
- // export possible ways an EBus can be invoked
- pybind11::enum_<Internal::EventType>(busModule, "EventType")
- .value("Event", Internal::EventType::Event)
- .value("Broadcast", Internal::EventType::Broadcast)
- .value("QueueEvent", Internal::EventType::QueueEvent)
- .value("QueueBroadcast", Internal::EventType::QueueBroadcast)
- .export_values();
- // export the EBuses flagged for Automation or Common scope
- for (auto&& busEntry : behaviorContext->m_ebuses)
- {
- AZStd::string& ebusName = busEntry.first;
- AZ::BehaviorEBus* behaviorEBus = busEntry.second;
- if (Scope::IsBehaviorFlaggedForEditor(behaviorEBus->m_attributes))
- {
- auto busCaller = pybind11::cpp_function([behaviorEBus](Internal::EventType eventType, AZStd::string_view eventName, pybind11::args pythonArgs)
- {
- return Internal::InvokeEbus(*behaviorEBus, eventType, eventName, pythonArgs);
- });
- auto createPythonProxyNotificationHandler = pybind11::cpp_function([behaviorEBus]()
- {
- return aznew Internal::PythonProxyNotificationHandler(behaviorEBus->m_name.c_str());
- });
- pybind11::module thisBusModule = busModule;
- auto moduleName = Module::GetName(behaviorEBus->m_attributes);
- if (moduleName)
- {
- // this will place the bus into either:
- // 1) if the module is valid, then azlmbr.<module name>.<ebus name>
- // 2) or, then azlmbr.bus.<ebus name>
- thisBusModule = Module::DeterminePackageModule(modulePackageMap, *moduleName, baseModule, busModule, true);
- }
- // for each notification handler type, make a convenient Python type to make the script more Python-ic
- if (behaviorEBus->m_createHandler && behaviorEBus->m_destroyHandler)
- {
- AZStd::string ebusNotificationName{ AZStd::string::format("%sHandler", ebusName.c_str()) };
- thisBusModule.attr(ebusNotificationName.c_str()) = createPythonProxyNotificationHandler;
- }
- // is a request EBus
- thisBusModule.attr(ebusName.c_str()) = busCaller;
- // log the bus symbol
- AZStd::string subModuleName = pybind11::cast<AZStd::string>(thisBusModule.attr("__name__"));
- PythonSymbolEventBus::QueueBroadcast(&PythonSymbolEventBus::Events::LogBus, subModuleName, ebusName, behaviorEBus);
- }
- }
- // export possible ways an EBus can be invoked
- pybind11::class_<Internal::PythonProxyNotificationHandler>(busModule, "NotificationHandler")
- .def(pybind11::init<AZStd::string_view>())
- .def("is_connected", &Internal::PythonProxyNotificationHandler::IsConnected)
- .def("connect", &Internal::PythonProxyNotificationHandler::Connect, pybind11::arg("busId") = pybind11::none())
- .def("disconnect", &Internal::PythonProxyNotificationHandler::Disconnect)
- .def("add_callback", &Internal::PythonProxyNotificationHandler::AddCallback)
- ;
- }
- }
- }
|