Skip to content

Commit 2b013bf

Browse files
authored
main loop callbacks (#105)
* loop * cleanup
1 parent b7f3a08 commit 2b013bf

File tree

2 files changed

+137
-16
lines changed

2 files changed

+137
-16
lines changed

module/pyjs/core.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import ast
77
import pyjs_core
88
from pyjs_core import JsValue, js_array, js_py_object
9+
import warnings
10+
import time
911

1012
def install_submodules():
1113
def _js_mod__getattr__(name: str) -> Any:
@@ -307,3 +309,49 @@ async def async_exec_eval(stmts, globals=None, locals=None):
307309
parsed_fn.body[0].body = parsed_stmts.body
308310
exec(compile(parsed_fn, filename="<ast>", mode="exec"), globals, locals)
309311
return await eval(f'{fn_name}()', globals, locals) # fmt: skip
312+
313+
314+
315+
class _CallbackEntryPoint:
316+
def __init__(self, py_callback):
317+
self._py_callback = py_callback
318+
self._last_time = time.time()
319+
def __call__(self):
320+
t = time.time()
321+
dt = t - self._last_time
322+
self._last_time = t
323+
self._py_callback(dt)
324+
325+
_callback_entry_point = None
326+
327+
328+
def set_main_loop_callback(py_callback, fps=0):
329+
330+
global _callback_entry_point
331+
if _callback_entry_point is not None:
332+
# show a warning if the callback is already set
333+
warnings.warn(""" A main loop callback is already set.
334+
This will be replaced by the new callback,
335+
use cancel_main_loop before setting a new callback to avoid this warning
336+
""",
337+
UserWarning)
338+
339+
cancel_main_loop()
340+
341+
_callback_entry_point = _CallbackEntryPoint(py_callback)
342+
343+
pyjs_core._set_main_loop_callback(
344+
_callback_entry_point,
345+
int(fps)
346+
)
347+
348+
349+
def cancel_main_loop():
350+
"""Cancel the main loop callback."""
351+
global _callback_entry_point
352+
if _callback_entry_point is not None:
353+
pyjs_core._cancel_main_loop()
354+
pyjs_core._set_noop_main_loop()
355+
_callback_entry_point = None
356+
else:
357+
warnings.warn("No main loop callback is set to cancel.", UserWarning)

src/export_pyjs_module.cpp

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,98 @@ namespace py = pybind11;
1616
namespace em = emscripten;
1717

1818
namespace pyjs
19-
{
19+
{
20+
21+
// struct LoopContext
22+
// {
23+
// py::object m_callback;
24+
// bool m_cancel_loop_on_error = true; // default to true
25+
// bool m_exit_loop = false;
26+
27+
// LoopContext(py::object callback, bool cancel_loop_on_error)
28+
// : m_callback(std::move(callback)), m_cancel_loop_on_error(cancel_loop_on_error), m_exit_loop(false) {}
29+
// };
30+
31+
32+
void wrapped_callback(void* cb_ptr) {
33+
// Reinterpret the void pointer back to a PyObject pointer
34+
auto py_object = reinterpret_cast<PyObject*>(cb_ptr);
35+
if(!py_object) {
36+
std::cerr << "Error: callback pointer is null." << std::endl;
37+
}
38+
// We can use PyObject_CallObject to call the Python function
39+
if (PyObject_CallNoArgs(py_object) == nullptr) {
40+
// If the call fails, we can print the error
41+
std::cerr << "Error calling Python callback:" << std::endl;
42+
PyErr_Print();
43+
}
44+
};
45+
46+
void noop_callback() {
47+
// This is a no-op callback, it does nothing
48+
49+
// we see a strange error when we run emscripten_cancel_main_loop
50+
// **WITHOUT setting a new loop right away**
51+
// so instead of just cancelling the loop, we need
52+
// to cancel and right away set a new loop
53+
}
54+
55+
void self_cancel_callback() {
56+
emscripten_cancel_main_loop();
57+
};
58+
59+
60+
void export_main_loop_callbacks(py::module_& pyjs_module)
61+
{
62+
63+
64+
65+
// // class for loop context
66+
// py::class_<LoopContext>(pyjs_module, "LoopContext")
67+
// .def(py::init<py::object, bool>(), py::arg("callback"), py::arg("cancel_loop_on_error") = true)
68+
// .def_readwrite("exit_loop", &LoopContext::m_exit_loop)
69+
// ;
70+
71+
72+
73+
// Export main loop callbacks
74+
pyjs_module.def("_set_main_loop_callback", [](py::handle callback, int fps) {
75+
76+
// get a PyObject * from the handle
77+
auto py_object = callback.ptr();
78+
79+
// convert the PyObject to void*
80+
void* callback_ptr = reinterpret_cast<void*>(py_object);
81+
82+
83+
// use emscripten_set_main_loop_arg
84+
emscripten_set_main_loop_arg(
85+
wrapped_callback,
86+
callback_ptr, // pass the callback pointer as argument
87+
fps, // frames per second
88+
false
89+
);
90+
});
91+
92+
// explicit cancel main loop
93+
pyjs_module.def("_cancel_main_loop", []() {
94+
// This will cancel the main loop if it is currently running
95+
emscripten_cancel_main_loop();
96+
});
97+
98+
pyjs_module.def("_set_noop_main_loop", []() {
99+
// This will set a no-op main loop
100+
emscripten_set_main_loop(noop_callback, 1, false); // set a no-op loop to avoid errors
101+
});
102+
103+
}
104+
105+
106+
20107
void export_pyjs_module(py::module_& pyjs_module)
21108
{
22109
export_js_proxy(pyjs_module);
23-
try
24-
{
25-
// pyjs_core_pseudo_init(pyjs_module);
26-
// pyjs_extend_js_val_pseudo_init(pyjs_module);
27-
// pyjs_error_handling_pseudo_init(pyjs_module);
28-
// pyjs_convert_pseudo_init(pyjs_module);
29-
// pyjs_convert_py_to_js_pseudo_init(pyjs_module);
30-
// pyjs_webloop_pseudo_init(pyjs_module);
31-
// pyjs_pyodide_polyfill_pseudo_init(pyjs_module);
32-
}
33-
catch (py::error_already_set& e)
34-
{
35-
std::cout << "error: " << e.what() << "\n";
36-
throw e;
37-
}
110+
export_main_loop_callbacks(pyjs_module);
38111

39112
}
40113
}

0 commit comments

Comments
 (0)