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..dc7e2ea 100644 --- a/src/export_pyjs_module.cpp +++ b/src/export_pyjs_module.cpp @@ -16,25 +16,98 @@ namespace py = pybind11; 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) {} + // }; + + + 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; + } + // 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 + }); + + } + + + void export_pyjs_module(py::module_& pyjs_module) { 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); - } - catch (py::error_already_set& e) - { - std::cout << "error: " << e.what() << "\n"; - throw e; - } + export_main_loop_callbacks(pyjs_module); } }