From 5261c534aaa41f911318b4b9ab6b4cbe8df9219e Mon Sep 17 00:00:00 2001 From: DerThorsten Date: Fri, 27 Jun 2025 13:04:54 +0200 Subject: [PATCH 1/2] loop --- module/pyjs/core.py | 48 ++++++++++++ src/export_pyjs_module.cpp | 150 +++++++++++++++++++++++++++++++++---- 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/module/pyjs/core.py b/module/pyjs/core.py index 545d3b3..0ccbc33 100644 --- a/module/pyjs/core.py +++ b/module/pyjs/core.py @@ -6,6 +6,8 @@ import ast import pyjs_core from pyjs_core import JsValue, js_array, js_py_object +import warnings +import time def install_submodules(): def _js_mod__getattr__(name: str) -> Any: @@ -307,3 +309,49 @@ async def async_exec_eval(stmts, globals=None, locals=None): parsed_fn.body[0].body = parsed_stmts.body exec(compile(parsed_fn, filename="", mode="exec"), globals, locals) return await eval(f'{fn_name}()', globals, locals) # fmt: skip + + + +class _CallbackEntryPoint: + def __init__(self, py_callback): + self._py_callback = py_callback + self._last_time = time.time() + def __call__(self): + t = time.time() + dt = t - self._last_time + self._last_time = t + self._py_callback(dt) + +_callback_entry_point = None + + +def set_main_loop_callback(py_callback, fps=0): + + global _callback_entry_point + if _callback_entry_point is not None: + # show a warning if the callback is already set + warnings.warn(""" A main loop callback is already set. + This will be replaced by the new callback, + use cancel_main_loop before setting a new callback to avoid this warning + """, + UserWarning) + + cancel_main_loop() + + _callback_entry_point = _CallbackEntryPoint(py_callback) + + pyjs_core._set_main_loop_callback( + _callback_entry_point, + int(fps) + ) + + +def cancel_main_loop(): + """Cancel the main loop callback.""" + global _callback_entry_point + if _callback_entry_point is not None: + pyjs_core._cancel_main_loop() + pyjs_core._set_noop_main_loop() + _callback_entry_point = None + else: + warnings.warn("No main loop callback is set to cancel.", UserWarning) \ No newline at end of file diff --git a/src/export_pyjs_module.cpp b/src/export_pyjs_module.cpp index 64239d8..38a192d 100644 --- a/src/export_pyjs_module.cpp +++ b/src/export_pyjs_module.cpp @@ -16,25 +16,143 @@ namespace py = pybind11; namespace em = emscripten; namespace pyjs -{ - void export_pyjs_module(py::module_& pyjs_module) +{ + + struct LoopContext { - export_js_proxy(pyjs_module); - try - { - // pyjs_core_pseudo_init(pyjs_module); - // pyjs_extend_js_val_pseudo_init(pyjs_module); - // pyjs_error_handling_pseudo_init(pyjs_module); - // pyjs_convert_pseudo_init(pyjs_module); - // pyjs_convert_py_to_js_pseudo_init(pyjs_module); - // pyjs_webloop_pseudo_init(pyjs_module); - // pyjs_pyodide_polyfill_pseudo_init(pyjs_module); + py::object m_callback; + bool m_cancel_loop_on_error = true; // default to true + bool m_exit_loop = false; + + LoopContext(py::object callback, bool cancel_loop_on_error) + : m_callback(std::move(callback)), m_cancel_loop_on_error(cancel_loop_on_error), m_exit_loop(false) {} + }; + + + + + // void wrapped_callback_without_cancel(void* cb_ptr) { + // // Reinterpret the void pointer back to a PyObject pointer + // auto py_object = reinterpret_cast(cb_ptr); + // // We can use PyObject_CallObject to call the Python function + // if (PyObject_CallNoArgs(py_object) == nullptr) { + // // If the call fails, we can print the error + // std::cerr << "Error calling Python callback." << std::endl; + // PyErr_Print(); + + // } + // }; + void wrapped_callback(void* cb_ptr) { + // Reinterpret the void pointer back to a PyObject pointer + auto py_object = reinterpret_cast(cb_ptr); + if(!py_object) { + std::cerr << "Error: callback pointer is null." << std::endl; } - catch (py::error_already_set& e) - { - std::cout << "error: " << e.what() << "\n"; - throw e; + // We can use PyObject_CallObject to call the Python function + if (PyObject_CallNoArgs(py_object) == nullptr) { + // If the call fails, we can print the error + std::cerr << "Error calling Python callback:" << std::endl; + PyErr_Print(); } + }; + + void noop_callback() { + // This is a no-op callback, it does nothing + + // we see a strange error when we run emscripten_cancel_main_loop + // **WITHOUT setting a new loop right away** + // so instead of just cancelling the loop, we need + // to cancel and right away set a new loop + } + + void self_cancel_callback() { + emscripten_cancel_main_loop(); + }; + + + void export_main_loop_callbacks(py::module_& pyjs_module) + { + + + + // class for loop context + py::class_(pyjs_module, "LoopContext") + .def(py::init(), py::arg("callback"), py::arg("cancel_loop_on_error") = true) + .def_readwrite("exit_loop", &LoopContext::m_exit_loop) + ; + + + + + + // Export main loop callbacks + pyjs_module.def("_set_main_loop_callback", [](py::handle callback, int fps) { + + // get a PyObject * from the handle + auto py_object = callback.ptr(); + + // convert the PyObject to void* + void* callback_ptr = reinterpret_cast(py_object); + + + // use emscripten_set_main_loop_arg + emscripten_set_main_loop_arg( + wrapped_callback, + callback_ptr, // pass the callback pointer as argument + fps, // frames per second + false + ); + }); + + // explicit cancel main loop + pyjs_module.def("_cancel_main_loop", []() { + // This will cancel the main loop if it is currently running + emscripten_cancel_main_loop(); + }); + + pyjs_module.def("_set_noop_main_loop", []() { + // This will set a no-op main loop + emscripten_set_main_loop(noop_callback, 1, false); // set a no-op loop to avoid errors + }); + + + // pyjs_module.def("set_main_loop_callback_hl", [](py::handle & callback, int fps) { + + // // get a PyObject * from the handle + // void * handle_ptr = reinterpret_cast(&callback); + + + + // // create a lambda without any caputres taking the callback pointer + // auto wrapped_callback = [](void* cb_ptr) { + + + // py::handle* handle = reinterpret_cast(cb_ptr); + + + // // We can use PyObject_CallObject to call the Python function + + // }; + + // // use emscripten_set_main_loop_arg + // emscripten_set_main_loop_arg( + // wrapped_callback, + // callback_ptr, // pass the callback pointer as argument + // fps, // frames per second + // false + // ); + // }); + + + + } + + + + void export_pyjs_module(py::module_& pyjs_module) + { + export_js_proxy(pyjs_module); + export_main_loop_callbacks(pyjs_module); } } From 01b4ec4f6dfad1de25118079f1fdb9e0b63755fb Mon Sep 17 00:00:00 2001 From: DerThorsten Date: Fri, 27 Jun 2025 13:31:22 +0200 Subject: [PATCH 2/2] cleanup --- src/export_pyjs_module.cpp | 73 ++++++++------------------------------ 1 file changed, 14 insertions(+), 59 deletions(-) diff --git a/src/export_pyjs_module.cpp b/src/export_pyjs_module.cpp index 38a192d..dc7e2ea 100644 --- a/src/export_pyjs_module.cpp +++ b/src/export_pyjs_module.cpp @@ -18,30 +18,17 @@ namespace em = emscripten; namespace pyjs { - struct LoopContext - { - py::object m_callback; - bool m_cancel_loop_on_error = true; // default to true - bool m_exit_loop = false; - - LoopContext(py::object callback, bool cancel_loop_on_error) - : m_callback(std::move(callback)), m_cancel_loop_on_error(cancel_loop_on_error), m_exit_loop(false) {} - }; - - + // struct LoopContext + // { + // py::object m_callback; + // bool m_cancel_loop_on_error = true; // default to true + // bool m_exit_loop = false; + + // LoopContext(py::object callback, bool cancel_loop_on_error) + // : m_callback(std::move(callback)), m_cancel_loop_on_error(cancel_loop_on_error), m_exit_loop(false) {} + // }; - // void wrapped_callback_without_cancel(void* cb_ptr) { - // // Reinterpret the void pointer back to a PyObject pointer - // auto py_object = reinterpret_cast(cb_ptr); - // // We can use PyObject_CallObject to call the Python function - // if (PyObject_CallNoArgs(py_object) == nullptr) { - // // If the call fails, we can print the error - // std::cerr << "Error calling Python callback." << std::endl; - // PyErr_Print(); - - // } - // }; void wrapped_callback(void* cb_ptr) { // Reinterpret the void pointer back to a PyObject pointer auto py_object = reinterpret_cast(cb_ptr); @@ -75,14 +62,12 @@ namespace pyjs - // class for loop context - py::class_(pyjs_module, "LoopContext") - .def(py::init(), py::arg("callback"), py::arg("cancel_loop_on_error") = true) - .def_readwrite("exit_loop", &LoopContext::m_exit_loop) - ; + // // class for loop context + // py::class_(pyjs_module, "LoopContext") + // .def(py::init(), py::arg("callback"), py::arg("cancel_loop_on_error") = true) + // .def_readwrite("exit_loop", &LoopContext::m_exit_loop) + // ; - - // Export main loop callbacks @@ -114,36 +99,6 @@ namespace pyjs // This will set a no-op main loop emscripten_set_main_loop(noop_callback, 1, false); // set a no-op loop to avoid errors }); - - - // pyjs_module.def("set_main_loop_callback_hl", [](py::handle & callback, int fps) { - - // // get a PyObject * from the handle - // void * handle_ptr = reinterpret_cast(&callback); - - - - // // create a lambda without any caputres taking the callback pointer - // auto wrapped_callback = [](void* cb_ptr) { - - - // py::handle* handle = reinterpret_cast(cb_ptr); - - - // // We can use PyObject_CallObject to call the Python function - - // }; - - // // use emscripten_set_main_loop_arg - // emscripten_set_main_loop_arg( - // wrapped_callback, - // callback_ptr, // pass the callback pointer as argument - // fps, // frames per second - // false - // ); - // }); - - }