mirror of https://github.com/pyodide/pyodide.git
ENG create_proxy and create_once_proxy APIs (#1334)
(for reducing memory leaks)
This commit is contained in:
parent
a5e21ba75a
commit
2d7a9f288e
|
@ -22,6 +22,8 @@ Backward compatibility of the API is not guaranteed at this point.
|
||||||
pyodide.console.repr_shorten
|
pyodide.console.repr_shorten
|
||||||
pyodide.console.displayhook
|
pyodide.console.displayhook
|
||||||
pyodide.webloop.WebLoop
|
pyodide.webloop.WebLoop
|
||||||
|
pyodide.create_proxy
|
||||||
|
pyodide.create_once_callable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -388,17 +388,37 @@ EM_JS_REF(JsRef, hiwire_dir, (JsRef idobj), {
|
||||||
return Module.hiwire.new_value(result);
|
return Module.hiwire.new_value(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static JsRef
|
||||||
|
convert_va_args(va_list args)
|
||||||
|
{
|
||||||
|
JsRef idargs = hiwire_array();
|
||||||
|
while (true) {
|
||||||
|
JsRef idarg = va_arg(args, JsRef);
|
||||||
|
if (idarg == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hiwire_push_array(idargs, idarg);
|
||||||
|
}
|
||||||
|
va_end(args);
|
||||||
|
return idargs;
|
||||||
|
}
|
||||||
|
|
||||||
EM_JS_REF(JsRef, hiwire_call, (JsRef idfunc, JsRef idargs), {
|
EM_JS_REF(JsRef, hiwire_call, (JsRef idfunc, JsRef idargs), {
|
||||||
let jsfunc = Module.hiwire.get_value(idfunc);
|
let jsfunc = Module.hiwire.get_value(idfunc);
|
||||||
let jsargs = Module.hiwire.get_value(idargs);
|
let jsargs = Module.hiwire.get_value(idargs);
|
||||||
return Module.hiwire.new_value(jsfunc(... jsargs));
|
return Module.hiwire.new_value(jsfunc(... jsargs));
|
||||||
});
|
});
|
||||||
|
|
||||||
EM_JS_REF(JsRef, hiwire_call_OneArg, (JsRef idfunc, JsRef idarg), {
|
JsRef
|
||||||
let jsfunc = Module.hiwire.get_value(idfunc);
|
hiwire_call_va(JsRef idobj, ...)
|
||||||
let jsarg = Module.hiwire.get_value(idarg);
|
{
|
||||||
return Module.hiwire.new_value(jsfunc(jsarg));
|
va_list args;
|
||||||
});
|
va_start(args, idobj);
|
||||||
|
JsRef idargs = convert_va_args(args);
|
||||||
|
JsRef idresult = hiwire_call(idobj, idargs);
|
||||||
|
hiwire_decref(idargs);
|
||||||
|
return idresult;
|
||||||
|
}
|
||||||
|
|
||||||
EM_JS_REF(JsRef,
|
EM_JS_REF(JsRef,
|
||||||
hiwire_call_bound,
|
hiwire_call_bound,
|
||||||
|
@ -427,6 +447,17 @@ EM_JS_REF(JsRef,
|
||||||
return Module.hiwire.new_value(jsobj[jsname](... jsargs));
|
return Module.hiwire.new_value(jsobj[jsname](... jsargs));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JsRef
|
||||||
|
hiwire_call_member_va(JsRef idobj, const char* ptrname, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
va_start(args, ptrname);
|
||||||
|
JsRef idargs = convert_va_args(args);
|
||||||
|
JsRef idresult = hiwire_call_member(idobj, ptrname, idargs);
|
||||||
|
hiwire_decref(idargs);
|
||||||
|
return idresult;
|
||||||
|
}
|
||||||
|
|
||||||
EM_JS_REF(JsRef, hiwire_new, (JsRef idobj, JsRef idargs), {
|
EM_JS_REF(JsRef, hiwire_new, (JsRef idobj, JsRef idargs), {
|
||||||
let jsobj = Module.hiwire.get_value(idobj);
|
let jsobj = Module.hiwire.get_value(idobj);
|
||||||
let jsargs = Module.hiwire.get_value(idargs);
|
let jsargs = Module.hiwire.get_value(idargs);
|
||||||
|
|
|
@ -364,13 +364,19 @@ hiwire_dir(JsRef idobj);
|
||||||
*
|
*
|
||||||
* idargs is a hiwire Array containing the arguments.
|
* idargs is a hiwire Array containing the arguments.
|
||||||
*
|
*
|
||||||
* Returns: New reference
|
|
||||||
*/
|
*/
|
||||||
JsRef
|
JsRef
|
||||||
hiwire_call(JsRef idobj, JsRef idargs);
|
hiwire_call(JsRef idobj, JsRef idargs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a function
|
||||||
|
*
|
||||||
|
* Arguments are specified as a NULL-terminated variable arguments list of
|
||||||
|
* JsRefs.
|
||||||
|
*
|
||||||
|
*/
|
||||||
JsRef
|
JsRef
|
||||||
hiwire_call_OneArg(JsRef idfunc, JsRef idarg);
|
hiwire_call_va(JsRef idobj, ...);
|
||||||
|
|
||||||
JsRef
|
JsRef
|
||||||
hiwire_call_bound(JsRef idfunc, JsRef idthis, JsRef idargs);
|
hiwire_call_bound(JsRef idfunc, JsRef idthis, JsRef idargs);
|
||||||
|
@ -378,15 +384,26 @@ hiwire_call_bound(JsRef idfunc, JsRef idthis, JsRef idargs);
|
||||||
/**
|
/**
|
||||||
* Call a member function.
|
* Call a member function.
|
||||||
*
|
*
|
||||||
* ptrname is the member name, as a char * to null-terminated UTF8.
|
* ptrname is the member name, as a null-terminated UTF8.
|
||||||
*
|
*
|
||||||
* idargs is a hiwire Array containing the arguments.
|
* idargs is a hiwire Array containing the arguments.
|
||||||
*
|
*
|
||||||
* Returns: New reference
|
|
||||||
*/
|
*/
|
||||||
JsRef
|
JsRef
|
||||||
hiwire_call_member(JsRef idobj, const char* ptrname, JsRef idargs);
|
hiwire_call_member(JsRef idobj, const char* ptrname, JsRef idargs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a member function.
|
||||||
|
*
|
||||||
|
* ptrname is the member name, as a null-terminated UTF8.
|
||||||
|
*
|
||||||
|
* Arguments are specified as a NULL-terminated variable arguments list of
|
||||||
|
* JsRefs.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
JsRef
|
||||||
|
hiwire_call_member_va(JsRef idobj, const char* ptrname, ...);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the constructor of a class object.
|
* Calls the constructor of a class object.
|
||||||
*
|
*
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#include "hiwire.h"
|
#include "hiwire.h"
|
||||||
#include "js2python.h"
|
#include "js2python.h"
|
||||||
#include "jsproxy.h"
|
#include "jsproxy.h"
|
||||||
|
#include "pyproxy.h"
|
||||||
#include "python2js.h"
|
#include "python2js.h"
|
||||||
|
|
||||||
#include "structmember.h"
|
#include "structmember.h"
|
||||||
|
@ -625,13 +626,14 @@ JsProxy_Await(JsProxy* self, PyObject* _args)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main
|
|
||||||
PyObject* result = NULL;
|
|
||||||
|
|
||||||
PyObject* loop = NULL;
|
PyObject* loop = NULL;
|
||||||
PyObject* fut = NULL;
|
PyObject* fut = NULL;
|
||||||
PyObject* set_result = NULL;
|
PyObject* set_result = NULL;
|
||||||
PyObject* set_exception = NULL;
|
PyObject* set_exception = NULL;
|
||||||
|
JsRef promise_id = NULL;
|
||||||
|
JsRef promise_handles = NULL;
|
||||||
|
JsRef promise_result = NULL;
|
||||||
|
PyObject* result = NULL;
|
||||||
|
|
||||||
loop = _PyObject_CallNoArg(asyncio_get_event_loop);
|
loop = _PyObject_CallNoArg(asyncio_get_event_loop);
|
||||||
FAIL_IF_NULL(loop);
|
FAIL_IF_NULL(loop);
|
||||||
|
@ -644,26 +646,123 @@ JsProxy_Await(JsProxy* self, PyObject* _args)
|
||||||
set_exception = _PyObject_GetAttrId(fut, &PyId_set_exception);
|
set_exception = _PyObject_GetAttrId(fut, &PyId_set_exception);
|
||||||
FAIL_IF_NULL(set_exception);
|
FAIL_IF_NULL(set_exception);
|
||||||
|
|
||||||
JsRef promise_id = hiwire_resolve_promise(self->js);
|
promise_id = hiwire_resolve_promise(self->js);
|
||||||
JsRef idargs = hiwire_array();
|
FAIL_IF_NULL(promise_id);
|
||||||
JsRef idarg;
|
promise_handles = create_promise_handles(set_result, set_exception);
|
||||||
// TODO: does this leak set_result and set_exception? See #1006.
|
FAIL_IF_NULL(promise_handles);
|
||||||
idarg = python2js(set_result);
|
promise_result = hiwire_call_member(promise_id, "then", promise_handles);
|
||||||
hiwire_push_array(idargs, idarg);
|
FAIL_IF_NULL(promise_result);
|
||||||
hiwire_decref(idarg);
|
|
||||||
idarg = python2js(set_exception);
|
|
||||||
hiwire_push_array(idargs, idarg);
|
|
||||||
hiwire_decref(idarg);
|
|
||||||
hiwire_decref(hiwire_call_member(promise_id, "then", idargs));
|
|
||||||
hiwire_decref(promise_id);
|
|
||||||
hiwire_decref(idargs);
|
|
||||||
result = _PyObject_CallMethodId(fut, &PyId___await__, NULL);
|
result = _PyObject_CallMethodId(fut, &PyId___await__, NULL);
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
Py_CLEAR(loop);
|
Py_CLEAR(loop);
|
||||||
|
Py_CLEAR(fut);
|
||||||
Py_CLEAR(set_result);
|
Py_CLEAR(set_result);
|
||||||
Py_CLEAR(set_exception);
|
Py_CLEAR(set_exception);
|
||||||
Py_DECREF(fut);
|
hiwire_CLEAR(promise_id);
|
||||||
|
hiwire_CLEAR(promise_handles);
|
||||||
|
hiwire_CLEAR(promise_result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload for `then` for JsProxies with a `then` method. Of course without
|
||||||
|
* this overload, the call would just fall through to the normal `then`
|
||||||
|
* function. The advantage of this overload is that it automatically releases
|
||||||
|
* the references to the onfulfilled and onrejected callbacks, which is quite
|
||||||
|
* hard to do otherwise.
|
||||||
|
*/
|
||||||
|
PyObject*
|
||||||
|
JsProxy_then(JsProxy* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
PyObject* onfulfilled = NULL;
|
||||||
|
PyObject* onrejected = NULL;
|
||||||
|
JsRef promise_handles = NULL;
|
||||||
|
JsRef result_promise = NULL;
|
||||||
|
PyObject* result = NULL;
|
||||||
|
|
||||||
|
static char* kwlist[] = { "onfulfilled", "onrejected", 0 };
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(
|
||||||
|
args, kwds, "|OO:then", kwlist, &onfulfilled, &onrejected)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (onfulfilled == Py_None) {
|
||||||
|
Py_CLEAR(onfulfilled);
|
||||||
|
}
|
||||||
|
if (onrejected == Py_None) {
|
||||||
|
Py_CLEAR(onrejected);
|
||||||
|
}
|
||||||
|
promise_handles = create_promise_handles(onfulfilled, onrejected);
|
||||||
|
FAIL_IF_NULL(promise_handles);
|
||||||
|
result_promise = hiwire_call_member(self->js, "then", promise_handles);
|
||||||
|
if (result_promise == NULL) {
|
||||||
|
Py_CLEAR(onfulfilled);
|
||||||
|
Py_CLEAR(onrejected);
|
||||||
|
FAIL();
|
||||||
|
}
|
||||||
|
result = JsProxy_create(result_promise);
|
||||||
|
|
||||||
|
finally:
|
||||||
|
hiwire_CLEAR(promise_handles);
|
||||||
|
hiwire_CLEAR(result_promise);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload for `catch` for JsProxies with a `then` method.
|
||||||
|
*/
|
||||||
|
PyObject*
|
||||||
|
JsProxy_catch(JsProxy* self, PyObject* onrejected)
|
||||||
|
{
|
||||||
|
JsRef promise_handles = NULL;
|
||||||
|
JsRef result_promise = NULL;
|
||||||
|
PyObject* result = NULL;
|
||||||
|
|
||||||
|
// We have to use create_promise_handles so that the handler gets released
|
||||||
|
// even if the promise resolves successfully.
|
||||||
|
promise_handles = create_promise_handles(NULL, onrejected);
|
||||||
|
FAIL_IF_NULL(promise_handles);
|
||||||
|
result_promise = hiwire_call_member(self->js, "then", promise_handles);
|
||||||
|
if (result_promise == NULL) {
|
||||||
|
Py_DECREF(onrejected);
|
||||||
|
FAIL();
|
||||||
|
}
|
||||||
|
result = JsProxy_create(result_promise);
|
||||||
|
|
||||||
|
finally:
|
||||||
|
hiwire_CLEAR(promise_handles);
|
||||||
|
hiwire_CLEAR(result_promise);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload for `finally` for JsProxies with a `then` method. This isn't
|
||||||
|
* strictly necessary since one could get the same effect by just calling
|
||||||
|
* create_once_callable on the argument, but it'd be bad to have `then` and
|
||||||
|
* `catch` handle freeing the handler automatically but require something extra
|
||||||
|
* to use `finally`.
|
||||||
|
*/
|
||||||
|
PyObject*
|
||||||
|
JsProxy_finally(JsProxy* self, PyObject* onfinally)
|
||||||
|
{
|
||||||
|
JsRef proxy = NULL;
|
||||||
|
JsRef result_promise = NULL;
|
||||||
|
PyObject* result = NULL;
|
||||||
|
|
||||||
|
// Finally method is called no matter what so we can use
|
||||||
|
// `create_once_callable`.
|
||||||
|
proxy = create_once_callable(onfinally);
|
||||||
|
FAIL_IF_NULL(proxy);
|
||||||
|
result_promise = hiwire_call_member_va(self->js, "finally", proxy, NULL);
|
||||||
|
if (result_promise == NULL) {
|
||||||
|
Py_DECREF(onfinally);
|
||||||
|
FAIL();
|
||||||
|
}
|
||||||
|
result = JsProxy_create(result_promise);
|
||||||
|
|
||||||
|
finally:
|
||||||
|
hiwire_CLEAR(proxy);
|
||||||
|
hiwire_CLEAR(result_promise);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1068,7 +1167,7 @@ JsProxy_create_subtype(int flags)
|
||||||
// Make sure these stack allocations are large enough to fit!
|
// Make sure these stack allocations are large enough to fit!
|
||||||
PyType_Slot slots[20];
|
PyType_Slot slots[20];
|
||||||
int cur_slot = 0;
|
int cur_slot = 0;
|
||||||
PyMethodDef methods[5];
|
PyMethodDef methods[10];
|
||||||
int cur_method = 0;
|
int cur_method = 0;
|
||||||
PyMemberDef members[5];
|
PyMemberDef members[5];
|
||||||
int cur_member = 0;
|
int cur_member = 0;
|
||||||
|
@ -1141,6 +1240,35 @@ JsProxy_create_subtype(int flags)
|
||||||
if (flags & IS_AWAITABLE) {
|
if (flags & IS_AWAITABLE) {
|
||||||
slots[cur_slot++] =
|
slots[cur_slot++] =
|
||||||
(PyType_Slot){ .slot = Py_am_await, .pfunc = (void*)JsProxy_Await };
|
(PyType_Slot){ .slot = Py_am_await, .pfunc = (void*)JsProxy_Await };
|
||||||
|
methods[cur_method++] = (PyMethodDef){
|
||||||
|
"then",
|
||||||
|
(PyCFunction)JsProxy_then,
|
||||||
|
METH_VARARGS | METH_KEYWORDS,
|
||||||
|
PyDoc_STR(
|
||||||
|
"then(onfulfilled : Callable = None, onrejected : Callable = None)"
|
||||||
|
" -> JsProxy"
|
||||||
|
"\n\n"
|
||||||
|
"The promise.then api, wrapped to manage the lifetimes of the "
|
||||||
|
"arguments"),
|
||||||
|
};
|
||||||
|
methods[cur_method++] = (PyMethodDef){
|
||||||
|
"catch",
|
||||||
|
(PyCFunction)JsProxy_catch,
|
||||||
|
METH_O,
|
||||||
|
PyDoc_STR("catch(onrejected : Callable) -> JsProxy"
|
||||||
|
"\n\n"
|
||||||
|
"The promise.catch api, wrapped to manage the lifetime of the "
|
||||||
|
"argument."),
|
||||||
|
};
|
||||||
|
methods[cur_method++] = (PyMethodDef){
|
||||||
|
"finally_",
|
||||||
|
(PyCFunction)JsProxy_finally,
|
||||||
|
METH_O,
|
||||||
|
PyDoc_STR("finally_(onrejected : Callable) -> JsProxy"
|
||||||
|
"\n\n"
|
||||||
|
"The promise.finally api, wrapped to manage the lifetime of "
|
||||||
|
"the argument."),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (flags & IS_CALLABLE) {
|
if (flags & IS_CALLABLE) {
|
||||||
tp_flags |= _Py_TPFLAGS_HAVE_VECTORCALL;
|
tp_flags |= _Py_TPFLAGS_HAVE_VECTORCALL;
|
||||||
|
|
|
@ -109,7 +109,7 @@ main(int argc, char** argv)
|
||||||
TRY_INIT_WITH_CORE_MODULE(error_handling);
|
TRY_INIT_WITH_CORE_MODULE(error_handling);
|
||||||
TRY_INIT(js2python);
|
TRY_INIT(js2python);
|
||||||
TRY_INIT_WITH_CORE_MODULE(JsProxy);
|
TRY_INIT_WITH_CORE_MODULE(JsProxy);
|
||||||
TRY_INIT(pyproxy);
|
TRY_INIT_WITH_CORE_MODULE(pyproxy);
|
||||||
TRY_INIT(keyboard_interrupt);
|
TRY_INIT(keyboard_interrupt);
|
||||||
|
|
||||||
PyObject* module_dict = PyImport_GetModuleDict(); // borrowed
|
PyObject* module_dict = PyImport_GetModuleDict(); // borrowed
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include "hiwire.h"
|
#include "hiwire.h"
|
||||||
#include "js2python.h"
|
#include "js2python.h"
|
||||||
|
#include "jsproxy.h"
|
||||||
#include "python2js.h"
|
#include "python2js.h"
|
||||||
|
|
||||||
_Py_IDENTIFIER(result);
|
_Py_IDENTIFIER(result);
|
||||||
|
@ -501,7 +502,7 @@ FutureDoneCallback_call_resolve(FutureDoneCallback* self, PyObject* result)
|
||||||
JsRef result_js = NULL;
|
JsRef result_js = NULL;
|
||||||
JsRef output = NULL;
|
JsRef output = NULL;
|
||||||
result_js = python2js(result);
|
result_js = python2js(result);
|
||||||
output = hiwire_call_OneArg(self->resolve_handle, result_js);
|
output = hiwire_call_va(self->resolve_handle, result_js, NULL);
|
||||||
|
|
||||||
success = true;
|
success = true;
|
||||||
finally:
|
finally:
|
||||||
|
@ -523,7 +524,7 @@ FutureDoneCallback_call_reject(FutureDoneCallback* self)
|
||||||
// wrap_exception looks up the current exception and wraps it in a Js error.
|
// wrap_exception looks up the current exception and wraps it in a Js error.
|
||||||
excval = wrap_exception(false);
|
excval = wrap_exception(false);
|
||||||
FAIL_IF_NULL(excval);
|
FAIL_IF_NULL(excval);
|
||||||
result = hiwire_call_OneArg(self->reject_handle, excval);
|
result = hiwire_call_va(self->reject_handle, excval, NULL);
|
||||||
|
|
||||||
success = true;
|
success = true;
|
||||||
finally:
|
finally:
|
||||||
|
@ -741,6 +742,22 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Module.callPyObject = function(ptrobj, ...jsargs) {
|
||||||
|
let idargs = Module.hiwire.new_value(jsargs);
|
||||||
|
let idresult;
|
||||||
|
try {
|
||||||
|
idresult = __pyproxy_apply(ptrobj, idargs);
|
||||||
|
} catch(e){
|
||||||
|
Module.fatal_error(e);
|
||||||
|
} finally {
|
||||||
|
Module.hiwire.decref(idargs);
|
||||||
|
}
|
||||||
|
if(idresult === 0){
|
||||||
|
_pythonexc2js();
|
||||||
|
}
|
||||||
|
return Module.hiwire.pop_value(idresult);
|
||||||
|
};
|
||||||
|
|
||||||
// Now a lot of boilerplate to wrap the abstract Object protocol wrappers
|
// Now a lot of boilerplate to wrap the abstract Object protocol wrappers
|
||||||
// above in Javascript functions.
|
// above in Javascript functions.
|
||||||
|
|
||||||
|
@ -785,6 +802,12 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
|
||||||
Module.hiwire.decref(idresult);
|
Module.hiwire.decref(idresult);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
apply(jsthis, jsargs) {
|
||||||
|
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||||
|
}
|
||||||
|
call(jsthis, ...jsargs){
|
||||||
|
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Controlled by HAS_LENGTH, appears for any object with __len__ or sq_length
|
// Controlled by HAS_LENGTH, appears for any object with __len__ or sq_length
|
||||||
|
@ -1107,20 +1130,7 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
apply: function (jsobj, jsthis, jsargs) {
|
apply: function (jsobj, jsthis, jsargs) {
|
||||||
let ptrobj = _getPtr(jsobj);
|
return jsobj.apply(jsthis, jsargs);
|
||||||
let idargs = Module.hiwire.new_value(jsargs);
|
|
||||||
let idresult;
|
|
||||||
try {
|
|
||||||
idresult = __pyproxy_apply(ptrobj, idargs);
|
|
||||||
} catch(e){
|
|
||||||
Module.fatal_error(e);
|
|
||||||
} finally {
|
|
||||||
Module.hiwire.decref(idargs);
|
|
||||||
}
|
|
||||||
if(idresult === 0){
|
|
||||||
_pythonexc2js();
|
|
||||||
}
|
|
||||||
return Module.hiwire.pop_value(idresult);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1220,14 +1230,138 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
|
||||||
Module.wrapNamespace = function wrapNamespace(ns){
|
Module.wrapNamespace = function wrapNamespace(ns){
|
||||||
return new Proxy(ns, NamespaceProxyHandlers);
|
return new Proxy(ns, NamespaceProxyHandlers);
|
||||||
};
|
};
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
int
|
EM_JS_REF(JsRef, create_once_callable, (PyObject * obj), {
|
||||||
pyproxy_init()
|
_Py_IncRef(obj);
|
||||||
|
let alreadyCalled = false;
|
||||||
|
function wrapper(... args)
|
||||||
|
{
|
||||||
|
if (alreadyCalled) {
|
||||||
|
throw new Error("OnceProxy can only be called once");
|
||||||
|
}
|
||||||
|
alreadyCalled = true;
|
||||||
|
try {
|
||||||
|
return Module.callPyObject(obj, ... args);
|
||||||
|
} finally {
|
||||||
|
_Py_DecRef(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrapper.destroy = function()
|
||||||
|
{
|
||||||
|
if (alreadyCalled) {
|
||||||
|
throw new Error("OnceProxy has already been destroyed");
|
||||||
|
}
|
||||||
|
alreadyCalled = true;
|
||||||
|
_Py_DecRef(obj);
|
||||||
|
};
|
||||||
|
return Module.hiwire.new_value(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
create_once_callable_py(PyObject* _mod, PyObject* obj)
|
||||||
{
|
{
|
||||||
|
JsRef ref = create_once_callable(obj);
|
||||||
|
PyObject* result = JsProxy_create(ref);
|
||||||
|
hiwire_decref(ref);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
EM_JS_REF(JsRef, create_promise_handles, (
|
||||||
|
PyObject* handle_result, PyObject* handle_exception
|
||||||
|
), {
|
||||||
|
if (handle_result) {
|
||||||
|
_Py_IncRef(handle_result);
|
||||||
|
}
|
||||||
|
if (handle_exception) {
|
||||||
|
_Py_IncRef(handle_exception);
|
||||||
|
}
|
||||||
|
let used = false;
|
||||||
|
function checkUsed(){
|
||||||
|
if (used) {
|
||||||
|
throw new Error("One of the promise handles has already been called.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function destroy(){
|
||||||
|
checkUsed();
|
||||||
|
used = true;
|
||||||
|
if(handle_result){
|
||||||
|
_Py_DecRef(handle_result);
|
||||||
|
}
|
||||||
|
if(handle_exception){
|
||||||
|
_Py_DecRef(handle_exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onFulfilled(res) {
|
||||||
|
checkUsed();
|
||||||
|
try {
|
||||||
|
if(handle_result){
|
||||||
|
return Module.callPyObject(handle_result, res);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onRejected(err) {
|
||||||
|
checkUsed();
|
||||||
|
try {
|
||||||
|
if(handle_exception){
|
||||||
|
return Module.callPyObject(handle_exception, err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onFulfilled.destroy = destroy;
|
||||||
|
onRejected.destroy = destroy;
|
||||||
|
return Module.hiwire.new_value(
|
||||||
|
[onFulfilled, onRejected]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
create_proxy(PyObject* _mod, PyObject* obj)
|
||||||
|
{
|
||||||
|
JsRef ref = pyproxy_new(obj);
|
||||||
|
PyObject* result = JsProxy_create(ref);
|
||||||
|
hiwire_decref(ref);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyMethodDef pyproxy_methods[] = {
|
||||||
|
{
|
||||||
|
"create_once_callable",
|
||||||
|
create_once_callable_py,
|
||||||
|
METH_O,
|
||||||
|
PyDoc_STR(
|
||||||
|
"create_once_callable(obj : Callable) -> JsProxy"
|
||||||
|
"\n\n"
|
||||||
|
"Wrap a Python callable in a Javascript function that can be called "
|
||||||
|
"once. After being called the proxy will decrement the reference count "
|
||||||
|
"of the Callable. The javascript function also has a `destroy` API that "
|
||||||
|
"can be used to release the proxy without calling it."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create_proxy",
|
||||||
|
create_proxy,
|
||||||
|
METH_O,
|
||||||
|
PyDoc_STR("create_proxy(obj : Any) -> JsProxy"
|
||||||
|
"\n\n"
|
||||||
|
"Create a `JsProxy` of a `PyProxy`. This allows explicit control "
|
||||||
|
"over the lifetime of the `PyProxy` from Python: call the "
|
||||||
|
"`destroy` API when done."),
|
||||||
|
},
|
||||||
|
{ NULL } /* Sentinel */
|
||||||
|
};
|
||||||
|
|
||||||
|
int
|
||||||
|
pyproxy_init(PyObject* core)
|
||||||
|
{
|
||||||
|
PyModule_AddFunctions(core, pyproxy_methods);
|
||||||
asyncio = PyImport_ImportModule("asyncio");
|
asyncio = PyImport_ImportModule("asyncio");
|
||||||
if (asyncio == NULL) {
|
if (asyncio == NULL) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
@ -3,15 +3,35 @@
|
||||||
#define PY_SSIZE_T_CLEAN
|
#define PY_SSIZE_T_CLEAN
|
||||||
#include "Python.h"
|
#include "Python.h"
|
||||||
|
|
||||||
/** Makes Python objects usable from Javascript.
|
/**
|
||||||
|
* Makes Python objects usable from Javascript.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This implements the Javascript Proxy handler interface as defined here:
|
// This implements the Javascript Proxy handler interface as defined here:
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
||||||
|
|
||||||
JsRef
|
JsRef
|
||||||
pyproxy_new(PyObject* obj);
|
pyproxy_new(PyObject* obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a Python callable in a Javascript function that can be called once.
|
||||||
|
* After being called, the reference count of the python object is automatically
|
||||||
|
* decremented. The Proxy also has a "destroy" API that can decrement the
|
||||||
|
* reference count without calling the function.
|
||||||
|
*/
|
||||||
|
JsRef
|
||||||
|
create_once_callable(PyObject* obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a pair of Python callables in a Javascript function that can be called
|
||||||
|
* once between the two of them. After being called, the reference counts of
|
||||||
|
* both python objects are automatically decremented. The wrappers also have a
|
||||||
|
* "destroy" API that can decrement the reference counts without calling the
|
||||||
|
* function. Intended for use with `promise.then`.
|
||||||
|
*/
|
||||||
|
JsRef
|
||||||
|
create_promise_handles(PyObject* onfulfilled, PyObject* onrejected);
|
||||||
|
|
||||||
int
|
int
|
||||||
pyproxy_init();
|
pyproxy_init();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from ._base import open_url, eval_code, eval_code_async, find_imports, as_nested_list
|
from ._base import open_url, eval_code, eval_code_async, find_imports, as_nested_list
|
||||||
from ._core import JsException # type: ignore
|
from ._core import JsException, create_once_callable, create_proxy # type: ignore
|
||||||
from ._importhooks import JsFinder
|
from ._importhooks import JsFinder
|
||||||
from .webloop import WebLoopPolicy
|
from .webloop import WebLoopPolicy
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -26,4 +26,6 @@ __all__ = [
|
||||||
"JsException",
|
"JsException",
|
||||||
"register_js_module",
|
"register_js_module",
|
||||||
"unregister_js_module",
|
"unregister_js_module",
|
||||||
|
"create_once_callable",
|
||||||
|
"create_proxy",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import platform
|
import platform
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
if platform.system() == "Emscripten":
|
if platform.system() == "Emscripten":
|
||||||
from _pyodide_core import JsProxy, JsException
|
from _pyodide_core import (
|
||||||
|
JsProxy,
|
||||||
|
JsException,
|
||||||
|
create_proxy,
|
||||||
|
create_once_callable,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Can add shims here if we are so inclined.
|
# Can add shims here if we are so inclined.
|
||||||
class JsException(Exception): # type: ignore
|
class JsException(Exception): # type: ignore
|
||||||
|
@ -16,5 +22,23 @@ else:
|
||||||
|
|
||||||
# Defined in jsproxy.c
|
# Defined in jsproxy.c
|
||||||
|
|
||||||
|
# Defined in jsproxy.c
|
||||||
|
|
||||||
__all__ = ["JsProxy", "JsException"]
|
def create_once_callable(obj: Callable) -> JsProxy:
|
||||||
|
"""Wrap a Python callable in a Javascript function that can be called
|
||||||
|
once. After being called the proxy will decrement the reference count
|
||||||
|
of the Callable. The javascript function also has a `destroy` API that
|
||||||
|
can be used to release the proxy without calling it.
|
||||||
|
"""
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def create_proxy(obj: Any) -> JsProxy:
|
||||||
|
"""Create a `JsProxy` of a `PyProxy`.
|
||||||
|
|
||||||
|
This allows explicit control over the lifetime of the `PyProxy` from
|
||||||
|
Python: call the `destroy` API when done.
|
||||||
|
"""
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["JsProxy", "JsException", "create_proxy", "create_once_callable"]
|
||||||
|
|
|
@ -132,11 +132,12 @@ class WebLoop(asyncio.AbstractEventLoop):
|
||||||
This uses `setTimeout(callback, delay)`
|
This uses `setTimeout(callback, delay)`
|
||||||
"""
|
"""
|
||||||
from js import setTimeout
|
from js import setTimeout
|
||||||
|
from . import create_once_callable
|
||||||
|
|
||||||
if delay < 0:
|
if delay < 0:
|
||||||
raise ValueError("Can't schedule in the past")
|
raise ValueError("Can't schedule in the past")
|
||||||
h = asyncio.Handle(callback, args, self, context=context)
|
h = asyncio.Handle(callback, args, self, context=context)
|
||||||
setTimeout(h._run, delay * 1000)
|
setTimeout(create_once_callable(h._run), delay * 1000)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def call_at(
|
def call_at(
|
||||||
|
|
|
@ -35,6 +35,122 @@ def test_await_jsproxy(selenium):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_then_jsproxy(selenium):
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
def prom(res, rej):
|
||||||
|
global resolve
|
||||||
|
global reject
|
||||||
|
resolve = res
|
||||||
|
reject = rej
|
||||||
|
|
||||||
|
from js import Promise
|
||||||
|
result = None
|
||||||
|
err = None
|
||||||
|
finally_occurred = False
|
||||||
|
|
||||||
|
def onfulfilled(value):
|
||||||
|
global result
|
||||||
|
result = value
|
||||||
|
|
||||||
|
def onrejected(value):
|
||||||
|
global err
|
||||||
|
err = value
|
||||||
|
|
||||||
|
def onfinally():
|
||||||
|
global finally_occurred
|
||||||
|
finally_occurred = True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.then(onfulfilled, onrejected)
|
||||||
|
resolve(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
assert result == 10
|
||||||
|
assert err is None
|
||||||
|
result = None
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.then(onfulfilled, onrejected)
|
||||||
|
reject(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
assert result is None
|
||||||
|
assert err == 10
|
||||||
|
err = None
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.catch(onrejected)
|
||||||
|
resolve(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run("assert err is None")
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.catch(onrejected)
|
||||||
|
reject(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
assert err == 10
|
||||||
|
err = None
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.finally_(onfinally)
|
||||||
|
resolve(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
assert finally_occurred
|
||||||
|
finally_occurred = False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
p = Promise.new(prom)
|
||||||
|
p.finally_(onfinally)
|
||||||
|
reject(10)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
time.sleep(0.01)
|
||||||
|
selenium.run(
|
||||||
|
"""
|
||||||
|
assert finally_occurred
|
||||||
|
finally_occurred = False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_await_fetch(selenium):
|
def test_await_fetch(selenium):
|
||||||
selenium.run(
|
selenium.run(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -255,3 +255,87 @@ def test_run_python_last_exc(selenium):
|
||||||
`);
|
`);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_once_callable(selenium):
|
||||||
|
selenium.run_js(
|
||||||
|
"""
|
||||||
|
window.call7 = function call7(f){
|
||||||
|
return f(7);
|
||||||
|
}
|
||||||
|
pyodide.runPython(`
|
||||||
|
from pyodide import create_once_callable, JsException
|
||||||
|
from js import call7;
|
||||||
|
from unittest import TestCase
|
||||||
|
raises = TestCase().assertRaisesRegex
|
||||||
|
class Square:
|
||||||
|
def __call__(self, x):
|
||||||
|
return x*x
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
global destroyed
|
||||||
|
destroyed = True
|
||||||
|
|
||||||
|
f = Square()
|
||||||
|
import sys
|
||||||
|
assert sys.getrefcount(f) == 2
|
||||||
|
proxy = create_once_callable(f)
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
assert call7(proxy) == 49
|
||||||
|
assert sys.getrefcount(f) == 2
|
||||||
|
with raises(JsException, "can only be called once"):
|
||||||
|
call7(proxy)
|
||||||
|
destroyed = False
|
||||||
|
del f
|
||||||
|
assert destroyed == True
|
||||||
|
del proxy # causes a fatal error =(
|
||||||
|
`);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_proxy(selenium):
|
||||||
|
selenium.run_js(
|
||||||
|
"""
|
||||||
|
window.testAddListener = function(f){
|
||||||
|
window.listener = f;
|
||||||
|
}
|
||||||
|
window.testCallListener = function(f){
|
||||||
|
return window.listener();
|
||||||
|
}
|
||||||
|
window.testRemoveListener = function(f){
|
||||||
|
return window.listener === f;
|
||||||
|
}
|
||||||
|
pyodide.runPython(`
|
||||||
|
from pyodide import create_proxy
|
||||||
|
from js import testAddListener, testCallListener, testRemoveListener;
|
||||||
|
class Test:
|
||||||
|
def __call__(self):
|
||||||
|
return 7
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
global destroyed
|
||||||
|
destroyed = True
|
||||||
|
|
||||||
|
f = Test()
|
||||||
|
import sys
|
||||||
|
assert sys.getrefcount(f) == 2
|
||||||
|
proxy = create_proxy(f)
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
assert proxy() == 7
|
||||||
|
testAddListener(proxy)
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
assert testCallListener() == 7
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
assert testCallListener() == 7
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
assert testRemoveListener(f)
|
||||||
|
assert sys.getrefcount(f) == 3
|
||||||
|
proxy.destroy()
|
||||||
|
assert sys.getrefcount(f) == 2
|
||||||
|
destroyed = False
|
||||||
|
del f
|
||||||
|
assert destroyed == True
|
||||||
|
`);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue