Skip to content

Commit

Permalink
Merge pull request #395 from Distributive-Network/philippe/385-fix
Browse files Browse the repository at this point in the history
Introducing proxy for python bytes
  • Loading branch information
philippedistributive authored Jul 30, 2024
2 parents 2e3274d + 88973b7 commit 8f1ac3c
Show file tree
Hide file tree
Showing 6 changed files with 758 additions and 54 deletions.
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
57 changes: 57 additions & 0 deletions include/PyBytesProxyHandler.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @file PyBytesProxyHandler.hh
* @author Philippe Laporte ([email protected])
* @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;

/**
* @brief [[Set]]
*
* @param cx pointer to JSContext
* @param proxy The proxy object who's property we wish to set
* @param id Key of the property we wish to set
* @param v Value that we wish to set the property to
* @param receiver The `this` value to use when executing any code
* @param result whether or not the call succeeded
* @return true call succeed
* @return false call failed and an exception has been raised
*/
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;

/**
* @brief Handles python object reference count when JS Proxy object is finalized
*
* @param gcx pointer to JS::GCContext
* @param proxy the proxy object being finalized
*/
void finalize(JS::GCContext *gcx, JSObject *proxy) const override;
};

#endif
120 changes: 78 additions & 42 deletions src/BufferType.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,56 @@
*/

#include "include/BufferType.hh"

#include "include/PyBytesProxyHandler.hh"

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

#include <limits.h>

// JS to Python

/* static */
const char *BufferType::_toPyBufferFormatCode(JS::Scalar::Type subtype) {
// floating point types
switch (subtype) {
case JS::Scalar::Float16:
return "e";
case JS::Scalar::Float32:
return "f";
case 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;
Expand All @@ -32,11 +75,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 +128,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 +160,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 +169,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 @@ -155,8 +220,11 @@ JS::Scalar::Type BufferType::_getPyBufferType(Py_buffer *bufView) {
return JS::Scalar::Float32;
} else if (typeCode == 'd') {
return JS::Scalar::Float64;
} else if (typeCode == 'e') {
return JS::Scalar::Float16;
}


// integer types
// 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
// see https://docs.python.org/3.9/library/array.html#module-array
Expand All @@ -178,38 +246,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

0 comments on commit 8f1ac3c

Please sign in to comment.