mirror of
https://github.com/zhigang1992/expo.git
synced 2026-06-14 01:22:53 +08:00
433 lines
15 KiB
C
433 lines
15 KiB
C
// Adapted from phoboslab/Ejecta on GitHub
|
|
// https://github.com/phoboslab/Ejecta/tree/8917cfbc17b3298f99ca7d719cfc57fcb8e43519/Source/Ejecta
|
|
// with Android compatibility changes and changes to allow coexisting with
|
|
// iOS >=10 built-in TypedArray API
|
|
|
|
#include "EXJSConvertTypedArray.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
|
|
#if defined __ARM_NEON__
|
|
#include <arm_neon.h>
|
|
#endif
|
|
|
|
#include <JavaScriptCore/JSValueRef.h>
|
|
#include <JavaScriptCore/JSObjectRef.h>
|
|
#include <JavaScriptCore/JSStringRef.h>
|
|
#include <JavaScriptCore/JSContextRef.h>
|
|
|
|
// These functions deal with getting and setting a JavaScript Typed Array's data.
|
|
// The process in which this is done is extremely backwards, prohibitively
|
|
// memory inefficient and painfully slow.
|
|
|
|
// JavaScriptCore still doesn't have an API to deal with Typed Arrays directly
|
|
// and nobody seems to care. See this WebKit bug for the details:
|
|
// https://bugs.webkit.org/show_bug.cgi?id=120112
|
|
|
|
// The naive method of getting the array's data would be to read the "length"
|
|
// property and call JSObjectGetPropertyAtIndex() for each element. This function
|
|
// returns a JSValue and since the array's data is just a raw memory pointer
|
|
// internally, this JSValue has be constructed first by JSC. Afterwards this
|
|
// JSValue has to be converted back into an int and stored in a native buffer.
|
|
// This ordeal takes about 1200ms per megabyte on an iPhone5S.
|
|
|
|
// So, instead of extracting each byte on its own, we create Int32 view on the
|
|
// Array, so that we can extract 4 bytes at a time. Now, the fastest method to
|
|
// get to the values is to call a function with .apply(null, theInt32Array).
|
|
// This causes the typed array to be converted into plain JSValues - we can then
|
|
// iterate all function arguments and convert each JSValue into a Int32.
|
|
|
|
// There's one big caveat, though: a function can only be called with 64k
|
|
// arguments at once, or it will blow the stack. So we have to divide the Int32
|
|
// Array into smaller subarrays and convert the data in chunks.
|
|
|
|
// Setting the Array's data works about the same. We can create a plain JS
|
|
// Array in reasonable time by treating the data as 32bit ints. This plain
|
|
// Array can then be used to call .set() on an Int32 View of the Typed Array.
|
|
|
|
// To be able to quickly determine the type of a typed array, we install an
|
|
// enum with the type id ("__ejTypedArrayType") onto each array constructor's
|
|
// prototype.
|
|
|
|
// This all brings the time down to about 7ms per megabyte for reading and about
|
|
// 20ms per megabyte for writing. Even though this 7ms/MB number may not sound
|
|
// all too bad, keep in mind that it's still 7ms slower than what we would have
|
|
// with a better API.
|
|
|
|
|
|
|
|
|
|
|
|
// Array Types to constructor names and element size. We need to obtain the
|
|
// Constructors from the JSGlobalObject when creating Typed Arrays and when
|
|
// attaching our methods to them.
|
|
|
|
const static struct {
|
|
const int elementSize;
|
|
const char * constructorName;
|
|
} TypeInfo[] = {
|
|
[kJSTypedArrayTypeNone] = {0, NULL},
|
|
[kJSTypedArrayTypeInt8Array] = {1, "Int8Array"},
|
|
[kJSTypedArrayTypeInt16Array] = {2, "Int16Array"},
|
|
[kJSTypedArrayTypeInt32Array] = {4, "Int32Array"},
|
|
[kJSTypedArrayTypeUint8Array] = {1, "Uint8Array"},
|
|
[kJSTypedArrayTypeUint8ClampedArray] = {1, "Uint8ClampedArray"},
|
|
[kJSTypedArrayTypeUint16Array] = {2, "Uint16Array"},
|
|
[kJSTypedArrayTypeUint32Array] = {4, "Uint32Array"},
|
|
[kJSTypedArrayTypeFloat32Array] = {4, "Float32Array"},
|
|
[kJSTypedArrayTypeFloat64Array] = {8, "Float64Array"},
|
|
[kJSTypedArrayTypeArrayBuffer] = {1, "ArrayBuffer"}
|
|
};
|
|
|
|
|
|
// Data from Typed Arrays smaller than CopyInChunksThreshold will be extracted
|
|
// one byte at a time.
|
|
|
|
const static int CopyInChunksThreshold = 32;
|
|
|
|
|
|
// For large arrays, copy the data in Chunks of 16k elements, so that we don't
|
|
// blow the stack.
|
|
|
|
const static int CopyChunkSize = 0x4000;
|
|
|
|
|
|
|
|
// Hobo version to get an Int32 from a JSValueRef. This is even faster than
|
|
// JSValueToNumberFast() because it blindly assumes the value is an Int32.
|
|
// A number stored as double will produce a bogus Int value. JSC stores all
|
|
// values that are outside of the Int32 range as double.
|
|
|
|
// This function is only fast on 64bit systems. On 32bit systems we can't
|
|
// easily access the integer value.
|
|
// See JavaScriptCore/runtime/JSCJSValue.h for an explanation of the NaN-
|
|
// boxing used in JSC.
|
|
|
|
// Use with extreme caution!
|
|
|
|
static inline int32_t GetInt32(JSContextRef ctx, JSValueRef v) {
|
|
#if __LP64__
|
|
return (int32_t)(0x00000000ffffffffll & (uint64_t)v);
|
|
#else
|
|
return JSValueToNumber(ctx, v, NULL);
|
|
#endif
|
|
}
|
|
|
|
|
|
// Hobo version to create an Int32 fast. Falls back to JSValueMakeNumber()
|
|
// on 32bit systems.
|
|
|
|
static inline JSValueRef MakeInt32(JSContextRef ctx, int32_t number) {
|
|
#if __LP64__
|
|
return (0xffff000000000000ll | (uint64_t)number);
|
|
#else
|
|
return JSValueMakeNumber(ctx, number);
|
|
#endif
|
|
}
|
|
|
|
|
|
// Shorthand to get a property by name
|
|
|
|
static JSValueRef GetPropertyNamed(JSContextRef ctx, JSObjectRef object, const char *name) {
|
|
JSStringRef jsPropertyName = JSStringCreateWithUTF8CString(name);
|
|
JSValueRef value = JSObjectGetProperty(ctx, object, jsPropertyName, NULL);
|
|
JSStringRelease(jsPropertyName);
|
|
return value;
|
|
}
|
|
|
|
|
|
// Shorthand to get the Constructor for the given JSTypedArrayType
|
|
|
|
static JSObjectRef GetConstructor(JSContextRef ctx, JSTypedArrayType type) {
|
|
const char *constructorName = TypeInfo[type].constructorName;
|
|
JSObjectRef global = JSContextGetGlobalObject(ctx);
|
|
return (JSObjectRef)GetPropertyNamed(ctx, global, constructorName);
|
|
}
|
|
|
|
|
|
// Create a typed array view from another typed array or arraybuffer
|
|
|
|
static JSObjectRef GetView(JSContextRef ctx, JSObjectRef object, JSTypedArrayType type, size_t count) {
|
|
JSTypedArrayType currentType = JSObjectGetTypedArrayType(ctx, object);
|
|
if( currentType == kJSTypedArrayTypeNone ) {
|
|
return NULL;
|
|
}
|
|
else if( currentType == type ) {
|
|
return object;
|
|
}
|
|
|
|
JSValueRef args[3];
|
|
if( currentType == kJSTypedArrayTypeArrayBuffer ) {
|
|
args[0] = object;
|
|
args[1] = MakeInt32(ctx, 0);
|
|
args[2] = MakeInt32(ctx, (int)count);
|
|
}
|
|
else {
|
|
args[0] = GetPropertyNamed(ctx, object, "buffer");
|
|
args[1] = GetPropertyNamed(ctx, object, "byteOffset");
|
|
args[2] = MakeInt32(ctx, (int)count);
|
|
}
|
|
JSObjectRef constructor = GetConstructor(ctx, type);
|
|
return JSObjectCallAsConstructor(ctx, constructor, 3, args, NULL);
|
|
}
|
|
|
|
|
|
// The callback called from JSObjectGetTypedArrayData with chunks of data. This writes
|
|
// into the state's data.
|
|
|
|
typedef struct {
|
|
int32_t *currentDataPtr;
|
|
JSObjectRef jsGetCallback;
|
|
JSObjectRef jsGetCallbackApply;
|
|
} AppendDataCallbackState;
|
|
|
|
static JSValueRef AppendDataCallback(
|
|
JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
|
|
size_t argc, const JSValueRef argv[], JSValueRef* exception
|
|
) {
|
|
AppendDataCallbackState *state = JSObjectGetPrivate(thisObject);
|
|
int32_t *dst = state->currentDataPtr;
|
|
int remainderStart = 0;
|
|
|
|
// On 64bit systems where ARM_NEON instructions are available we can use
|
|
// some SIMD intrinsics to extract the lower 32bit of each JSValueRef.
|
|
// Hopefully the JSValueRef encodes an Int32 so the lower 32bit corresponds
|
|
// to that Int32 exactly.
|
|
|
|
#if __LP64__ && defined __ARM_NEON__
|
|
// Iterate over the arguments in packs of 4.
|
|
// Load arguments as 2 lanes of 4 int32 values and store the first
|
|
// lane (the lower 32bit) into the dst buffer.
|
|
|
|
int argPacks4 = (int)argc/4;
|
|
int32_t *src = (int32_t*)argv;
|
|
|
|
for(int i = 0; i < argPacks4; i++ ) {
|
|
const int32x4x2_t lanes32 = vld2q_s32(src);
|
|
vst1q_s32(dst, lanes32.val[0]);
|
|
src += 8;
|
|
dst += 4;
|
|
}
|
|
remainderStart = argPacks4 * 4;
|
|
#endif
|
|
|
|
for( int i = remainderStart; i < argc; i++ ) {
|
|
*(dst++) = GetInt32(ctx, argv[i]);
|
|
}
|
|
|
|
state->currentDataPtr += argc;
|
|
return MakeInt32(ctx, (int)argc);
|
|
}
|
|
|
|
|
|
// To store the AppendDataCallbackState and access it _per context_, we create
|
|
// a plain JSObject where we can store a pointer to the state in the private
|
|
// data.
|
|
|
|
static void FinalizeAppendDataCallbackState(JSObjectRef object) {
|
|
AppendDataCallbackState *state = JSObjectGetPrivate(object);
|
|
free(state);
|
|
}
|
|
|
|
static JSObjectRef CreateAppendDataCallbackState(JSContextRef ctx) {
|
|
// Create a JS function that calls the AppendDataCallback
|
|
JSObjectRef jsAppendDataCallback = JSObjectMakeFunctionWithCallback(ctx, NULL, AppendDataCallback);
|
|
JSValueProtect(ctx, jsAppendDataCallback);
|
|
|
|
// Allocate and create the state and attach the AppendDataCallback
|
|
// function and its .apply property.
|
|
AppendDataCallbackState *state = malloc(sizeof(AppendDataCallbackState));
|
|
state->currentDataPtr = NULL;
|
|
state->jsGetCallback = jsAppendDataCallback;
|
|
state->jsGetCallbackApply = (JSObjectRef)GetPropertyNamed(ctx, jsAppendDataCallback, "apply");
|
|
|
|
|
|
// Create the empty js object that holds the state in its Private data
|
|
JSClassDefinition internalStateClassDef = kJSClassDefinitionEmpty;
|
|
internalStateClassDef.finalize = FinalizeAppendDataCallbackState;
|
|
|
|
JSClassRef internalStateClass = JSClassCreate(&internalStateClassDef);
|
|
JSObjectRef internalStateObject = JSObjectMake(ctx, internalStateClass, state);
|
|
JSClassRelease(internalStateClass);
|
|
return internalStateObject;
|
|
}
|
|
|
|
|
|
|
|
|
|
void JSContextPrepareTypedArrayAPI(JSContextRef ctx) {
|
|
// The __ejTypedArrayType property is read only, not enumerable
|
|
JSPropertyAttributes attributes =
|
|
// kJSPropertyAttributeReadOnly |
|
|
kJSPropertyAttributeDontEnum |
|
|
kJSPropertyAttributeDontDelete;
|
|
|
|
// Install the __ejTypedArrayType property on each Typed Array prototype
|
|
JSStringRef jsTypeName = JSStringCreateWithUTF8CString("__ejTypedArrayType");
|
|
|
|
for( int type = kJSTypedArrayTypeInt8Array; type <= kJSTypedArrayTypeArrayBuffer; type++ ) {
|
|
JSObjectRef jsConstructor = GetConstructor(ctx, type);
|
|
JSObjectRef jsPrototype = (JSObjectRef)GetPropertyNamed(ctx, jsConstructor, "prototype");
|
|
|
|
JSValueRef jsType = MakeInt32(ctx, type);
|
|
JSObjectSetProperty(ctx, jsPrototype, jsTypeName, jsType, attributes, NULL);
|
|
}
|
|
|
|
JSStringRelease(jsTypeName);
|
|
|
|
|
|
// Create the state object for the AppendData Callback and attach it to the global
|
|
// object
|
|
JSObjectRef jsCallbackStateObject = CreateAppendDataCallbackState(ctx);
|
|
|
|
JSStringRef jsInternalStateName = JSStringCreateWithUTF8CString("__ejTypedArrayState");
|
|
JSObjectRef global = JSContextGetGlobalObject(ctx);
|
|
JSObjectSetProperty(ctx, global, jsInternalStateName, jsCallbackStateObject, attributes, NULL);
|
|
JSStringRelease(jsInternalStateName);
|
|
}
|
|
|
|
|
|
JSTypedArrayType JSObjectGetTypedArrayType(JSContextRef ctx, JSObjectRef object) {
|
|
if (!JSValueIsObject(ctx, object)) {
|
|
return kJSTypedArrayTypeNone;
|
|
}
|
|
JSValueRef jsType = GetPropertyNamed(ctx, object, "__ejTypedArrayType");
|
|
if (!JSValueToBoolean(ctx, jsType)) {
|
|
return kJSTypedArrayTypeNone;
|
|
};
|
|
return jsType ? GetInt32(ctx, jsType) : kJSTypedArrayTypeNone;
|
|
}
|
|
|
|
|
|
JSObjectRef JSObjectMakeTypedArrayWithHack(JSContextRef ctx, JSTypedArrayType arrayType, size_t numElements) {
|
|
JSObjectRef jsConstructor = GetConstructor(ctx, arrayType);
|
|
if( !jsConstructor ) {
|
|
return NULL;
|
|
}
|
|
|
|
JSValueRef jsArgs[] = { MakeInt32(ctx, (int)numElements) };
|
|
return JSObjectCallAsConstructor(ctx, jsConstructor, 1, jsArgs, NULL);
|
|
}
|
|
|
|
|
|
JSObjectRef JSObjectMakeTypedArrayWithData(JSContextRef ctx, JSTypedArrayType arrayType, void *data, size_t length) {
|
|
size_t numElements = length / TypeInfo[arrayType].elementSize;
|
|
|
|
JSObjectRef array = JSObjectMakeTypedArrayWithHack(ctx, arrayType, numElements);
|
|
JSObjectSetTypedArrayData(ctx, array, data, length);
|
|
return array;
|
|
}
|
|
|
|
|
|
void *JSObjectGetTypedArrayDataMalloc(JSContextRef ctx, JSObjectRef object, size_t *plength) {
|
|
if (JSObjectGetTypedArrayType(ctx, object) == kJSTypedArrayTypeNone) {
|
|
if (plength) {
|
|
*plength = 0;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
size_t length = GetInt32(ctx, GetPropertyNamed(ctx, object, "byteLength"));
|
|
if (plength) {
|
|
*plength = length;
|
|
}
|
|
|
|
if( !length ) {
|
|
return NULL;
|
|
}
|
|
|
|
size_t int32Count = length / 4;
|
|
size_t uint8Count = length % 4;
|
|
|
|
// For very small typed arrays it's faster to read all bytes individually
|
|
// instead of doing the callback dance.
|
|
if( length < CopyInChunksThreshold ) {
|
|
int32Count = 0;
|
|
uint8Count = length;
|
|
}
|
|
|
|
void *data = malloc(length);
|
|
|
|
// Read data in large chunks of 32bit values
|
|
if( int32Count ) {
|
|
JSObjectRef int32View = GetView(ctx, object, kJSTypedArrayTypeInt32Array, int32Count);
|
|
if( !int32View ) {
|
|
return NULL;
|
|
}
|
|
|
|
JSObjectRef jsState = (JSObjectRef)GetPropertyNamed(ctx, JSContextGetGlobalObject(ctx), "__ejTypedArrayState");
|
|
AppendDataCallbackState *state = JSObjectGetPrivate(jsState);
|
|
state->currentDataPtr = data;
|
|
|
|
|
|
// If the whole data is smaller than the chunk size, we only have to call our callback once, otherwise
|
|
// we have to create subarrays and call the callback with these.
|
|
if( int32Count < CopyChunkSize ) {
|
|
JSValueRef getArgs[] = {jsState, int32View};
|
|
JSObjectCallAsFunction(ctx, state->jsGetCallbackApply, state->jsGetCallback, 2, getArgs, NULL);
|
|
}
|
|
else {
|
|
JSObjectRef subarrayFunc = (JSObjectRef)GetPropertyNamed(ctx, int32View, "subarray");
|
|
|
|
for( int i = 0; i < int32Count; i+= CopyChunkSize) {
|
|
JSValueRef subarrayArgs[] = {MakeInt32(ctx, i), MakeInt32(ctx, i + CopyChunkSize)};
|
|
JSObjectRef jsSubarray = (JSObjectRef)JSObjectCallAsFunction(ctx, subarrayFunc, int32View, 2, subarrayArgs, NULL);
|
|
|
|
JSValueRef getArgs[] = {jsState, jsSubarray};
|
|
JSObjectCallAsFunction(ctx, state->jsGetCallbackApply, state->jsGetCallback, 2, getArgs, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read remaining bytes directly
|
|
if( uint8Count ) {
|
|
uint8_t *values8 = data;
|
|
JSObjectRef uint8View = GetView(ctx, object, kJSTypedArrayTypeUint8Array, length);
|
|
for( int i = 0; i < uint8Count; i++ ) {
|
|
int index = (int)int32Count * 4 + i;
|
|
values8[index] = GetInt32(ctx, JSObjectGetPropertyAtIndex(ctx, uint8View, index, NULL));
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
void JSObjectSetTypedArrayData(JSContextRef ctx, JSObjectRef object, void *data, size_t length) {
|
|
size_t int32Count = length / 4;
|
|
size_t uint8Count = length % 4;
|
|
|
|
if( int32Count ) {
|
|
JSObjectRef int32View = GetView(ctx, object, kJSTypedArrayTypeInt32Array, int32Count);
|
|
if( !int32View ) {
|
|
return;
|
|
}
|
|
|
|
// Construct the JSValues and create the plain JSArray
|
|
JSValueRef *jsValues = malloc(int32Count * sizeof(JSValueRef));
|
|
|
|
const int32_t *values32 = data;
|
|
for(int i = 0; i < int32Count; i++) {
|
|
jsValues[i] = MakeInt32(ctx, values32[i]);
|
|
}
|
|
JSObjectRef jsArray = JSObjectMakeArray(ctx, int32Count, jsValues, NULL);
|
|
|
|
free(jsValues);
|
|
|
|
// Call the .set() function on the Typed Array
|
|
JSObjectRef setFunction = (JSObjectRef)GetPropertyNamed(ctx, int32View, "set");
|
|
JSObjectCallAsFunction(ctx, setFunction, int32View, 1, (JSValueRef[]){jsArray}, NULL);
|
|
}
|
|
|
|
// Set remaining bytes directly
|
|
if( uint8Count ) {
|
|
const uint8_t *values8 = data;
|
|
JSObjectRef uint8View = GetView(ctx, object, kJSTypedArrayTypeUint8Array, length);
|
|
for( int i = 0; i < uint8Count; i++ ) {
|
|
int index = (int)int32Count * 4 + i;
|
|
JSObjectSetPropertyAtIndex(ctx, uint8View, index, MakeInt32(ctx, values8[index]), NULL);
|
|
}
|
|
}
|
|
}
|