mirror of https://github.com/pyodide/pyodide.git
555 lines
16 KiB
C++
555 lines
16 KiB
C++
#include <string>
|
|
#include <codecvt>
|
|
#include <locale>
|
|
|
|
#include <emscripten.h>
|
|
#include <emscripten/bind.h>
|
|
#include <emscripten/val.h>
|
|
#include <Python.h>
|
|
#include <node.h>
|
|
|
|
// TODO: Bound methods should probably have their own class, rather than using
|
|
// JsProxy for everything
|
|
|
|
using emscripten::val;
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Forward declarations
|
|
|
|
val pythonToJs(PyObject *x);
|
|
PyObject *jsToPython(val x, val *parent=NULL, const char *name=NULL);
|
|
static PyObject *locals = NULL;
|
|
static PyObject *globals = NULL;
|
|
static PyObject *original_globals = NULL;
|
|
static PyObject *JsProxy_cnew(val v, val *parent=NULL, const char *name=NULL);
|
|
static val *undefined;
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// JsProxy
|
|
//
|
|
// This is a Python object that provides ideomatic access to a Javascript object.
|
|
|
|
typedef struct {
|
|
PyObject_HEAD
|
|
val *js;
|
|
val *parent;
|
|
char *name;
|
|
} JsProxy;
|
|
|
|
static void JsProxy_dealloc(JsProxy *self) {
|
|
delete self->js;
|
|
if (self->parent) {
|
|
delete self->parent;
|
|
}
|
|
if (self->name) {
|
|
free(self->name);
|
|
}
|
|
Py_TYPE(self)->tp_free((PyObject *)self);
|
|
}
|
|
|
|
static PyObject *JsProxy_GetAttr(PyObject *o, PyObject *attr_name) {
|
|
JsProxy *self = (JsProxy *)o;
|
|
|
|
PyObject *str = PyObject_Str(attr_name);
|
|
if (str == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
std::string s(PyUnicode_AsUTF8(str));
|
|
val v = (*self->js)[s];
|
|
Py_DECREF(str);
|
|
|
|
return jsToPython(v, self->js, s.c_str());
|
|
}
|
|
|
|
static int JsProxy_SetAttr(PyObject *o, PyObject *attr_name, PyObject *value) {
|
|
JsProxy *self = (JsProxy *)o;
|
|
|
|
PyObject *attr_name_py_str = PyObject_Str(attr_name);
|
|
if (attr_name_py_str == NULL) {
|
|
return NULL;
|
|
}
|
|
std::string attr_name_str(PyUnicode_AsUTF8(attr_name_py_str));
|
|
|
|
val value_js = pythonToJs(value);
|
|
|
|
(*self->js).set(attr_name_str, value_js);
|
|
Py_DECREF(attr_name_py_str);
|
|
|
|
return 0;
|
|
}
|
|
static PyObject* JsProxy_Call(PyObject *o, PyObject *args, PyObject *kwargs) {
|
|
JsProxy *self = (JsProxy *)o;
|
|
|
|
Py_ssize_t nargs = PyTuple_Size(args);
|
|
|
|
// TODO: There's probably some way to not have to explicitly expand arguments
|
|
// here.
|
|
|
|
if (self->parent) {
|
|
switch (nargs) {
|
|
case 0:
|
|
return jsToPython((*self->parent).call<val>(self->name));
|
|
case 1:
|
|
return jsToPython((*self->parent).call<val>(
|
|
self->name,
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0))));
|
|
case 2:
|
|
return jsToPython((*self->parent).call<val>(
|
|
self->name,
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1))));
|
|
case 3:
|
|
return jsToPython((*self->parent).call<val>(
|
|
self->name,
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 2))));
|
|
case 4:
|
|
return jsToPython((*self->parent).call<val>(
|
|
self->name,
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 2)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 3))));
|
|
}
|
|
} else {
|
|
switch (nargs) {
|
|
case 0:
|
|
return jsToPython((*self->js)());
|
|
case 1:
|
|
return jsToPython((*self->js)(
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0))));
|
|
case 2:
|
|
return jsToPython((*self->js)(
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1))));
|
|
case 3:
|
|
return jsToPython((*self->js)(
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 2))));
|
|
case 4:
|
|
return jsToPython((*self->js)(
|
|
pythonToJs(PyTuple_GET_ITEM(args, 0)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 1)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 2)),
|
|
pythonToJs(PyTuple_GET_ITEM(args, 3))));
|
|
}
|
|
}
|
|
// TODO: Handle exception here
|
|
return NULL;
|
|
}
|
|
|
|
static PyTypeObject JsProxyType = {
|
|
PyVarObject_HEAD_INIT(NULL, 0)
|
|
"JsProxy", /* tp_name */
|
|
sizeof(JsProxy), /* tp_basicsize */
|
|
0, /* tp_itemsize */
|
|
(destructor)JsProxy_dealloc, /* tp_dealloc */
|
|
0, /* tp_print */
|
|
0, /* tp_getattr */
|
|
0, /* tp_setattr */
|
|
0, /* tp_reserved */
|
|
0, /* tp_repr */
|
|
0, /* tp_as_number */
|
|
0, /* tp_as_sequence */
|
|
0, /* tp_as_mapping */
|
|
0, /* tp_hash */
|
|
JsProxy_Call, /* tp_call */
|
|
0, /* tp_str */
|
|
JsProxy_GetAttr, /* tp_getattro */
|
|
JsProxy_SetAttr, /* tp_setattro */
|
|
0, /* tp_as_buffer */
|
|
Py_TPFLAGS_DEFAULT,
|
|
"A proxy to make a Javascript object behave like a Python object",
|
|
0, /* tp_traverse */
|
|
0, /* tp_clear */
|
|
0, /* tp_richcompare */
|
|
0, /* tp_weaklistoffset */
|
|
0, /* tp_iter */
|
|
0, /* tp_iternext */
|
|
0, /* tp_methods */
|
|
0, /* tp_members */
|
|
0, /* tp_getset */
|
|
0, /* tp_base */
|
|
0, /* tp_dict */
|
|
0, /* tp_descr_get */
|
|
0, /* tp_descr_set */
|
|
0, /* tp_dictoffset */
|
|
0, /* tp_init */
|
|
0, /* tp_alloc */
|
|
0, /* tp_new */
|
|
};
|
|
|
|
static PyObject *JsProxy_cnew(val v, val *parent, const char *name) {
|
|
JsProxy *self;
|
|
self = (JsProxy *)JsProxyType.tp_alloc(&JsProxyType, 0);
|
|
self->js = new val(v);
|
|
if (parent) {
|
|
self->parent = new val(*parent);
|
|
size_t n = strnlen(name, 256);
|
|
char *copy = (char *)malloc(n + 1);
|
|
strncpy(copy, name, n + 1);
|
|
self->name = copy;
|
|
} else {
|
|
self->parent = NULL;
|
|
}
|
|
return (PyObject *)self;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// LocalsProxy
|
|
//
|
|
// This is an object designed to be used as a "locals" namespace dictionary.
|
|
// It first looks for things in its own internal dictionary, and failing that,
|
|
// looks in the Javascript global namespace. This is a way of merging the
|
|
// Python and Javascript namespaces without fullying copying either one.
|
|
|
|
typedef struct {
|
|
PyObject_HEAD
|
|
PyObject *locals;
|
|
} LocalsProxy;
|
|
|
|
static void LocalsProxy_dealloc(LocalsProxy *self) {
|
|
Py_DECREF(self->locals);
|
|
Py_TYPE(self)->tp_free((PyObject *)self);
|
|
}
|
|
|
|
static Py_ssize_t LocalsProxy_length(PyObject *o) {
|
|
LocalsProxy *self = (LocalsProxy *)o;
|
|
|
|
return PyDict_Size(self->locals);
|
|
}
|
|
|
|
PyObject* LocalsProxy_get(PyObject *o, PyObject *key) {
|
|
LocalsProxy *self = (LocalsProxy *)o;
|
|
|
|
{
|
|
PyObject *str = PyObject_Str(key);
|
|
if (str == NULL) {
|
|
return NULL;
|
|
}
|
|
char *c = PyUnicode_AsUTF8(str);
|
|
Py_DECREF(str);
|
|
}
|
|
|
|
PyObject *py_val = PyDict_GetItem(self->locals, key);
|
|
if (py_val != NULL) {
|
|
Py_INCREF(py_val);
|
|
return py_val;
|
|
}
|
|
|
|
PyObject *str = PyObject_Str(key);
|
|
if (str == NULL) {
|
|
return NULL;
|
|
}
|
|
char *c = PyUnicode_AsUTF8(str);
|
|
val v = val::global(c);
|
|
Py_DECREF(str);
|
|
return jsToPython(v);
|
|
}
|
|
|
|
int LocalsProxy_set(PyObject *o, PyObject *k, PyObject *v) {
|
|
LocalsProxy *self = (LocalsProxy *)o;
|
|
|
|
if (v == NULL) {
|
|
// TODO: This might not actually be here to delete...
|
|
return PyDict_DelItem(self->locals, k);
|
|
} else {
|
|
return PyDict_SetItem(self->locals, k, v);
|
|
}
|
|
}
|
|
|
|
static PyMappingMethods LocalsProxy_as_mapping = {
|
|
LocalsProxy_length,
|
|
LocalsProxy_get,
|
|
LocalsProxy_set
|
|
};
|
|
|
|
static PyTypeObject LocalsProxyType = {
|
|
PyVarObject_HEAD_INIT(NULL, 0)
|
|
"LocalsProxy", /* tp_name */
|
|
sizeof(LocalsProxy), /* tp_basicsize */
|
|
0, /* tp_itemsize */
|
|
(destructor)LocalsProxy_dealloc, /* tp_dealloc */
|
|
0, /* tp_print */
|
|
0, /* tp_getattr */
|
|
0, /* tp_setattr */
|
|
0, /* tp_reserved */
|
|
0, /* tp_repr */
|
|
0, /* tp_as_number */
|
|
0, /* tp_as_sequence */
|
|
&LocalsProxy_as_mapping, /* tp_as_mapping */
|
|
0, /* tp_hash */
|
|
0, /* tp_call */
|
|
0, /* tp_str */
|
|
0, /* tp_getattro */
|
|
0, /* tp_setattro */
|
|
0, /* tp_as_buffer */
|
|
Py_TPFLAGS_DEFAULT,
|
|
"A proxy that looks in a dict first, otherwise the JS global namespace.",
|
|
0, /* tp_traverse */
|
|
0, /* tp_clear */
|
|
0, /* tp_richcompare */
|
|
0, /* tp_weaklistoffset */
|
|
0, /* tp_iter */
|
|
0, /* tp_iternext */
|
|
0, /* tp_methods */
|
|
0, /* tp_members */
|
|
0, /* tp_getset */
|
|
0, /* tp_base */
|
|
0, /* tp_dict */
|
|
0, /* tp_descr_get */
|
|
0, /* tp_descr_set */
|
|
0, /* tp_dictoffset */
|
|
0, /* tp_init */
|
|
0, /* tp_alloc */
|
|
0, /* tp_new */
|
|
};
|
|
|
|
static PyObject *LocalsProxy_cnew(PyObject *d)
|
|
{
|
|
LocalsProxy *self;
|
|
self = (LocalsProxy *)LocalsProxyType.tp_alloc(&LocalsProxyType, 0);
|
|
if (self != NULL) {
|
|
Py_INCREF(d);
|
|
self->locals = d;
|
|
}
|
|
|
|
return (PyObject *)self;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Conversions
|
|
|
|
val pythonExcToJS() {
|
|
PyObject *type;
|
|
PyObject *value;
|
|
PyObject *traceback;
|
|
PyObject *str;
|
|
|
|
PyErr_Fetch(&type, &value, &traceback);
|
|
|
|
str = PyObject_Str(value);
|
|
// TODO: Return a JS Error object rather than a string here...?
|
|
val result = pythonToJs(str);
|
|
|
|
Py_DECREF(str);
|
|
Py_DECREF(type);
|
|
Py_DECREF(value);
|
|
Py_DECREF(traceback);
|
|
|
|
return result;
|
|
}
|
|
|
|
val pythonToJs(PyObject *x) {
|
|
// TODO: bool, None
|
|
if (x == Py_None) {
|
|
return val(*undefined);
|
|
} else if (x == Py_True) {
|
|
return val(true);
|
|
} else if (x == Py_False) {
|
|
return val(false);
|
|
} else if (PyLong_Check(x)) {
|
|
long x_long = PyLong_AsLongLong(x);
|
|
if (x_long == -1 && PyErr_Occurred()) {
|
|
return pythonExcToJS();
|
|
}
|
|
return val(x_long);
|
|
} else if (PyFloat_Check(x)) {
|
|
double x_double = PyFloat_AsDouble(x);
|
|
if (x_double == -1.0 && PyErr_Occurred()) {
|
|
return pythonExcToJS();
|
|
}
|
|
return val(x_double);
|
|
} else if (PyUnicode_Check(x)) {
|
|
// TODO: Not clear whether this is UTF-16 or UCS2
|
|
// TODO: This is doing two copies. Can we reduce?
|
|
Py_ssize_t length;
|
|
wchar_t *chars = PyUnicode_AsWideCharString(x, &length);
|
|
if (chars == NULL) {
|
|
return pythonExcToJS();
|
|
}
|
|
std::wstring x_str(chars, length);
|
|
PyMem_Free(chars);
|
|
return val(x_str);
|
|
} else if (PyBytes_Check(x)) {
|
|
// TODO: This is doing two copies. Can we reduce?
|
|
char *x_buff;
|
|
Py_ssize_t length;
|
|
PyBytes_AsStringAndSize(x, &x_buff, &length);
|
|
std::string x_str(x_buff, length);
|
|
return val(x_str);
|
|
} else if (PyObject_TypeCheck(x, &JsProxyType)) {
|
|
JsProxy *js_proxy = (JsProxy *)x;
|
|
return val(*(js_proxy->js));
|
|
} else if (PySequence_Check(x)) {
|
|
val array = val::global("Array");
|
|
val x_array = array.new_();
|
|
size_t length = PySequence_Size(x);
|
|
for (size_t i = 0; i < length; ++i) {
|
|
PyObject *item = PySequence_GetItem(x, i);
|
|
if (item == NULL) {
|
|
return pythonExcToJS();
|
|
}
|
|
x_array.call<int>("push", pythonToJs(item));
|
|
Py_DECREF(item);
|
|
}
|
|
return x_array;
|
|
} else if (PyDict_Check(x)) {
|
|
val object = val::global("Object");
|
|
val x_object = object.new_();
|
|
PyObject *k, *v;
|
|
Py_ssize_t pos = 0;
|
|
while (PyDict_Next(x, &pos, &k, &v)) {
|
|
x_object.set(pythonToJs(k), pythonToJs(v));
|
|
}
|
|
return x_object;
|
|
} else {
|
|
return val(x);
|
|
}
|
|
}
|
|
|
|
PyObject *jsToPython(val x, val *parent, const char *name) {
|
|
val xType = x.typeOf();
|
|
|
|
if (xType.equals(val("string"))) {
|
|
std::wstring x_str = x.as<std::wstring>();
|
|
return PyUnicode_FromWideChar(&*x_str.begin(), x_str.size());
|
|
} else if (xType.equals(val("number"))) {
|
|
double x_double = x.as<double>();
|
|
return PyFloat_FromDouble(x_double);
|
|
} else if (xType.equals(val("undefined"))) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
} else {
|
|
return JsProxy_cnew(x, parent, name);
|
|
}
|
|
}
|
|
|
|
static bool is_whitespace(char x) {
|
|
switch (x) {
|
|
case ' ':
|
|
case '\n':
|
|
case '\r':
|
|
case '\t':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
val runPython(std::wstring code) {
|
|
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
|
|
std::string code_utf8 = conv.to_bytes(code);
|
|
std::string::iterator last_line = code_utf8.end();
|
|
|
|
PyCompilerFlags cf;
|
|
cf.cf_flags = PyCF_SOURCE_IS_UTF8;
|
|
PyEval_MergeCompilerFlags(&cf);
|
|
|
|
// Find the last non-whitespace-only line since that will provide the result
|
|
// TODO: This way to find the last line will probably break in many ways
|
|
if (code_utf8.size() == 0) {
|
|
return val(*undefined);
|
|
}
|
|
last_line--;
|
|
for (; last_line != code_utf8.begin() && is_whitespace(*last_line); last_line--) {}
|
|
for (; last_line != code_utf8.begin() && *last_line != '\n'; last_line--) {}
|
|
|
|
int do_eval_line = 1;
|
|
_node *co;
|
|
co = PyParser_SimpleParseStringFlags(&*last_line, Py_eval_input, cf.cf_flags);
|
|
if (co == NULL) {
|
|
do_eval_line = 0;
|
|
PyErr_Clear();
|
|
}
|
|
PyNode_Free(co);
|
|
|
|
PyObject *ret;
|
|
if (do_eval_line == 0 || last_line != code_utf8.begin()) {
|
|
if (do_eval_line) {
|
|
*last_line = 0;
|
|
last_line++;
|
|
}
|
|
ret = PyRun_StringFlags(&*code_utf8.begin(), Py_file_input, globals, locals, &cf);
|
|
if (ret == NULL) {
|
|
return pythonExcToJS();
|
|
}
|
|
Py_DECREF(ret);
|
|
}
|
|
|
|
switch (do_eval_line) {
|
|
case 0:
|
|
Py_INCREF(Py_None);
|
|
ret = Py_None;
|
|
break;
|
|
case 1:
|
|
ret = PyRun_StringFlags(&*last_line, Py_eval_input, globals, locals, &cf);
|
|
break;
|
|
case 2:
|
|
ret = PyRun_StringFlags(&*last_line, Py_file_input, globals, locals, &cf);
|
|
break;
|
|
}
|
|
|
|
if (ret == NULL) {
|
|
return pythonExcToJS();
|
|
}
|
|
|
|
// Now copy all the variables over to the Javascript side
|
|
{
|
|
val js_globals = val::global("window");
|
|
PyObject *k, *v;
|
|
Py_ssize_t pos = 0;
|
|
|
|
while (PyDict_Next(globals, &pos, &k, &v)) {
|
|
if (!PyDict_Contains(original_globals, k)) {
|
|
js_globals.set(pythonToJs(k), pythonToJs(v));
|
|
}
|
|
}
|
|
}
|
|
|
|
val result = pythonToJs(ret);
|
|
Py_DECREF(ret);
|
|
return result;
|
|
}
|
|
|
|
EMSCRIPTEN_BINDINGS(python) {
|
|
emscripten::function("runPython", &runPython);
|
|
emscripten::class_<PyObject>("PyObject");
|
|
}
|
|
|
|
extern "C" {
|
|
int main(int argc, char** argv) {
|
|
setenv("PYTHONHOME", "/", 0);
|
|
|
|
Py_InitializeEx(0);
|
|
|
|
if (PyType_Ready(&JsProxyType) < 0)
|
|
return 1;
|
|
|
|
if (PyType_Ready(&LocalsProxyType) < 0)
|
|
return 1;
|
|
|
|
PyObject *m = PyImport_AddModule("__main__");
|
|
if (m == NULL)
|
|
return 1;
|
|
globals = PyModule_GetDict(m);
|
|
|
|
m = PyImport_AddModule("builtins");
|
|
PyDict_Update(globals, PyModule_GetDict(m));
|
|
|
|
original_globals = PyDict_Copy(globals);
|
|
|
|
locals = LocalsProxy_cnew(globals);
|
|
if (locals == NULL)
|
|
return 1;
|
|
|
|
undefined = new val(val::global("undefined"));
|
|
|
|
emscripten_exit_with_live_runtime();
|
|
return 0;
|
|
}
|
|
}
|