Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing proxy for python bytes #395

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/PyBaseProxyHandler.hh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public:
bool isExtensible(JSContext *cx, JS::HandleObject proxy, bool *extensible) const override final;
};

enum ProxySlots {PyObjectSlot};
enum ProxySlots {PyObjectSlot, OtherSlot};

typedef struct {
const char *name; /* The name of the method */
Expand Down
39 changes: 39 additions & 0 deletions include/PyBytesProxyHandler.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @file PyBytesProxyHandler.hh
* @author Philippe Laporte (philippe@distributive.network)
* @brief Struct for creating JS Uint8Array-like proxy objects for immutable bytes objects
* @date 2024-07-23
*
* @copyright Copyright (c) 2024 Distributive Corp.
*
*/

#ifndef PythonMonkey_PyBytesProxy_
#define PythonMonkey_PyBytesProxy_


#include "include/PyObjectProxyHandler.hh"


/**
* @brief This struct is the ProxyHandler for JS Proxy Iterable pythonmonkey creates to handle coercion from python iterables to JS Objects
*
*/
struct PyBytesProxyHandler : public PyObjectProxyHandler {
public:
PyBytesProxyHandler() : PyObjectProxyHandler(&family) {};
static const char family;

bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
JS::HandleValue v, JS::HandleValue receiver,
JS::ObjectOpResult &result) const override;

bool getOwnPropertyDescriptor(
JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> desc
) const override;

void finalize(JS::GCContext *gcx, JSObject *proxy) const override;
};

#endif
113 changes: 71 additions & 42 deletions src/BufferType.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,53 @@
*/

#include "include/BufferType.hh"

#include "include/PyBytesProxyHandler.hh"

#include <jsapi.h>
#include <js/ArrayBuffer.h>
#include <js/experimental/TypedData.h>
#include <js/ScalarType.h>


// JS to Python

/* static */
const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) {
// floating point types
if (subtype == JS::Scalar::Float32) {
return "f";
} else if (subtype == JS::Scalar::Float64) {
return "d";
}

// integer types
bool isSigned = JS::Scalar::isSignedIntType(subtype);
uint8_t byteSize = JS::Scalar::byteSize(subtype);
// Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures,
// but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`)
switch (byteSize) {
case sizeof(char):
return isSigned ? "b" : "B";
case sizeof(short):
return isSigned ? "h" : "H";
case sizeof(int):
return isSigned ? "i" : "I";
// case sizeof(long): // compile error: duplicate case value
// // And this is usually where the bit widths on 32/64-bit systems don't agree,
// // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models
// return isSigned ? "l" : "L";
case sizeof(long long):
return isSigned ? "q" : "Q";
default: // invalid
return "x"; // type code for pad bytes, no value
}
}

/* static */
bool BufferType::isSupportedJsTypes(JSObject *obj) {
return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj);
}

PyObject *BufferType::getPyObject(JSContext *cx, JS::HandleObject bufObj) {
PyObject *pyObject;
if (JS_IsTypedArrayObject(bufObj)) {
Expand All @@ -32,11 +71,6 @@ PyObject *BufferType::getPyObject(JSContext *cx, JS::HandleObject bufObj) {
return pyObject;
}

/* static */
bool BufferType::isSupportedJsTypes(JSObject *obj) {
return JS::IsArrayBufferObject(obj) || JS_IsTypedArrayObject(obj);
}

/* static */
PyObject *BufferType::fromJsTypedArray(JSContext *cx, JS::HandleObject typedArray) {
JS::Scalar::Type subtype = JS_GetArrayBufferViewType(typedArray);
Expand Down Expand Up @@ -90,15 +124,29 @@ PyObject *BufferType::fromJsArrayBuffer(JSContext *cx, JS::HandleObject arrayBuf
return PyMemoryView_FromBuffer(&bufInfo);
}


// Python to JS

static PyBytesProxyHandler pyBytesProxyHandler;


JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
Py_INCREF(pyObject);

// Get the pyObject's underlying buffer pointer and size
Py_buffer *view = new Py_buffer{};
bool immutable = false;
if (PyObject_GetBuffer(pyObject, view, PyBUF_ND | PyBUF_WRITABLE /* C-contiguous and writable */ | PyBUF_FORMAT) < 0) {
// the buffer is immutable (e.g., Python `bytes` type is read-only)
return nullptr; // raises a PyExc_BufferError
PyErr_Clear(); // a PyExc_BufferError was raised

if (PyObject_GetBuffer(pyObject, view, PyBUF_ND /* C-contiguous */ | PyBUF_FORMAT) < 0) {
return nullptr; // a PyExc_BufferError was raised again
}

immutable = true;
}

if (view->ndim != 1) {
PyErr_SetString(PyExc_BufferError, "multidimensional arrays are not allowed");
BufferType::_releasePyBuffer(view);
Expand All @@ -108,7 +156,7 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
// Determine the TypedArray's subtype (Uint8Array, Float64Array, ...)
JS::Scalar::Type subtype = _getPyBufferType(view);

JSObject *arrayBuffer = nullptr;
JSObject *arrayBuffer;
if (view->len > 0) {
// Create a new ExternalArrayBuffer object
// Note: data will be copied instead of transferring the ownership when this external ArrayBuffer is "transferred" to a worker thread.
Expand All @@ -117,16 +165,29 @@ JSObject *BufferType::toJsTypedArray(JSContext *cx, PyObject *pyObject) {
view->buf /* data pointer */,
{BufferType::_releasePyBuffer, view /* the `bufView` argument to `_releasePyBuffer` */}
);

arrayBuffer = JS::NewExternalArrayBuffer(cx,
view->len /* byteLength */, std::move(dataPtr)
);
} else { // empty buffer
arrayBuffer = JS::NewArrayBuffer(cx, 0);
BufferType::_releasePyBuffer(view); // the buffer is no longer needed since we are creating a brand new empty ArrayBuffer
}
JS::RootedObject arrayBufferRooted(cx, arrayBuffer);

return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted);
if (!immutable) {
JS::RootedObject arrayBufferRooted(cx, arrayBuffer);
return _newTypedArrayWithBuffer(cx, subtype, arrayBufferRooted);
} else {
JS::RootedValue v(cx);
JS::RootedObject uint8ArrayPrototype(cx);
JS_GetClassPrototype(cx, JSProto_Uint8Array, &uint8ArrayPrototype); // so that instanceof will work, not that prototype methods will
JSObject *proxy = js::NewProxyObject(cx, &pyBytesProxyHandler, v, uint8ArrayPrototype.get());
JS::SetReservedSlot(proxy, PyObjectSlot, JS::PrivateValue(pyObject));
JS::PersistentRootedObject *arrayBufferPointer = new JS::PersistentRootedObject(cx);
arrayBufferPointer->set(arrayBuffer);
JS::SetReservedSlot(proxy, OtherSlot, JS::PrivateValue(arrayBufferPointer));
return proxy;
}
}

/* static */
Expand Down Expand Up @@ -178,38 +239,6 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) {
}
}

/* static */
const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) {
// floating point types
if (subtype == JS::Scalar::Float32) {
return "f";
} else if (subtype == JS::Scalar::Float64) {
return "d";
}

// integer types
bool isSigned = JS::Scalar::isSignedIntType(subtype);
uint8_t byteSize = JS::Scalar::byteSize(subtype);
// Python `array` type codes are strictly mapped to basic C types (e.g., `int`), widths may vary on different architectures,
// but JS TypedArray uses fixed-width integer types (e.g., `uint32_t`)
switch (byteSize) {
case sizeof(char):
return isSigned ? "b" : "B";
case sizeof(short):
return isSigned ? "h" : "H";
case sizeof(int):
return isSigned ? "i" : "I";
// case sizeof(long): // compile error: duplicate case value
// // And this is usually where the bit widths on 32/64-bit systems don't agree,
// // see https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models
// return isSigned ? "l" : "L";
case sizeof(long long):
return isSigned ? "q" : "Q";
default: // invalid
return "x"; // type code for pad bytes, no value
}
}

JSObject *BufferType::_newTypedArrayWithBuffer(JSContext *cx, JS::Scalar::Type subtype, JS::HandleObject arrayBuffer) {
switch (subtype) {
#define NEW_TYPED_ARRAY_WITH_BUFFER(ExternalType, NativeType, Name) \
Expand Down
Loading
Loading