Skip to content

Commit 8f1ac3c

Browse files
Merge pull request #395 from Distributive-Network/philippe/385-fix
Introducing proxy for python bytes
2 parents 2e3274d + 88973b7 commit 8f1ac3c

File tree

6 files changed

+758
-54
lines changed

6 files changed

+758
-54
lines changed

include/PyBaseProxyHandler.hh

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public:
3030
bool isExtensible(JSContext *cx, JS::HandleObject proxy, bool *extensible) const override final;
3131
};
3232

33-
enum ProxySlots {PyObjectSlot};
33+
enum ProxySlots {PyObjectSlot, OtherSlot};
3434

3535
typedef struct {
3636
const char *name; /* The name of the method */

include/PyBytesProxyHandler.hh

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @file PyBytesProxyHandler.hh
3+
* @author Philippe Laporte ([email protected])
4+
* @brief Struct for creating JS Uint8Array-like proxy objects for immutable bytes objects
5+
* @date 2024-07-23
6+
*
7+
* @copyright Copyright (c) 2024 Distributive Corp.
8+
*
9+
*/
10+
11+
#ifndef PythonMonkey_PyBytesProxy_
12+
#define PythonMonkey_PyBytesProxy_
13+
14+
15+
#include "include/PyObjectProxyHandler.hh"
16+
17+
18+
/**
19+
* @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects
20+
*
21+
*/
22+
struct PyBytesProxyHandler : public PyObjectProxyHandler {
23+
public:
24+
PyBytesProxyHandler() : PyObjectProxyHandler(&family) {};
25+
static const char family;
26+
27+
/**
28+
* @brief [[Set]]
29+
*
30+
* @param cx pointer to JSContext
31+
* @param proxy The proxy object who's property we wish to set
32+
* @param id Key of the property we wish to set
33+
* @param v Value that we wish to set the property to
34+
* @param receiver The `this` value to use when executing any code
35+
* @param result whether or not the call succeeded
36+
* @return true call succeed
37+
* @return false call failed and an exception has been raised
38+
*/
39+
bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
40+
JS::HandleValue v, JS::HandleValue receiver,
41+
JS::ObjectOpResult &result) const override;
42+
43+
bool getOwnPropertyDescriptor(
44+
JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
45+
JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> desc
46+
) const override;
47+
48+
/**
49+
* @brief Handles python object reference count when JS Proxy object is finalized
50+
*
51+
* @param gcx pointer to JS::GCContext
52+
* @param proxy the proxy object being finalized
53+
*/
54+
void finalize(JS::GCContext *gcx, JSObject *proxy) const override;
55+
};
56+
57+
#endif

src/BufferType.cc

+78-42
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,56 @@
99
*/
1010

1111
#include "include/BufferType.hh"
12-
12+
#include "include/PyBytesProxyHandler.hh"
1313

1414
#include <jsapi.h>
1515
#include <js/ArrayBuffer.h>
1616
#include <js/experimental/TypedData.h>
1717
#include <js/ScalarType.h>
1818

19+
#include <limits.h>
20+
21+
// JS to Python
22+
23+
/* static */
24+
const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) {
25+
// floating point types
26+
switch (subtype) {
27+
case JS::Scalar::Float16:
28+
return "e";
29+
case JS::Scalar::Float32:
30+
return "f";
31+
case JS::Scalar::Float64:
32+
return "d";
33+
}
34+
35+
// integer types
36+
bool isSigned = JS::Scalar::isSignedIntType(subtype);
37+
uint8_t byteSize = JS::Scalar::byteSize(subtype);
38+
// Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures,
39+
// but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`)
40+
switch (byteSize) {
41+
case sizeof(char):
42+
return isSigned ? "b" : "B";
43+
case sizeof(short):
44+
return isSigned ? "h" : "H";
45+
case sizeof(int):
46+
return isSigned ? "i" : "I";
47+
// case sizeof(long): // compile error: duplicate case value
48+
// // And this is usually where the bit widths on 32/64-bit systems don't agree,
49+
// // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models
50+
// return isSigned ? "l" : "L";
51+
case sizeof(long long):
52+
return isSigned ? "q" : "Q";
53+
default: // invalid
54+
return "x"; // type code for pad bytes, no value
55+
}
56+
}
57+
58+
/* static */
59+
bool BufferType::isSupportedJsTypes(JSObject *obj) {
60+
return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj);
61+
}
1962

2063
PyObject *BufferType::getPyObject(JSContext *cx, JS::HandleObject bufObj) {
2164
PyObject *pyObject;
@@ -32,11 +75,6 @@ PyObject *BufferType::getPyObject(JSContext *cx, JS::HandleObject bufObj) {
3275
return pyObject;
3376
}
3477

35-
/* static */
36-
bool BufferType::isSupportedJsTypes(JSObject *obj) {
37-
return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj);
38-
}
39-
4078
/* static */
4179
PyObject *BufferType::fromJsTypedArray(JSContext *cx, JS::HandleObject typedArray) {
4280
JS::Scalar::Type subtype = JS_GetArrayBufferViewType(typedArray);
@@ -90,15 +128,29 @@ PyObject *BufferType::fromJsArrayBuffer(JSContext *cx, JS::HandleObject arrayBuf
90128
return PyMemoryView_FromBuffer(&bufInfo);
91129
}
92130

131+
132+
// Python to JS
133+
134+
static PyBytesProxyHandler pyBytesProxyHandler;
135+
136+
93137
JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
94138
Py_INCREF(pyObject);
95139

96140
// Get the pyObject's underlying buffer pointer and size
97141
Py_buffer *view = new Py_buffer{};
142+
bool immutable = false;
98143
if (PyObject_GetBuffer(pyObject, view, PyBUF_ND | PyBUF_WRITABLE /* C-contiguous and writable */ | PyBUF_FORMAT) < 0) {
99144
// the buffer is immutable (e.g., Python `bytes` type is read-only)
100-
return nullptr; // raises a PyExc_BufferError
145+
PyErr_Clear(); // a PyExc_BufferError was raised
146+
147+
if (PyObject_GetBuffer(pyObject, view, PyBUF_ND /* C-contiguous */ | PyBUF_FORMAT) < 0) {
148+
return nullptr; // a PyExc_BufferError was raised again
149+
}
150+
151+
immutable = true;
101152
}
153+
102154
if (view->ndim != 1) {
103155
PyErr_SetString(PyExc_BufferError, "multidimensional arrays are not allowed");
104156
BufferType::_releasePyBuffer(view);
@@ -108,7 +160,7 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
108160
// Determine the TypedArray's subtype (Uint8Array, Float64Array, ...)
109161
JS::Scalar::Type subtype = _getPyBufferType(view);
110162

111-
JSObject *arrayBuffer = nullptr;
163+
JSObject *arrayBuffer;
112164
if (view->len > 0) {
113165
// Create a new ExternalArrayBuffer object
114166
// Note: data will be copied instead of transferring the ownership when this external ArrayBuffer is "transferred" to a worker thread.
@@ -117,16 +169,29 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
117169
view->buf /* data pointer */,
118170
{BufferType::_releasePyBuffer, view /* the `bufView` argument to `_releasePyBuffer` */}
119171
);
172+
120173
arrayBuffer = JS::NewExternalArrayBuffer(cx,
121174
view->len /* byteLength */, std::move(dataPtr)
122175
);
123176
} else { // empty buffer
124177
arrayBuffer = JS::NewArrayBuffer(cx, 0);
125178
BufferType::_releasePyBuffer(view); // the buffer is no longer needed since we are creating a brand new empty ArrayBuffer
126179
}
127-
JS::RootedObject arrayBufferRooted(cx, arrayBuffer);
128180

129-
return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted);
181+
if (!immutable) {
182+
JS::RootedObject arrayBufferRooted(cx, arrayBuffer);
183+
return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted);
184+
} else {
185+
JS::RootedValue v(cx);
186+
JS::RootedObject uint8ArrayPrototype(cx);
187+
JS_GetClassPrototype(cx, JSProto_Uint8Array, &uint8ArrayPrototype); // so that instanceof will work, not that prototype methods will
188+
JSObject *proxy = js::NewProxyObject(cx, &pyBytesProxyHandler, v, uint8ArrayPrototype.get());
189+
JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(pyObject));
190+
JS::PersistentRootedObject *arrayBufferPointer = new JS::PersistentRootedObject(cx);
191+
arrayBufferPointer->set(arrayBuffer);
192+
JS::SetReservedSlot(proxy, OtherSlot, JS::PrivateValue(arrayBufferPointer));
193+
return proxy;
194+
}
130195
}
131196

132197
/* static */
@@ -155,8 +220,11 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) {
155220
return JS::Scalar::Float32;
156221
} else if (typeCode == 'd') {
157222
return JS::Scalar::Float64;
223+
} else if (typeCode == 'e') {
224+
return JS::Scalar::Float16;
158225
}
159226

227+
160228
// integer types
161229
// We can't rely on the type codes alone since the typecodes are mapped to C types and would have different sizes on different architectures
162230
// see https://docs.python.org/3.9/library/array.html#module-array
@@ -178,38 +246,6 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) {
178246
}
179247
}
180248

181-
/* static */
182-
const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) {
183-
// floating point types
184-
if (subtype == JS::Scalar::Float32) {
185-
return "f";
186-
} else if (subtype == JS::Scalar::Float64) {
187-
return "d";
188-
}
189-
190-
// integer types
191-
bool isSigned = JS::Scalar::isSignedIntType(subtype);
192-
uint8_t byteSize = JS::Scalar::byteSize(subtype);
193-
// Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures,
194-
// but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`)
195-
switch (byteSize) {
196-
case sizeof(char):
197-
return isSigned ? "b" : "B";
198-
case sizeof(short):
199-
return isSigned ? "h" : "H";
200-
case sizeof(int):
201-
return isSigned ? "i" : "I";
202-
// case sizeof(long): // compile error: duplicate case value
203-
// // And this is usually where the bit widths on 32/64-bit systems don't agree,
204-
// // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models
205-
// return isSigned ? "l" : "L";
206-
case sizeof(long long):
207-
return isSigned ? "q" : "Q";
208-
default: // invalid
209-
return "x"; // type code for pad bytes, no value
210-
}
211-
}
212-
213249
JSObject *BufferType::_newTypedArrayWithBuffer(JSContext *cx, JS::Scalar::Type subtype, JS::HandleObject arrayBuffer) {
214250
switch (subtype) {
215251
#define NEW_TYPED_ARRAY_WITH_BUFFER(ExternalType, NativeType, Name) \

0 commit comments

Comments
 (0)