All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.whl.quickjs.wrapper.QuickJSContext Maven / Gradle / Ivy

There is a newer version: 2.2.1
Show newest version
package com.whl.quickjs.wrapper;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

public class QuickJSContext implements Closeable {

    @Override
    public void close() throws IOException {
        destroy();
    }

    public interface Console {
        void log(String info);
        void info(String info);
        void warn(String info);
        void error(String info);
    }

    public interface LeakDetectionListener {
        void notifyLeakDetected(JSObject leak, String stringValue);
    }

    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(new JSObjectCreator() {
            @Override
            public JSObject newObject(QuickJSContext context, long pointer) {
                return new QuickJSObject(context, pointer);
            }

            @Override
            public JSArray newArray(QuickJSContext context, long pointer) {
                return new QuickJSArray(context, pointer);
            }

            @Override
            public JSFunction newFunction(QuickJSContext context, long pointer, long thisPointer) {
                return new QuickJSFunction(context, pointer, thisPointer);
            }
        });
    }

    public static QuickJSContext create(JSObjectCreator creator) {
        return new QuickJSContext(creator);
    }

    public boolean isLiveObject(JSObject jsObj) {
        return isLiveObject(runtime, jsObj.getPointer());
    }

    public void setConsole(Console console) {
        if (console == null) {
            return;
        }

        JSObject consoleObj = getGlobalObject().getJSObject("console");
        consoleObj.setProperty("stdout", args -> {
            if (args.length == 2) {
                String level = (String) args[0];
                String info = (String) args[1];
                switch (level) {
                    case "info":
                        console.info(info);
                        break;
                    case "warn":
                        console.warn(info);
                        break;
                    case "error":
                        console.error(info);
                        break;
                    case "log":
                    case "debug":
                    default:
                        console.log(info);
                        break;
                }
            }

            return null;
        });
        consoleObj.release();
    }

    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());
    }

    public JSObjectCreator getCreator() {
        return creator;
    }

    // will use stdout to print.
    public void dumpObjects() {
        dumpObjects(runtime, null);
    }

    private final long runtime;
    private final long context;
    private final long currentThreadId;
    private boolean destroyed = false;
    private final HashMap callFunctionMap = new HashMap<>();

    private ModuleLoader moduleLoader;
    private JSObject globalObject;
    private final JSObjectCreator creator;
    private final List objectRecords = new ArrayList<>();
    private LeakDetectionListener leakDetectionListener;
    private boolean enableStackTrace = false;

    private QuickJSContext(JSObjectCreator creator) {
        try {
            // 这里代理一层 creator,用来记录 js 对象.
            this.creator = new JSObjectCreator() {
                @Override
                public JSObject newObject(QuickJSContext c, long pointer) {
                    JSObject o = creator.newObject(c, pointer);
                    if (enableStackTrace) {
                        o.setStackTrace(new Throwable());
                    }
                    objectRecords.add(o);
                    return o;
                }

                @Override
                public JSArray newArray(QuickJSContext c, long pointer) {
                    JSArray o = creator.newArray(c, pointer);
                    if (enableStackTrace) {
                        o.setStackTrace(new Throwable());
                    }
                    objectRecords.add(o);
                    return o;
                }

                @Override
                public JSFunction newFunction(QuickJSContext c, long pointer, long thisPointer) {
                    JSFunction o = creator.newFunction(c, pointer, thisPointer);
                    if (enableStackTrace) {
                        o.setStackTrace(new Throwable());
                    }
                    objectRecords.add(o);
                    return o;
                }
            };
            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();
    }

    public void setEnableStackTrace(boolean enableStackTrace) {
        this.enableStackTrace = enableStackTrace;
    }

    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();

        if (moduleLoader == null) {
            throw new NullPointerException("The moduleLoader can not be null!");
        }

        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 Object evaluate(String script) {
        return evaluate(script, UNKNOWN_FILE);
    }

    public Object evaluate(String script, String fileName) {
        if (script == null) {
            throw new NullPointerException("Script cannot be null with " + fileName);
        }

        checkSameThread();
        checkDestroyed();
        return evaluate(context, script, fileName);
    }

    public JSObject getGlobalObject() {
        checkSameThread();
        checkDestroyed();

        if (globalObject == null) {
            globalObject = getGlobalObject(context);
        }

        return globalObject;
    }

    public void setLeakDetectionListener(LeakDetectionListener leakDetectionListener) {
        this.leakDetectionListener = leakDetectionListener;
    }

    public void destroy() {
        checkSameThread();
        checkDestroyed();

        callFunctionMap.clear();
        releaseObjectRecords();
        objectRecords.clear();
        destroyContext(context);
        destroyed = true;
    }

    public void releaseObjectRecords() {
        releaseObjectRecords(true);
    }

    public void releaseObjectRecords(boolean needRelease) {
        // 检测是否有未被释放引用的对象,如果有的话,根据计数释放一下
        JSFunction format = getGlobalObject().getJSFunction("format");
        Iterator objectIterator = objectRecords.iterator();
        while (objectIterator.hasNext()) {
            JSObject object = objectIterator.next();
            // 全局对象交由引擎层会回收,这里先过滤掉
            if (!object.isRefCountZero() && object != getGlobalObject()) {
                int refCount = object.getRefCount();
                if (leakDetectionListener != null) {
                    String value = null;
                    if (format != null) {
                        value = (String) format.call(object);
                    }
                    leakDetectionListener.notifyLeakDetected(object, value);
                }

                if (needRelease) {
                    for (int j = 0; j < refCount; j++) {
                        // 这里不能直接调用 object.release 方法,因为 release 里会调用 list.remove 导致并发修改异常
                        object.decrementRefCount();
                        freeValue(context, object.getPointer());
                    }

                    if (object.getRefCount() == 0) {
                        objectIterator.remove();
                    }
                }
            }
        }
    }

    public List getObjectRecords() {
        return objectRecords;
    }

    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) {
            // Todo 优化:可以只传 callFunctionId 给到 JNI.
            putCallFunction((JSCallFunction) value);
        }

        setProperty(context, jsObj.getPointer(), name, value);
    }

    private void putCallFunction(JSCallFunction callFunction) {
        int callFunctionId = callFunction.hashCode();
        callFunctionMap.put(callFunctionId, 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);
        }

        if (ret instanceof JSObject) {
            // 注意:JSObject 对象作为参数返回到️ JavaScript 中,不需要调用 release 方法,
            // JS 引擎会进行 free,但是这里需要手动对 JSObject 对象的计数减一。
            ((JSObject) ret).decrementRefCount();
        }

        return ret;
    }

    /**
     * JS 引擎层的对象计数减一。
     */
    public void freeValue(JSObject jsObj) {
        checkSameThread();
        checkDestroyed();

        freeValue(context, jsObj.getPointer());

        // todo 如果计数为 0,从 objectRecords 里移除掉
        if (jsObj.getRefCount() == 0) {
            objectRecords.remove(jsObj);
        }
    }

    /**
     * @VisibleForTesting
     * 该方法仅供单元测试使用
     */
    int getCallFunctionMapSize() {
        return callFunctionMap.size();
    }

    /**
     * JS 引擎层的对象计数加一。
     */
    private void dupValue(JSObject jsObj) {
        checkSameThread();
        checkDestroyed();

        dupValue(context, jsObj.getPointer());
    }

    public int length(JSArray jsArray) {
        checkSameThread();
        checkDestroyed();

        // todo 待优化
        if (!isLiveObject(jsArray)) {
            return 0;
        }

        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 (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            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);
    }

    public JSObject createNewJSObject() {
        return parseJSON("{}");
    }

    public JSArray createNewJSArray() {
        return (JSArray) parseJSON("[]");
    }

    /**
     * Use {@link #parse(String)} replace.
     */
    @Deprecated
    public JSObject parseJSON(String json) {
        checkSameThread();
        checkDestroyed();

        Object obj = parseJSON(context, json);
        if (!(obj instanceof JSObject)) {
            throw new QuickJSException("Only parse json with valid format, must be start with '{', if it contains other case, use parse(String) replace.");
        }

        return (JSObject) obj;
    }

    public Object parse(String json) {
        checkSameThread();
        checkDestroyed();
        return parseJSON(context, json);
    }

    public byte[] compile(String script) {
        return compile(script, UNKNOWN_FILE);
    }

    public byte[] compile(String script, String fileName) {
        if (script == null) {
            throw new NullPointerException("Script cannot be null with " + fileName);
        }

        checkSameThread();
        checkDestroyed();

        return compile(context, script, fileName, false);
    }

    public byte[] compileModule(String script) {
        return compileModule(script, UNKNOWN_FILE);
    }

    public byte[] compileModule(String script, String fileName) {
        if (script == null) {
            throw new NullPointerException("Script cannot be null with " + fileName);
        }

        checkSameThread();
        checkDestroyed();

        return compile(context, script, fileName, true);
    }

    public Object execute(byte[] code) {
        if (code == null) {
            throw new NullPointerException("Bytecode cannot be null");
        }

        checkSameThread();
        checkDestroyed();

        return execute(context, code);
    }

    public Object evaluateModule(String script, String moduleName) {
        if (script == null) {
            throw new NullPointerException("Script cannot be null with " + moduleName);
        }

        checkSameThread();
        checkDestroyed();
        return evaluateModule(context, script, moduleName);
    }

    public Object evaluateModule(String script) {
        return evaluateModule(script, UNKNOWN_FILE);
    }

    public void throwJSException(String error) {
        // throw $error;
        String errorScript = "throw " + "\"" + error + "\"" + ";";
        evaluate(errorScript);
    }

    public Object getOwnPropertyNames(JSObject object) {
        return getOwnPropertyNames(context, object.getPointer());
    }

    // 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);
    private native Object evaluateModule(long context, String script, String fileName);
    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); // Bytecode execute
    private native Object getOwnPropertyNames(long context, long objValue);

    // destroy context and runtime
    private native void destroyContext(long context);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy