Skip to content

Commit a5134f1

Browse files
Introducing proxy for python bytes
1 parent 2e3274d commit a5134f1

File tree

6 files changed

+394
-56
lines changed

6 files changed

+394
-56
lines changed

include/PyBaseProxyHandler.hh

Lines changed: 1 addition & 1 deletion
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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @file PyBytesProxyHandler.hh
3+
* @author Philippe Laporte ([email protected])
4+
* @brief Struct for creating JS 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+
bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
28+
JS::HandleValue v, JS::HandleValue receiver,
29+
JS::ObjectOpResult &result) const override;
30+
31+
bool getOwnPropertyDescriptor(
32+
JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
33+
JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> desc
34+
) const override;
35+
36+
void finalize(JS::GCContext *gcx, JSObject *proxy) const override;
37+
38+
/**
39+
* @brief An array of method definitions for bytes prototype methods
40+
*
41+
*/
42+
static JSMethodDef bytes_methods[];
43+
};
44+
45+
#endif

src/BufferType.cc

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,53 @@
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

1919

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

35-
/* static */
36-
bool BufferType::isSupportedJsTypes(JSObject *obj) {
37-
return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj);
38-
}
39-
4074
/* static */
4175
PyObject *BufferType::fromJsTypedArray(JSContext *cx, JS::HandleObject typedArray) {
4276
JS::Scalar::Type subtype = JS_GetArrayBufferViewType(typedArray);
@@ -90,16 +124,31 @@ PyObject *BufferType::fromJsArrayBuffer(JSContext *cx, JS::HandleObject arrayBuf
90124
return PyMemoryView_FromBuffer(&bufInfo);
91125
}
92126

127+
128+
// Python to JS
129+
130+
static PyBytesProxyHandler pyBytesProxyHandler;
131+
132+
93133
JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
94-
Py_INCREF(pyObject);
134+
Py_INCREF(pyObject); // TODO remove
95135

96136
// Get the pyObject's underlying buffer pointer and size
97137
Py_buffer *view = new Py_buffer{};
138+
bool immutable = false;
98139
if (PyObject_GetBuffer(pyObject, view, PyBUF_ND | PyBUF_WRITABLE /* C-contiguous and writable */ | PyBUF_FORMAT) < 0) {
140+
99141
// the buffer is immutable (e.g., Python `bytes` type is read-only)
100-
return nullptr; // raises a PyExc_BufferError
142+
PyErr_Clear(); // a PyExc_BufferError was raised
143+
144+
if (PyObject_GetBuffer(pyObject, view, PyBUF_ND /* C-contiguous and writable */ | PyBUF_FORMAT) < 0) {
145+
return nullptr; // and a PyExc_BufferError was raised again
146+
}
147+
148+
immutable = true;
101149
}
102-
if (view->ndim != 1) {
150+
151+
if (view->ndim != 1 && !immutable) {
103152
PyErr_SetString(PyExc_BufferError, "multidimensional arrays are not allowed");
104153
BufferType::_releasePyBuffer(view);
105154
return nullptr;
@@ -108,7 +157,7 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
108157
// Determine the TypedArray's subtype (Uint8Array, Float64Array, ...)
109158
JS::Scalar::Type subtype = _getPyBufferType(view);
110159

111-
JSObject *arrayBuffer = nullptr;
160+
JSObject *arrayBuffer;
112161
if (view->len > 0) {
113162
// Create a new ExternalArrayBuffer object
114163
// Note: data will be copied instead of transferring the ownership when this external ArrayBuffer is "transferred" to a worker thread.
@@ -117,16 +166,29 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
117166
view->buf /* data pointer */,
118167
{BufferType::_releasePyBuffer, view /* the `bufView` argument to `_releasePyBuffer` */}
119168
);
169+
120170
arrayBuffer = JS::NewExternalArrayBuffer(cx,
121171
view->len /* byteLength */, std::move(dataPtr)
122172
);
123173
} else { // empty buffer
124174
arrayBuffer = JS::NewArrayBuffer(cx, 0);
125175
BufferType::_releasePyBuffer(view); // the buffer is no longer needed since we are creating a brand new empty ArrayBuffer
126176
}
127-
JS::RootedObject arrayBufferRooted(cx, arrayBuffer);
128177

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

132194
/* static */
@@ -178,38 +240,6 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) {
178240
}
179241
}
180242

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-
213243
JSObject *BufferType::_newTypedArrayWithBuffer(JSContext *cx, JS::Scalar::Type subtype, JS::HandleObject arrayBuffer) {
214244
switch (subtype) {
215245
#define NEW_TYPED_ARRAY_WITH_BUFFER(ExternalType, NativeType, Name) \

0 commit comments

Comments
 (0)