Skip to content

Commit

Permalink
Merge pull request #84 from HarlonWang/feature/2.4.0
Browse files Browse the repository at this point in the history
支持 ArrayBuffer 和 byte 类型互转
  • Loading branch information
HarlonWang authored Nov 14, 2024
2 parents c4912a8 + 0cb3b5b commit d561f7b
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 40 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## 2.4.0 *(2024-11-14)*
- 新增方法: 获取使用内存的大小信息(getMemoryUsedSize)
- 支持 ArrayBuffer 转为 Byte 数组(深拷贝,对性能有一些影响)

## 2.2.1 *(2024-09-29)*
- JSObject 增加 toMap 方法,支持转 HashMap 类型

Expand Down
59 changes: 27 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ QuickJS wrapper for Android/JVM.
- JavaScript exception handler
- Compile bytecode
- Supports converting JS object types to Java HashMap.
- ESModule (import, export)

Experimental Features Stability not guaranteed.
- ESModule (import, export)
- Supports ArrayBuffer to a byte array type.

## Download

Expand Down Expand Up @@ -85,20 +86,14 @@ QuickJSLoader.init();

```Java
QuickJSContext context = QuickJSContext.create();
```

### Destroy QuickJSContext
// evaluating JavaScript
context.evaluate("var a = 1 + 2;");

```Java
// destroy QuickJSContext
context.destroy();
```

### Evaluating JavaScript

```Java
context.evaluate("var a = 1 + 2;");
```

### Console Support
```Java
context.setConsole(your console implementation.);
Expand All @@ -107,28 +102,28 @@ context.setConsole(your console implementation.);
### Supported Types

#### Java and JavaScript can directly convert to each other for the following basic types
- `boolean`
- `int`
- `long`
- `double`
- `String`
- `null`

#### Mutual conversion of JS object types
- `JSObject` represents a JavaScript object
- `JSFunction` represents a JavaScript function
- `JSArray` represents a JavaScript Array

#### About Long type
There is no Long type in JavaScript, the conversion of Long type is special.

- Java --> JavaScript
- The Long value <= Number.MAX_SAFE_INTEGER, will be convert to Number type.
- The Long value > Number.MAX_SAFE_INTEGER, will be convert to BigInt type.
- Number.MIN_SAFE_INTEGER is the same to above.

- JavaScript --> Java
- Number(Int64) or BigInt --> Long type
| JavaScript | Java |
|-------------|-------------------|
| null | null |
| undefined | null |
| boolean | Boolean |
| Number | Long/Int/Double |
| string | String |
| Array | JSArray |
| object | JSObject |
| Function | JSFunction |
| ArrayBuffer | byte[](Deep copy) |

Since JavaScript doesn't have a `long` type, additional information about `long`:

Java --> JavaScript
- The Long value <= Number.MAX_SAFE_INTEGER, will be convert to Number type.
- The Long value > Number.MAX_SAFE_INTEGER, will be convert to BigInt type.
- Number.MIN_SAFE_INTEGER is the same to above.

JavaScript --> Java
- Number(Int64) or BigInt --> Long type


### Set Property
Java
Expand Down
9 changes: 9 additions & 0 deletions native/cpp/quickjs_context_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,13 @@ Java_com_whl_quickjs_wrapper_QuickJSContext_getOwnPropertyNames(JNIEnv *env, job
jlong context, jlong obj_value) {
auto wrapper = reinterpret_cast<QuickJSWrapper*>(context);
return wrapper->getOwnPropertyNames(env, thiz, obj_value);
}
extern "C"
JNIEXPORT jlong JNICALL
Java_com_whl_quickjs_wrapper_QuickJSContext_getMemoryUsedSize(JNIEnv *env, jobject thiz,
jlong runtime) {
auto *rt = reinterpret_cast<JSRuntime*>(runtime);
JSMemoryUsage usage;
JS_ComputeMemoryUsage(rt, &usage);
return (jlong)usage.memory_used_size;
}
34 changes: 33 additions & 1 deletion native/cpp/quickjs_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ static string getJavaName(JNIEnv* env, jobject javaClass) {
return str;
}

// quickjs 没有提供 JS_IsArrayBuffer 方法,这里通过取巧的方式来实现,后续可以替换掉
static bool JS_IsArrayBuffer(JSValue value) {
// quickjs 里的 ArrayBuffer 对应的类型枚举值
int8_t JS_CLASS_ARRAY_BUFFER = 19;
return JS_GetClassID(value) == JS_CLASS_ARRAY_BUFFER;
}

static void tryToTriggerOnError(JSContext *ctx, JSValueConst *error) {
JSValue global = JS_GetGlobalObject(ctx);
JSValue onerror = JS_GetPropertyStr(ctx, global, "onError");
Expand Down Expand Up @@ -231,6 +238,12 @@ jsModuleLoaderFunc(JSContext *ctx, const char *module_name, void *opaque) {
int scriptLen = env->GetStringUTFLength((jstring) result);
JSValue func_val = JS_Eval(ctx, script, scriptLen, module_name,
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
if (JS_IsException(func_val)) {
JS_FreeValue(ctx, func_val);
throwJSException(env, ctx);
return (JSModuleDef *) JS_VALUE_GET_PTR(JS_EXCEPTION);
}

m = JS_VALUE_GET_PTR(func_val);
JS_FreeValue(ctx, func_val);
}
Expand Down Expand Up @@ -333,6 +346,7 @@ QuickJSWrapper::QuickJSWrapper(JNIEnv *env, jobject thiz, JSRuntime *rt) {
quickjsContextClass = (jclass)(jniEnv->NewGlobalRef(jniEnv->FindClass("com/whl/quickjs/wrapper/QuickJSContext")));
moduleLoaderClass = (jclass)(jniEnv->NewGlobalRef(jniEnv->FindClass("com/whl/quickjs/wrapper/ModuleLoader")));
creatorClass = (jclass)(jniEnv->NewGlobalRef(jniEnv->FindClass("com/whl/quickjs/wrapper/JSObjectCreator")));
byteArrayClass = (jclass) jniEnv->NewGlobalRef(env->FindClass("[B"));

booleanValueOf = jniEnv->GetStaticMethodID(booleanClass, "valueOf", "(Z)Ljava/lang/Boolean;");
integerValueOf = jniEnv->GetStaticMethodID(integerClass, "valueOf", "(I)Ljava/lang/Integer;");
Expand Down Expand Up @@ -376,6 +390,7 @@ QuickJSWrapper::~QuickJSWrapper() {
jniEnv->DeleteGlobalRef(moduleLoaderClass);
jniEnv->DeleteGlobalRef(quickjsContextClass);
jniEnv->DeleteGlobalRef(creatorClass);
jniEnv->DeleteGlobalRef(byteArrayClass);
}

jobject QuickJSWrapper::toJavaObject(JNIEnv *env, jobject thiz, JSValueConst& this_obj, JSValueConst& value) const{
Expand Down Expand Up @@ -436,6 +451,16 @@ jobject QuickJSWrapper::toJavaObject(JNIEnv *env, jobject thiz, JSValueConst& th
result = env->CallObjectMethod(creatorObj, newFunctionM, thiz, value_ptr, obj_ptr);
} else if (JS_IsArray(context, value)) {
result = env->CallObjectMethod(creatorObj, newArrayM, thiz, value_ptr);
} else if (JS_IsArrayBuffer(value)) {
size_t byteLength = 0;
uint8_t *buffer = JS_GetArrayBuffer(context, &byteLength, value);
jbyteArray byteArray = env->NewByteArray(byteLength);
void *elementsPtr = env->GetPrimitiveArrayCritical(byteArray, nullptr);
jbyte *elements = reinterpret_cast<jbyte *>(elementsPtr);
memcpy(elements, buffer, byteLength);
result = byteArray;
JS_FreeValue(context, value);
env->ReleasePrimitiveArrayCritical(byteArray, elements, 0);
} else {
result = env->CallObjectMethod(creatorObj, newObjectM, thiz, value_ptr);
}
Expand Down Expand Up @@ -514,7 +539,8 @@ jobject QuickJSWrapper::call(JNIEnv *env, jobject thiz, jlong func, jlong this_o
// 基础类型(例如 string )和 Java callback 类型需要使用完 free.
if (env->IsInstanceOf(arg, stringClass) || env->IsInstanceOf(arg, doubleClass) ||
env->IsInstanceOf(arg, integerClass) || env->IsInstanceOf(arg, longClass) ||
env->IsInstanceOf(arg, booleanClass) || env->IsInstanceOf(arg, jsCallFunctionClass)) {
env->IsInstanceOf(arg, booleanClass) || env->IsInstanceOf(arg, jsCallFunctionClass)
|| env->IsInstanceOf(arg, byteArrayClass)) {
freeArguments.push_back(jsArg);
}

Expand Down Expand Up @@ -676,6 +702,12 @@ JSValue QuickJSWrapper::toJSValue(JNIEnv *env, jobject thiz, jobject value) cons
}
} else if (env->IsInstanceOf(value, booleanClass)) {
result = JS_NewBool(context, env->CallBooleanMethod(value, booleanGetValue));
} else if (env->IsInstanceOf(value, byteArrayClass)) {
jbyteArray bytes = static_cast<jbyteArray>(value);
jbyte* byteData = env->GetByteArrayElements(bytes, nullptr);
jsize length = env->GetArrayLength(bytes);
result = JS_NewArrayBufferCopy(context, reinterpret_cast<uint8_t*>(byteData), length);
env->ReleaseByteArrayElements(bytes, byteData, JNI_ABORT);
} else if (env->IsInstanceOf(value, jsObjectClass)) {
result = JS_MKPTR(JS_TAG_OBJECT, reinterpret_cast<void *>(env->CallLongMethod(value, jsObjectGetValue)));
} else if (env->IsInstanceOf(value, jsCallFunctionClass)) {
Expand Down
1 change: 1 addition & 0 deletions native/cpp/quickjs_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class QuickJSWrapper {
jclass quickjsContextClass;
jclass moduleLoaderClass;
jclass creatorClass;
jclass byteArrayClass;
JSValue ownPropertyNames;

jmethodID booleanValueOf;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1236,4 +1236,26 @@ public void testObjectLeakDetection() {
context.destroy();
}

@Test
public void testArrayBytes() {
QuickJSContext context = createContext();
byte[] bytes = "test测试".getBytes();
byte[] buffer = (byte[]) context.evaluate("new Int8Array([116, 101, 115, 116, -26, -75, -117, -24, -81, -107]).buffer");
assertArrayEquals(bytes, buffer);

context.getGlobalObject().setProperty("testBuffer", bytes);
byte[] testBuffers = context.getGlobalObject().getBytes("testBuffer");
assertArrayEquals(testBuffers, buffer);

context.destroy();
}

@Test
public void testArrayBytes1() {
QuickJSContext context = createContext();
JSFunction bufferTest = (JSFunction) context.evaluate("const bufferTest = (buffer) => { if(new Int8Array(buffer)[0] !== 116) { throw Error('failed, not equal'); }; }; bufferTest;");
bufferTest.callVoid("test测试".getBytes());
context.destroy();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface JSObject {
void setProperty(String name, JSObject value);
void setProperty(String name, boolean value);
void setProperty(String name, double value);
void setProperty(String name, byte[] value);
void setProperty(String name, JSCallFunction value);
void setProperty(String name, Class<?> clazz);
long getPointer();
Expand All @@ -32,6 +33,7 @@ public interface JSObject {
Double getDoubleProperty(String name);
Double getDouble(String name);
Long getLong(String name);
byte[] getBytes(String name);
@Deprecated
JSObject getJSObjectProperty(String name);
JSObject getJSObject(String name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public void setMemoryLimit(int memoryLimitSize) {
setMemoryLimit(runtime, memoryLimitSize);
}

// Return the byte size.
public long getMemoryUsedSize() {
return getMemoryUsedSize(runtime);
}

public void dumpMemoryUsage(File target) {
if (target == null || !target.exists()) {
return;
Expand Down Expand Up @@ -578,6 +583,7 @@ public Object getOwnPropertyNames(JSObject object) {
private native void setMemoryLimit(long runtime, int size);
private native void dumpMemoryUsage(long runtime, String fileName);
private native void dumpObjects(long runtime, String fileName);
private native long getMemoryUsedSize(long runtime);

// context
private native long createContext(long runtime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public class QuickJSFunction extends QuickJSObject implements JSFunction {
* 函数执行状态
*/
enum Status {
NOT_STARTED,
IN_PROGRESS,
COMPLETED
NOT_CALLED,
CALLING,
CALLED
}
private int stashTimes = 0;
private Status currentStatus = Status.NOT_STARTED;
private Status currentStatus = Status.NOT_CALLED;

private final long thisPointer;

Expand All @@ -30,7 +30,7 @@ public QuickJSFunction(QuickJSContext context, long pointer, long thisPointer) {
public void release() {
// call 函数未执行完,触发了 release 操作,会导致 quickjs 野指针异常,
// 这里暂存一下,待函数执行完,才执行 release。
if (currentStatus == Status.IN_PROGRESS) {
if (currentStatus == Status.CALLING) {
stashTimes++;
return;
}
Expand All @@ -41,9 +41,9 @@ public void release() {
public Object call(Object... args) {
checkRefCountIsZero();

currentStatus = Status.IN_PROGRESS;
currentStatus = Status.CALLING;
Object ret = getContext().call(this, thisPointer, args);
currentStatus = Status.COMPLETED;
currentStatus = Status.CALLED;

if (stashTimes > 0) {
// 如果有暂存,这里需要恢复下 release 操作
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public void setProperty(String name, double value) {
setPropertyObject(name, value);
}

@Override
public void setProperty(String name, byte[] value) {
setPropertyObject(name, value);
}

@Override
public void setProperty(String name, JSCallFunction value) {
setPropertyObject(name, value);
Expand Down Expand Up @@ -176,6 +181,12 @@ public Long getLong(String name) {
return value instanceof Long ? (Long) value : null;
}

@Override
public byte[] getBytes(String name) {
Object value = getProperty(name);
return value instanceof byte[] ? (byte[]) value : null;
}

@Override
public JSObject getJSObjectProperty(String name) {
return getJSObject(name);
Expand Down

0 comments on commit d561f7b

Please sign in to comment.