diff --git a/quickjs/build.gradle b/quickjs/build.gradle index 16c55fe4c..a8a0354dc 100644 --- a/quickjs/build.gradle +++ b/quickjs/build.gradle @@ -13,5 +13,5 @@ android { dependencies { implementation project(':catvod') - implementation 'wang.harlon.quickjs:wrapper-android:1.0.0-beta' + implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.4' } \ No newline at end of file diff --git a/quickjs/src/main/java/com/fongmi/quickjs/crawler/Spider.java b/quickjs/src/main/java/com/fongmi/quickjs/crawler/Spider.java index 94c7b415a..51fa02728 100644 --- a/quickjs/src/main/java/com/fongmi/quickjs/crawler/Spider.java +++ b/quickjs/src/main/java/com/fongmi/quickjs/crawler/Spider.java @@ -5,8 +5,8 @@ import android.content.Context; import androidx.media3.common.util.UriUtil; import com.fongmi.quickjs.bean.Res; +import com.fongmi.quickjs.method.Async; import com.fongmi.quickjs.method.Console; -import com.fongmi.quickjs.method.Function; import com.fongmi.quickjs.method.Global; import com.fongmi.quickjs.method.Local; import com.fongmi.quickjs.utils.JSUtil; @@ -32,6 +32,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import dalvik.system.DexClassLoader; +import java9.util.concurrent.CompletableFuture; public class Spider extends com.github.catvod.crawler.Spider { @@ -60,12 +61,13 @@ public class Spider extends com.github.catvod.crawler.Spider { } private Object call(String func, Object... args) throws Exception { - return executor.submit((Function.call(jsObject, func, args))).get(); + return CompletableFuture.supplyAsync(() -> Async.run(jsObject, func, args), executor).join().get(); } @Override public void init(Context context, String extend) throws Exception { - call("init", Json.valid(extend) ? ctx.parse(extend) : extend); + if (cat) call("init", submit(() -> cfg(extend)).get()); + else call("init", Json.valid(extend) ? ctx.parse(extend) : extend); } @Override @@ -152,8 +154,8 @@ public class Spider extends com.github.catvod.crawler.Spider { @Override public byte[] getModuleBytecode(String moduleName) { - String code = Module.get().fetch(moduleName); - return code.startsWith("//bb") ? Module.get().bb(code) : ctx.compileModule(code, moduleName); + String content = Module.get().fetch(moduleName); + return content.startsWith("//bb") ? Module.get().bb(content) : ctx.compileModule(content, moduleName); } }); } @@ -202,19 +204,26 @@ public class Spider extends com.github.catvod.crawler.Spider { } private void createObj() { + String jsEval = "__jsEvalReturn"; String spider = "__JS_SPIDER__"; String global = "globalThis." + spider; String content = Module.get().fetch(api); - String catOnly = "\n" + global + " = __jsEvalReturn();"; - if (content.contains("__jsEvalReturn")) { - cat = true; - ctx.evaluateModule(content.concat(catOnly), api); - } else if (content.contains(spider)) { - ctx.evaluateModule(content.replace(spider, global), api); - } else { - ctx.evaluateModule(content.replaceAll("export default.*?[{]", global + " = {"), api); - } + if (content.startsWith("//bb") || content.contains(jsEval)) cat = true; + if (content.startsWith("//bb")) ctx.execute(Module.get().bb(content), spider, jsEval); + else if (content.contains(jsEval)) ctx.evaluateModule(content, api, jsEval); + else if (content.contains(spider)) ctx.evaluateModule(content.replace(spider, global), api); + else ctx.evaluateModule(content.replaceAll("export default.*?[{]", global + " = {"), api); jsObject = (JSObject) ctx.getProperty(ctx.getGlobalObject(), spider); + if (cat) ctx.evaluate("req = http"); + } + + private JSObject cfg(String ext) { + JSObject cfg = ctx.createNewJSObject(); + cfg.setProperty("stype", 3); + cfg.setProperty("skey", key); + if (Json.invalid(ext)) cfg.setProperty("ext", ext); + else cfg.setProperty("ext", (JSObject) ctx.parse(ext)); + return cfg; } private Object[] proxy1(Map params) throws Exception { diff --git a/quickjs/src/main/java/com/fongmi/quickjs/method/Async.java b/quickjs/src/main/java/com/fongmi/quickjs/method/Async.java new file mode 100644 index 000000000..013bfae20 --- /dev/null +++ b/quickjs/src/main/java/com/fongmi/quickjs/method/Async.java @@ -0,0 +1,49 @@ +package com.fongmi.quickjs.method; + +import com.whl.quickjs.wrapper.JSCallFunction; +import com.whl.quickjs.wrapper.JSFunction; +import com.whl.quickjs.wrapper.JSObject; + +import java9.util.concurrent.CompletableFuture; + +public class Async { + + private final CompletableFuture future; + + public static CompletableFuture run(JSObject object, String name, Object[] args) { + return new Async().call(object, name, args); + } + + private Async() { + this.future = new CompletableFuture<>(); + } + + private CompletableFuture call(JSObject object, String name, Object[] args) { + JSFunction function = object.getJSFunction(name); + if (function == null) return empty(); + Object result = function.call(args); + if (result instanceof JSObject) return then(result); + future.complete(result); + return future; + } + + private CompletableFuture empty() { + future.complete(null); + return future; + } + + private CompletableFuture then(Object result) { + JSObject promise = (JSObject) result; + JSFunction then = promise.getJSFunction("then"); + if (then != null) then.call(func); + return future; + } + + private final JSCallFunction func = new JSCallFunction() { + @Override + public Object call(Object... args) { + future.complete(args[0]); + return null; + } + }; +} diff --git a/quickjs/src/main/java/com/fongmi/quickjs/utils/Crypto.java b/quickjs/src/main/java/com/fongmi/quickjs/utils/Crypto.java index 45f28f776..574f52de2 100644 --- a/quickjs/src/main/java/com/fongmi/quickjs/utils/Crypto.java +++ b/quickjs/src/main/java/com/fongmi/quickjs/utils/Crypto.java @@ -28,7 +28,7 @@ public class Crypto { if (iv == null) cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, keySpec); else cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(ivBuf)); byte[] inBuf = inBase64 ? Base64.decode(input.replaceAll("_", "/").replaceAll("-", "+"), Base64.DEFAULT) : input.getBytes("UTF-8"); - return outBase64 ? Base64.encodeToString(cipher.doFinal(inBuf), Base64.DEFAULT) : new String(cipher.doFinal(inBuf), "UTF-8"); + return outBase64 ? Base64.encodeToString(cipher.doFinal(inBuf), Base64.NO_WRAP) : new String(cipher.doFinal(inBuf), "UTF-8"); } catch (Exception e) { e.printStackTrace(); return ""; @@ -53,7 +53,7 @@ public class Crypto { bufIdx = bufEndIdx; outBytes = concatArrays(outBytes, tmpBytes); } - return outBase64 ? Base64.encodeToString(outBytes, Base64.DEFAULT) : new String(outBytes, "UTF-8"); + return outBase64 ? Base64.encodeToString(outBytes, Base64.NO_WRAP) : new String(outBytes, "UTF-8"); } catch (Exception e) { e.printStackTrace(); return ""; diff --git a/quickjs/src/main/java/com/whl/quickjs/android/QuickJSLoader.java b/quickjs/src/main/java/com/whl/quickjs/android/QuickJSLoader.java new file mode 100644 index 000000000..bec1273f5 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/android/QuickJSLoader.java @@ -0,0 +1,18 @@ +package com.whl.quickjs.android; + +/** + * Created by Harlon Wang on 2022/8/12. + */ +public final class QuickJSLoader { + + public static void init() { + System.loadLibrary("quickjs-android-wrapper"); + } + + /** + * Start threads to show stdout and stderr in logcat. + * + * @param tag Android Tag + */ + public native static void startRedirectingStdoutStderr(String tag); +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/JSArray.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSArray.java new file mode 100644 index 000000000..a4221d743 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSArray.java @@ -0,0 +1,23 @@ +package com.whl.quickjs.wrapper; + +public class JSArray extends JSObject { + + public JSArray(QuickJSContext context, long pointer) { + super(context, pointer); + } + + public int length() { + checkReleased(); + return getContext().length(this); + } + + public Object get(int index) { + checkReleased(); + return getContext().get(this, index); + } + + public void set(Object value, int index) { + checkReleased(); + getContext().set(this, value, index); + } +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/JSCallFunction.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSCallFunction.java new file mode 100644 index 000000000..afbe264f5 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSCallFunction.java @@ -0,0 +1,6 @@ +package com.whl.quickjs.wrapper; + +public interface JSCallFunction { + + Object call(Object... args); +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/JSFunction.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSFunction.java new file mode 100644 index 000000000..c2e298cdb --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSFunction.java @@ -0,0 +1,15 @@ +package com.whl.quickjs.wrapper; + +public class JSFunction extends JSObject { + + private final long objPointer; + + public JSFunction(QuickJSContext context, long objPointer, long pointer) { + super(context, pointer); + this.objPointer = objPointer; + } + + public Object call(Object... args) { + return getContext().call(this, objPointer, args); + } +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/JSMethod.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSMethod.java new file mode 100644 index 000000000..0fc8b64e8 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSMethod.java @@ -0,0 +1,11 @@ +package com.whl.quickjs.wrapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.METHOD}) +public @interface JSMethod { +} \ No newline at end of file diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/JSObject.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSObject.java new file mode 100644 index 000000000..fd046d148 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/JSObject.java @@ -0,0 +1,203 @@ +package com.whl.quickjs.wrapper; + +import androidx.annotation.NonNull; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +public class JSObject { + + private final QuickJSContext context; + private final long pointer; + + private boolean isReleased; + + public JSObject(QuickJSContext context, long pointer) { + this.context = context; + this.pointer = pointer; + } + + public long getPointer() { + return pointer; + } + + public QuickJSContext getContext() { + return context; + } + + public Object getProperty(String name) { + checkReleased(); + return context.getProperty(this, name); + } + + public void setProperty(String name, String value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, int value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, long value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, JSObject value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, boolean value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, double value) { + context.setProperty(this, name, value); + } + + public void setProperty(String name, JSCallFunction value) { + context.setProperty(this, name, value); + } + + /** + * Class 添加 {@link JSMethod} 的方法会被注入到 JSContext 中 + * 注意:该方法暂不支持匿名内部类的注册,因为匿名内部类构造参数不是无参的,newInstance 时会报错 + * + * @param name + * @param clazz + */ + public void setProperty(String name, Class clazz) { + Object javaObj = null; + try { + javaObj = clazz.newInstance(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } + + if (javaObj == null) { + throw new NullPointerException("The JavaObj cannot be null. An error occurred in newInstance!"); + } + + JSObject jsObj = context.createNewJSObject(); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(JSMethod.class)) { + Object finalJavaObj = javaObj; + jsObj.setProperty(method.getName(), args -> { + try { + return method.invoke(finalJavaObj, args); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + }); + } + } + + setProperty(name, jsObj); + } + + public String getString(String name) { + Object value = getProperty(name); + return value instanceof String ? (String) value : null; + } + + public Integer getInteger(String name) { + Object value = getProperty(name); + return value instanceof Integer ? (Integer) value : null; + } + + public Boolean getBoolean(String name) { + Object value = getProperty(name); + return value instanceof Boolean ? (Boolean) value : null; + } + + public Double getDouble(String name) { + Object value = getProperty(name); + return value instanceof Double ? (Double) value : null; + } + + public Long getLong(String name) { + Object value = getProperty(name); + return value instanceof Long ? (Long) value : null; + } + + public JSObject getJSObject(String name) { + Object value = getProperty(name); + return value instanceof JSObject ? (JSObject) value : null; + } + + public JSFunction getJSFunction(String name) { + Object value = getProperty(name); + return value instanceof JSFunction ? (JSFunction) value : null; + } + + public JSArray getJSArray(String name) { + Object value = getProperty(name); + return value instanceof JSArray ? (JSArray) value : null; + } + + public JSArray getNames() { + JSFunction getOwnPropertyNames = (JSFunction) context.evaluate("Object.getOwnPropertyNames"); + return (JSArray) getOwnPropertyNames.call(this); + } + + /** + * JSObject 确定不再使用后,调用该方法可主动释放对 JS 对象的引用。 + * 注意:该方法不能调用多次以及释放后不能再被使用对应的 JS 对象。 + */ + public void release() { + checkReleased(); + context.freeValue(this); + isReleased = true; + } + + public void hold() { + context.hold(this); + } + + /** + * 这里与 JavaScript 的 toString 方法保持一致 + * 返回结果参考:https://262.ecma-international.org/14.0/#sec-tostring + * + * @return toString in JavaScript. + */ + @NonNull + @Override + public String toString() { + checkReleased(); + JSFunction toString = getJSFunction("toString"); + return (String) toString.call(); + } + + public String stringify() { + return context.stringify(this); + } + + final void checkReleased() { + if (isReleased) { + throw new NullPointerException("This JSObject was Released, Can not call this!"); + } + } + + public boolean isAlive() { + return context.isLiveObject(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JSObject jsObject = (JSObject) o; + return pointer == jsObject.pointer; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new long[]{pointer}); + } +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/ModuleLoader.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/ModuleLoader.java new file mode 100644 index 000000000..160c4ef93 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/ModuleLoader.java @@ -0,0 +1,46 @@ +package com.whl.quickjs.wrapper; + +/** + * Created by Harlon Wang on 2023/8/26. + * 该类仅提供给 Native 层调用 + */ +public abstract class ModuleLoader { + /** + * 模块加载模式: + * True 会调用 {@link #getModuleBytecode(String)} + * False 会调用 {@link #getModuleStringCode(String)} + * + * @return 是否字节码模式 + */ + public abstract boolean isBytecodeMode(); + + /** + * 获取字节码代码内容 + * + * @param moduleName 模块路径名,例如 "xxx.js" + * @return 代码内容 + */ + public abstract byte[] getModuleBytecode(String moduleName); + + /** + * 获取字符串代码内容 + * + * @param moduleName 模块路径名,例如 "xxx.js" + * @return 代码内容 + */ + public abstract String getModuleStringCode(String moduleName); + + + /** + * 该方法返回结果会作为 moduleName 参数给到 {@link #getModuleBytecode(String)} + * 或者 {@link #getModuleStringCode(String)} 中使用,默认返回 moduleName。 + * 一般可以在这里对模块名称进行转换处理。 + * + * @param baseModuleName 使用 Import 的所在模块名称 + * @param moduleName 需要加载的模块名称 + * @return 模块名称 + */ + public String moduleNormalizeName(String baseModuleName, String moduleName) { + return moduleName; + } +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/NativeCleaner.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/NativeCleaner.java new file mode 100644 index 000000000..39b6153c6 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/NativeCleaner.java @@ -0,0 +1,76 @@ +package com.whl.quickjs.wrapper; + +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.HashSet; +import java.util.Set; + +/** + * https://youtu.be/7_caITSjk1k + */ +abstract class NativeCleaner { + + private Set> phantomReferences = new HashSet<>(); + private ReferenceQueue referenceQueue = new ReferenceQueue<>(); + + /** + * Returns the size of not removed objects. + */ + public int size() { + return phantomReferences.size(); + } + + /** + * Registers the object and the native pointer to this cleaner. + * + * @param referent the object + * @param pointer the native pointer + */ + public void register(T referent, long pointer) { + phantomReferences.add(new NativeReference<>(referent, pointer, referenceQueue)); + } + + /** + * Releases the native resources associated with the native pointer. + * It's called in {@link #clean()} on objects recycled by GC, + * or in {@link #forceClean()} on all objects. + * It's only called once on each object. + * + * @param pointer the native pointer + */ + public abstract void onRemove(long pointer); + + /** + * Calls {@link #onRemove(long)} on objects recycled by GC. + */ + @SuppressWarnings("unchecked") + public void clean() { + NativeReference ref; + while ((ref = (NativeReference) referenceQueue.poll()) != null) { + if (phantomReferences.contains(ref)) { + onRemove(ref.pointer); + phantomReferences.remove(ref); + } + } + } + + /** + * Calls {@link #onRemove(long)} on all objects. + */ + public void forceClean() { + for (NativeReference ref : phantomReferences) { + onRemove(ref.pointer); + } + phantomReferences.clear(); + } + + private static class NativeReference extends PhantomReference { + + private long pointer; + + private NativeReference(T referent, long pointer, ReferenceQueue q) { + super(referent, q); + this.pointer = pointer; + } + } +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSContext.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSContext.java new file mode 100644 index 000000000..5ba9c95cd --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSContext.java @@ -0,0 +1,365 @@ +package com.whl.quickjs.wrapper; + +import java.io.File; +import java.util.HashMap; + +public class QuickJSContext { + + public static abstract class DefaultModuleLoader extends ModuleLoader { + + @Override + public boolean isBytecodeMode() { + return false; + } + + @Override + public byte[] getModuleBytecode(String moduleName) { + return null; + } + } + + public static abstract class BytecodeModuleLoader extends ModuleLoader { + + @Override + public boolean isBytecodeMode() { + return true; + } + + @Override + public String getModuleStringCode(String moduleName) { + return null; + } + } + + private static final String UNKNOWN_FILE = "unknown.js"; + + public static QuickJSContext create() { + return new QuickJSContext(); + } + + public boolean isLiveObject(JSObject jsObj) { + return isLiveObject(runtime, jsObj.getPointer()); + } + + public void setMaxStackSize(int maxStackSize) { + setMaxStackSize(runtime, maxStackSize); + } + + public void runGC() { + runGC(runtime); + } + + public void setMemoryLimit(int memoryLimitSize) { + setMemoryLimit(runtime, memoryLimitSize); + } + + public void dumpMemoryUsage(File target) { + if (target == null || !target.exists()) return; + dumpMemoryUsage(runtime, target.getAbsolutePath()); + } + + // will use stdout to print. + public void dumpMemoryUsage() { + dumpMemoryUsage(runtime, null); + } + + public void dumpObjects(File target) { + if (target == null || !target.exists()) return; + dumpObjects(runtime, target.getAbsolutePath()); + } + + // will use stdout to print. + public void dumpObjects() { + dumpObjects(runtime, null); + } + + private final long runtime; + private final long context; + private final NativeCleaner nativeCleaner = new NativeCleaner() { + @Override + public void onRemove(long pointer) { + freeDupValue(context, pointer); + } + }; + private final long currentThreadId; + private boolean destroyed = false; + private final HashMap callFunctionMap = new HashMap<>(); + + private ModuleLoader moduleLoader; + + private QuickJSContext() { + try { + runtime = createRuntime(); + context = createContext(runtime); + } catch (UnsatisfiedLinkError e) { + throw new QuickJSException("The so library must be initialized before createContext! QuickJSLoader.init should be called on the Android platform. In the JVM, you need to manually call System.loadLibrary"); + } + currentThreadId = Thread.currentThread().getId(); + } + + private void checkSameThread() { + boolean isSameThread = currentThreadId == Thread.currentThread().getId(); + if (!isSameThread) { + throw new QuickJSException("Must be call same thread in QuickJSContext.create!"); + } + } + + public long getCurrentThreadId() { + return currentThreadId; + } + + public void setModuleLoader(ModuleLoader moduleLoader) { + checkSameThread(); + checkDestroyed(); + this.moduleLoader = moduleLoader; + } + + public ModuleLoader getModuleLoader() { + return moduleLoader; + } + + private void checkDestroyed() { + if (destroyed) { + throw new QuickJSException("Can not called this after QuickJSContext was destroyed!"); + } + } + + public JSObject getGlobalObject() { + checkSameThread(); + checkDestroyed(); + return getGlobalObject(context); + } + + public void destroy() { + checkSameThread(); + checkDestroyed(); + nativeCleaner.forceClean(); + callFunctionMap.clear(); + destroyContext(context); + destroyed = true; + } + + public String stringify(JSObject jsObj) { + checkSameThread(); + checkDestroyed(); + return stringify(context, jsObj.getPointer()); + } + + public Object getProperty(JSObject jsObj, String name) { + checkSameThread(); + checkDestroyed(); + return getProperty(context, jsObj.getPointer(), name); + } + + public void setProperty(JSObject jsObj, String name, Object value) { + checkSameThread(); + checkDestroyed(); + if (value instanceof JSCallFunction) putCallFunction((JSCallFunction) value); + setProperty(context, jsObj.getPointer(), name, value); + } + + private void putCallFunction(JSCallFunction callFunction) { + int callFunctionId = callFunction.hashCode(); + callFunctionMap.put(callFunctionId, (JSCallFunction) callFunction); + } + + /** + * 该方法只提供给 Native 层回调. + * + * @param callFunctionId JSCallFunction 对象标识 + */ + public void removeCallFunction(int callFunctionId) { + callFunctionMap.remove(callFunctionId); + } + + /** + * 该方法只提供给 Native 层回调. + * + * @param callFunctionId JSCallFunction 对象标识 + * @param args JS 到 Java 的参数映射 + */ + public Object callFunctionBack(int callFunctionId, Object... args) { + checkSameThread(); + checkDestroyed(); + JSCallFunction callFunction = callFunctionMap.get(callFunctionId); + Object ret = callFunction.call(args); + if (ret instanceof JSCallFunction) putCallFunction((JSCallFunction) ret); + return ret; + } + + public void freeValue(JSObject jsObj) { + checkSameThread(); + checkDestroyed(); + freeValue(context, jsObj.getPointer()); + } + + /** + * @VisibleForTesting 该方法仅供单元测试使用 + */ + int getCallFunctionMapSize() { + return callFunctionMap.size(); + } + + /** + * Native 层注册的 JS 方法里的对象需要在其他地方使用, + * 调用该方法进行计数加一增加引用,不然 JS 方法执行完会被回收掉。 + * 注意:不再使用的时候,调用对应的 {@link #freeDupValue(JSObject)} 方法进行计数减一。 + */ + private void dupValue(JSObject jsObj) { + checkSameThread(); + checkDestroyed(); + dupValue(context, jsObj.getPointer()); + } + + /** + * 引用计数减一,对应 {@link #dupValue(JSObject)} + */ + private void freeDupValue(JSObject jsObj) { + checkSameThread(); + checkDestroyed(); + freeDupValue(context, jsObj.getPointer()); + } + + public int length(JSArray jsArray) { + checkSameThread(); + checkDestroyed(); + return length(context, jsArray.getPointer()); + } + + public Object get(JSArray jsArray, int index) { + checkSameThread(); + checkDestroyed(); + return get(context, jsArray.getPointer(), index); + } + + public void set(JSArray jsArray, Object value, int index) { + checkSameThread(); + checkDestroyed(); + set(context, jsArray.getPointer(), value, index); + } + + Object call(JSObject func, long objPointer, Object... args) { + checkSameThread(); + checkDestroyed(); + for (Object arg : args) if (arg instanceof JSCallFunction) putCallFunction((JSCallFunction) arg); + return call(context, func.getPointer(), objPointer, args); + } + + /** + * Automatically manage the release of objects, + * the hold method is equivalent to call the + * dupValue and freeDupValue methods with NativeCleaner. + */ + public void hold(JSObject jsObj) { + checkSameThread(); + checkDestroyed(); + dupValue(jsObj); + nativeCleaner.register(jsObj, jsObj.getPointer()); + } + + public JSObject createNewJSObject() { + return (JSObject) parse("{}"); + } + + public JSArray createNewJSArray() { + return (JSArray) parse("[]"); + } + + public Object parse(String json) { + checkSameThread(); + checkDestroyed(); + return parseJSON(context, json); + } + + public byte[] compile(String source) { + return compile(source, UNKNOWN_FILE); + } + + public byte[] compile(String source, String fileName) { + checkSameThread(); + checkDestroyed(); + return compile(context, source, fileName, false); + } + + public byte[] compileModule(String source) { + return compileModule(source, UNKNOWN_FILE); + } + + public byte[] compileModule(String source, String fileName) { + checkSameThread(); + checkDestroyed(); + return compile(context, source, fileName, true); + } + + public Object execute(byte[] code) { + return execute(code, UNKNOWN_FILE); + } + + public Object execute(byte[] code, String fileName) { + return execute(code, fileName, "default"); + } + + public Object execute(byte[] code, String fileName, String extName) { + checkSameThread(); + checkDestroyed(); + return execute(context, code, fileName, extName); + } + + public Object evaluate(String script) { + return evaluate(script, UNKNOWN_FILE); + } + + public Object evaluate(String script, String fileName) { + checkSameThread(); + checkDestroyed(); + return evaluate(context, script, fileName, "default", false); + } + + public Object evaluateModule(String script) { + return evaluateModule(script, UNKNOWN_FILE); + } + + public Object evaluateModule(String script, String moduleName) { + return evaluateModule(script, moduleName, "default"); + } + + public Object evaluateModule(String script, String moduleName, String extName) { + checkSameThread(); + checkDestroyed(); + return evaluate(context, script, moduleName, extName, true); + } + + public void throwJSException(String error) { + // throw $error; + String errorScript = "throw " + "\"" + error + "\"" + ";"; + evaluate(errorScript); + } + + // runtime + private native long createRuntime(); + private native void setMaxStackSize(long runtime, int size); // The default is 1024 * 256, and 0 means unlimited. + private native boolean isLiveObject(long runtime, long objValue); + private native void runGC(long runtime); + private native void setMemoryLimit(long runtime, int size); + private native void dumpMemoryUsage(long runtime, String fileName); + private native void dumpObjects(long runtime, String fileName); + // context + private native long createContext(long runtime); + private native Object evaluate(long context, String script, String fileName, String extName, boolean isModule); + private native JSObject getGlobalObject(long context); + private native Object call(long context, long func, long thisObj, Object[] args); + private native Object getProperty(long context, long objValue, String name); + private native void setProperty(long context, long objValue, String name, Object value); + private native String stringify(long context, long objValue); + private native int length(long context, long objValue); + private native Object get(long context, long objValue, int index); + private native void set(long context, long objValue, Object value, int index); + private native void freeValue(long context, long objValue); + private native void dupValue(long context, long objValue); + private native void freeDupValue(long context, long objValue); + private native Object parseJSON(long context, String json); + private native byte[] compile(long context, String sourceCode, String fileName, boolean isModule); // Bytecode compile + private native Object execute(long context, byte[] bytecode, String fileName, String extName); + // destroy context and runtime + private native void destroyContext(long context); +} diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java new file mode 100644 index 000000000..340eae466 --- /dev/null +++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java @@ -0,0 +1,22 @@ +package com.whl.quickjs.wrapper; + +/** + * Created by Harlon Wang on 2022/2/8. + */ +public class QuickJSException extends RuntimeException { + + private final boolean jsError; + + public QuickJSException(String message) { + this(message, false); + } + + public QuickJSException(String message, boolean jsError) { + super(message); + this.jsError = jsError; + } + + public boolean isJSError() { + return jsError; + } +} diff --git a/quickjs/src/main/jniLibs/arm64-v8a/libquickjs-android-wrapper.so b/quickjs/src/main/jniLibs/arm64-v8a/libquickjs-android-wrapper.so new file mode 100644 index 000000000..1654982bf Binary files /dev/null and b/quickjs/src/main/jniLibs/arm64-v8a/libquickjs-android-wrapper.so differ diff --git a/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so b/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so new file mode 100644 index 000000000..08b9bbdb6 Binary files /dev/null and b/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so differ