Commit e2ab1ee9 authored by Michael Droettboom's avatar Michael Droettboom Committed by GitHub

Merge pull request #68 from iodide-project/typed-arrays

Convert typedarrays to/from buffers/memoryviews
parents 9c37f4df 737602b5
......@@ -26,15 +26,31 @@ Python. The values are copied and any connection to the original object is lost.
| `list`, `tuple` | `Array` |
| `dict` | `Object` |
Additionally, Python `bytes` and `buffer` objects are converted to/from Javascript
`Uint8ClampedArray` typed arrays. In this case, however, the underlying data is
not copied, and is shared between the Python and Javascript sides. This makes
passing raw memory between the languages (which in practice can be quite large)
very efficient.
Aside: This is the technology on which matplotlib images are passed to
Javascript to render in a canvas, and will be the basis of sharing Numpy arrays
with n-dimensional array data structures in Javascript.
## Typed arrays
Javascript typed arrays (Int8Array and friends) are converted to Python
`memoryviews`. This happens with a single binary memory copy (since Python can't
access arrays on the Javascript heap), and the data type is preserved. This
makes it easy to correctly convert it to a Numpy array using `numpy.asarray`:
```javascript
array = Float32Array([1, 2, 3])
```
```python
from js import array
import numpy as np
numpy_array = np.asarray(array)
```
Python `bytes` and `buffer` objects are converted to Javascript as
`Uint8ClampedArray`s, without any memory copy at all, and is thus very
efficient, but be aware that any changes to the buffer will be reflected in both
places.
Numpy arrays are currently converted to Javascript as nested (regular) Arrays. A
more efficient method will probably emerge as we decide on an ndarray
implementation for Javascript.
## Class instances
......
......@@ -129,9 +129,9 @@ EM_JS(void, hiwire_set_member_obj, (int idobj, int ididx, int idval), {
});
EM_JS(void, hiwire_delete_member_obj, (int idobj, int ididx), {
var jsobj = Module.hiwire_get_value(idobj);
var jsidx = Module.hiwire_get_value(ididx);
delete jsobj[jsidx];
var jsobj = Module.hiwire_get_value(idobj);
var jsidx = Module.hiwire_get_value(ididx);
delete jsobj[jsidx];
});
EM_JS(void, hiwire_call, (int idfunc, int idargs), {
......@@ -176,9 +176,10 @@ EM_JS(int, hiwire_typeof, (int idobj), {
return Module.hiwire_new_value(typeof Module.hiwire_get_value(idobj));
});
#define MAKE_OPERATOR(name, op) \
EM_JS(int, hiwire_##name, (int ida, int idb), { \
return (Module.hiwire_get_value(ida) op Module.hiwire_get_value(idb)) ? 1 : 0; \
#define MAKE_OPERATOR(name, op) \
EM_JS(int, hiwire_##name, (int ida, int idb), { \
return (Module.hiwire_get_value(ida) op Module.hiwire_get_value(idb)) ? 1 \
: 0; \
});
MAKE_OPERATOR(less_than, <);
......@@ -190,7 +191,9 @@ MAKE_OPERATOR(greater_than_equal, >=);
EM_JS(int, hiwire_next, (int idobj), {
var jsobj = Module.hiwire_get_value(idobj);
// clang-format off
if (jsobj.next === undefined) {
// clang-format on
return -1;
}
......@@ -201,3 +204,57 @@ EM_JS(int, hiwire_nonzero, (int idobj), {
var jsobj = Module.hiwire_get_value(idobj);
return (jsobj != 0) ? 1 : 0;
});
EM_JS(int, hiwire_is_typedarray, (int idobj), {
var jsobj = Module.hiwire_get_value(idobj);
// clang-format off
return (jsobj['byteLength'] !== undefined) ? 1 : 0;
// clang-format on
});
EM_JS(int, hiwire_get_byteLength, (int idobj), {
var jsobj = Module.hiwire_get_value(idobj);
return jsobj['byteLength'];
});
EM_JS(int, hiwire_copy_to_ptr, (int idobj, int ptr), {
var jsobj = Module.hiwire_get_value(idobj);
Module.HEAPU8.set(new Uint8Array(jsobj.buffer), ptr);
});
EM_JS(int, hiwire_get_dtype, (int idobj), {
var jsobj = Module.hiwire_get_value(idobj);
switch (jsobj.constructor.name) {
case 'Int8Array':
dtype = 1; // INT8_TYPE;
break;
case 'Uint8Array':
dtype = 2; // UINT8_TYPE;
break;
case 'Uint8ClampedArray':
dtype = 3; // UINT8CLAMPED_TYPE;
break;
case 'Int16Array':
dtype = 4; // INT16_TYPE;
break;
case 'Uint16Array':
dtype = 5; // UINT16_TYPE;
break;
case 'Int32Array':
dtype = 6; // INT32_TYPE;
break;
case 'Uint32Array':
dtype = 7; // UINT32_TYPE;
break;
case 'Float32Array':
dtype = 8; // FLOAT32_TYPE;
break;
case 'Float64Array':
dtype = 9; // FLOAT64_TYPE;
break;
default:
dtype = 3; // UINT8CLAMPED_TYPE;
break;
}
return dtype;
});
......@@ -340,6 +340,48 @@ hiwire_next(int idobj);
* Returns 1 if the value is non-zero.
*
*/
int hiwire_nonzero(int idobj);
int
hiwire_nonzero(int idobj);
/**
* Returns 1 if the value is a typedarray.
*/
int
hiwire_is_typedarray(int idobj);
/**
* Returns the value of obj.byteLength.
*
* There is no error checking. Caller must ensure that hiwire_is_typedarray is
* true.
*/
int
hiwire_get_byteLength(int idobj);
/**
* Copies the buffer contents of a given typed array or buffer into the memory
* at ptr.
*/
int
hiwire_copy_to_ptr(int idobj, int ptr);
#define INT8_TYPE 1
#define UINT8_TYPE 2
#define UINT8CLAMPED_TYPE 3
#define INT16_TYPE 4
#define UINT16_TYPE 5
#define INT32_TYPE 6
#define UINT32_TYPE 7
#define FLOAT32_TYPE 8
#define FLOAT64_TYPE 9
/**
* Get a data type identifier for a given typedarray.
*
* It will be one of INT8_TYPE, UINT8_TYPE, UINT8CLAMPED_TYPE, INT16_TYPE,
* UINT16_TYPE, INT32_TYPE, UINT32_TYPE, FLOAT32_TYPE, FLOAT64_TYPE.
*/
int
hiwire_get_dtype(int idobj);
#endif /* HIWIRE_H */
......@@ -49,15 +49,10 @@ _js2python_pyproxy(PyObject* val)
}
int
_js2python_init_bytes(int length)
_js2python_memoryview(int id)
{
return (int)PyBytes_FromStringAndSize(NULL, length);
}
int
_js2python_get_bytes_ptr(PyObject* val)
{
return (int)PyBytes_AsString(val);
PyObject* jsproxy = JsProxy_cnew(id);
return (int)PyMemoryView_FromObject(jsproxy);
}
int
......@@ -88,10 +83,7 @@ EM_JS(int, __js2python, (int id), {
} else if (Module.PyProxy.isPyProxy(value)) {
return __js2python_pyproxy(Module.PyProxy.getPtr(value));
} else if (value['byteLength'] !== undefined) {
var result = __js2python_init_bytes(value['byteLength']);
var ptr = __js2python_get_bytes_ptr(result);
Module.HEAPU8.set(new Uint8Array(value.buffer), ptr);
return result;
return __js2python_memoryview(id);
} else {
return __js2python_jsproxy(id);
}
......
......@@ -16,12 +16,14 @@ JsBoundMethod_cnew(int this_, const char* name);
typedef struct
{
PyObject_HEAD int js;
PyObject* bytes;
} JsProxy;
static void
JsProxy_dealloc(JsProxy* self)
{
hiwire_decref(self->js);
Py_XDECREF(self->bytes);
Py_TYPE(self)->tp_free((PyObject*)self);
}
......@@ -116,17 +118,18 @@ JsProxy_Call(PyObject* o, PyObject* args, PyObject* kwargs)
}
static PyObject*
JsProxy_RichCompare(PyObject *a, PyObject *b, int op) {
JsProxy_RichCompare(PyObject* a, PyObject* b, int op)
{
JsProxy* aproxy = (JsProxy*)a;
if (!JsProxy_Check(b)) {
switch (op) {
case Py_EQ:
Py_RETURN_FALSE;
case Py_NE:
Py_RETURN_TRUE;
default:
return Py_NotImplemented;
case Py_EQ:
Py_RETURN_FALSE;
case Py_NE:
Py_RETURN_TRUE;
default:
return Py_NotImplemented;
}
}
......@@ -134,24 +137,24 @@ JsProxy_RichCompare(PyObject *a, PyObject *b, int op) {
int ida = python2js(a);
int idb = python2js(b);
switch (op) {
case Py_LT:
result = hiwire_less_than(ida, idb);
break;
case Py_LE:
result = hiwire_less_than_equal(ida, idb);
break;
case Py_EQ:
result = hiwire_equal(ida, idb);
break;
case Py_NE:
result = hiwire_not_equal(ida, idb);
break;
case Py_GT:
result = hiwire_greater_than(ida, idb);
break;
case Py_GE:
result = hiwire_greater_than_equal(ida, idb);
break;
case Py_LT:
result = hiwire_less_than(ida, idb);
break;
case Py_LE:
result = hiwire_less_than_equal(ida, idb);
break;
case Py_EQ:
result = hiwire_equal(ida, idb);
break;
case Py_NE:
result = hiwire_not_equal(ida, idb);
break;
case Py_GT:
result = hiwire_greater_than(ida, idb);
break;
case Py_GE:
result = hiwire_greater_than_equal(ida, idb);
break;
}
hiwire_decref(ida);
......@@ -164,14 +167,14 @@ JsProxy_RichCompare(PyObject *a, PyObject *b, int op) {
}
static PyObject*
JsProxy_GetIter(PyObject *o)
JsProxy_GetIter(PyObject* o)
{
Py_INCREF(o);
return o;
}
static PyObject*
JsProxy_IterNext(PyObject *o)
JsProxy_IterNext(PyObject* o)
{
JsProxy* self = (JsProxy*)o;
......@@ -215,7 +218,7 @@ JsProxy_New(PyObject* o, PyObject* args, PyObject* kwargs)
return pyresult;
}
Py_ssize_t
static Py_ssize_t
JsProxy_length(PyObject* o)
{
JsProxy* self = (JsProxy*)o;
......@@ -223,7 +226,7 @@ JsProxy_length(PyObject* o)
return hiwire_get_length(self->js);
}
PyObject*
static PyObject*
JsProxy_subscript(PyObject* o, PyObject* pyidx)
{
JsProxy* self = (JsProxy*)o;
......@@ -236,7 +239,7 @@ JsProxy_subscript(PyObject* o, PyObject* pyidx)
return pyresult;
}
int
static int
JsProxy_ass_subscript(PyObject* o, PyObject* pyidx, PyObject* pyvalue)
{
JsProxy* self = (JsProxy*)o;
......@@ -252,19 +255,112 @@ JsProxy_ass_subscript(PyObject* o, PyObject* pyidx, PyObject* pyvalue)
return 0;
}
static int
JsProxy_GetBuffer(PyObject* o, Py_buffer* view, int flags)
{
JsProxy* self = (JsProxy*)o;
if (!hiwire_is_typedarray(self->js)) {
PyErr_SetString(PyExc_BufferError, "Can not use as buffer");
view->obj = NULL;
return -1;
}
Py_ssize_t byteLength = hiwire_get_byteLength(self->js);
if (self->bytes == NULL) {
self->bytes = PyBytes_FromStringAndSize(NULL, byteLength);
if (self->bytes == NULL) {
return -1;
}
}
void* ptr = PyBytes_AsString(self->bytes);
hiwire_copy_to_ptr(self->js, (int)ptr);
int dtype = hiwire_get_dtype(self->js);
char* format;
Py_ssize_t itemsize;
switch (dtype) {
case INT8_TYPE:
format = "b";
itemsize = 1;
break;
case UINT8_TYPE:
format = "B";
itemsize = 1;
break;
case UINT8CLAMPED_TYPE:
format = "B";
itemsize = 1;
break;
case INT16_TYPE:
format = "h";
itemsize = 2;
break;
case UINT16_TYPE:
format = "H";
itemsize = 2;
break;
case INT32_TYPE:
format = "i";
itemsize = 4;
break;
case UINT32_TYPE:
format = "I";
itemsize = 4;
break;
case FLOAT32_TYPE:
format = "f";
itemsize = 4;
break;
case FLOAT64_TYPE:
format = "d";
itemsize = 8;
break;
default:
format = "B";
itemsize = 1;
break;
}
Py_INCREF(self);
view->buf = ptr;
view->obj = (PyObject*)self;
view->len = byteLength;
view->readonly = 0;
view->itemsize = itemsize;
view->format = format;
view->ndim = 1;
view->shape = NULL;
view->strides = NULL;
view->suboffsets = NULL;
return 0;
}
// clang-format off
static PyMappingMethods JsProxy_MappingMethods = {
JsProxy_length,
JsProxy_subscript,
JsProxy_ass_subscript,
};
// clang-format on
static PyMethodDef JsProxy_Methods[] = { { "new",
(PyCFunction)JsProxy_New,
METH_VARARGS | METH_KEYWORDS,
"Construct a new instance" },
{ NULL } };
static PyBufferProcs JsProxy_BufferProcs = {
JsProxy_GetBuffer,
NULL
};
static PyMethodDef JsProxy_Methods[] = {
{ "new",
(PyCFunction)JsProxy_New,
METH_VARARGS | METH_KEYWORDS,
"Construct a new instance" },
{ NULL }
};
// clang-format on
static PyTypeObject JsProxyType = {
.tp_name = "JsProxy",
......@@ -280,7 +376,8 @@ static PyTypeObject JsProxyType = {
.tp_as_mapping = &JsProxy_MappingMethods,
.tp_iter = JsProxy_GetIter,
.tp_iternext = JsProxy_IterNext,
.tp_repr = JsProxy_Repr
.tp_repr = JsProxy_Repr,
.tp_as_buffer = &JsProxy_BufferProcs
};
PyObject*
......@@ -289,6 +386,7 @@ JsProxy_cnew(int idobj)
JsProxy* self;
self = (JsProxy*)JsProxyType.tp_alloc(&JsProxyType, 0);
self->js = hiwire_incref(idobj);
self->bytes = NULL;
return (PyObject*)self;
}
......
......@@ -6,3 +6,26 @@ def test_numpy(selenium):
assert all(len(y) == 64 for y in x)
for y in x:
assert all(z == 0 for z in y)
def test_typed_arrays(selenium):
selenium.load_package("numpy")
selenium.run("import numpy")
for (jstype, npytype) in (
('Int8Array', 'int8'),
('Uint8Array', 'uint8'),
('Uint8ClampedArray', 'uint8'),
('Int16Array', 'int16'),
('Uint16Array', 'uint16'),
('Int32Array', 'int32'),
('Uint32Array', 'uint32'),
('Float32Array', 'float32'),
('Float64Array', 'float64')):
print(jstype, npytype)
selenium.run_js(
f'window.array = new {jstype}([1, 2, 3, 4]);\n')
assert selenium.run(
'from js import array\n'
'npyarray = numpy.asarray(array)\n'
f'npyarray.dtype.name == "{npytype}" '
'and npyarray == [1, 2, 3, 4]')
......@@ -93,18 +93,43 @@ def test_js2python(selenium):
'jspython is open')
assert selenium.run(
'from js import jsbytes\n'
'jsbytes == b"\x01\x02\x03"')
'(jsbytes.tolist() == [1, 2, 3]) '
'and (jsbytes.tobytes() == b"\x01\x02\x03")')
assert selenium.run(
'from js import jsfloats\n'
'print(jsfloats)\n'
'import struct\n'
'expected = struct.pack("fff", 1, 2, 3)\n'
'jsfloats == expected')
'(jsfloats.tolist() == [1, 2, 3]) '
'and (jsfloats.tobytes() == expected)')
assert selenium.run(
'from js import jsobject\n'
'str(jsobject) == "[object XMLHttpRequest]"')
def test_typed_arrays(selenium):
for (jstype, pytype) in (
('Int8Array', 'b'),
('Uint8Array', 'B'),
('Uint8ClampedArray', 'B'),
('Int16Array', 'h'),
('Uint16Array', 'H'),
('Int32Array', 'i'),
('Uint32Array', 'I'),
('Float32Array', 'f'),
('Float64Array', 'd')):
print(jstype, pytype)
selenium.run_js(
f'window.array = new {jstype}([1, 2, 3, 4]);\n')
assert selenium.run(
'from js import array\n'
'import struct\n'
f'expected = struct.pack("{pytype*4}", 1, 2, 3, 4)\n'
'print(array.format, array.tolist(), array.tobytes())\n'
f'array.format == "{pytype}" '
'and array.tolist() == [1, 2, 3, 4] '
'and array.tobytes() == expected')
def test_import_js(selenium):
result = selenium.run(
"from js import window\nwindow.title = 'Foo'\nwindow.title")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment