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

nativeMain.com.dokar.quickjs.QuickJs.native.kt Maven / Gradle / Ivy

The newest version!
package com.dokar.quickjs

import com.dokar.quickjs.binding.AsyncFunctionBinding
import com.dokar.quickjs.binding.Binding
import com.dokar.quickjs.binding.FunctionBinding
import com.dokar.quickjs.binding.JsObjectHandle
import com.dokar.quickjs.binding.ObjectBinding
import com.dokar.quickjs.bridge.ExecuteJobResult
import com.dokar.quickjs.bridge.JsPromise
import com.dokar.quickjs.bridge.compile
import com.dokar.quickjs.bridge.defineFunction
import com.dokar.quickjs.bridge.defineObject
import com.dokar.quickjs.bridge.evaluate
import com.dokar.quickjs.bridge.executePendingJob
import com.dokar.quickjs.bridge.invokeJsFunction
import com.dokar.quickjs.bridge.ktMemoryUsage
import com.dokar.quickjs.bridge.objectHandleToStableRef
import com.dokar.quickjs.bridge.setPromiseRejectionHandler
import com.dokar.quickjs.converter.TypeConverter
import com.dokar.quickjs.converter.TypeConverters
import com.dokar.quickjs.converter.castValueOr
import com.dokar.quickjs.converter.typeOfInstance
import com.dokar.quickjs.util.withLockSync
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.StableRef
import kotlinx.cinterop.toKStringFromUtf8
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import quickjs.JSContext
import quickjs.JSRuntime
import quickjs.JSValue
import quickjs.JS_FreeContext
import quickjs.JS_FreeRuntime
import quickjs.JS_FreeValue
import quickjs.JS_GetRuntime
import quickjs.JS_NewContext
import quickjs.JS_NewRuntime
import quickjs.JS_RunGC
import quickjs.JS_SetMaxStackSize
import quickjs.JS_SetMemoryLimit
import quickjs.JS_UpdateStackTop
import quickjs.quickjs_version
import kotlin.coroutines.cancellation.CancellationException
import kotlin.reflect.typeOf

@OptIn(ExperimentalForeignApi::class)
actual class QuickJs private constructor(
    private val jobDispatcher: CoroutineDispatcher
) {
    private val runtime: CPointer = JS_NewRuntime()
        ?: qjsError("Failed to create js runtime.")

    private val context: CPointer = JS_NewContext(runtime)
        ?: qjsError("Failed to create js context.")

    private val ref = StableRef.create(this)

    private var evalException: Throwable? = null

    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        if (evalException == null) {
            evalException = throwable
        }
    }
    private val coroutineScope = CoroutineScope(jobDispatcher + exceptionHandler)

    private val objectBindings = mutableMapOf()
    private val globalFunctions = mutableMapOf()

    private val managedJsValues = mutableListOf>()

    private val modules = mutableListOf()

    private val jobsMutex = Mutex()
    private val asyncJobs = mutableListOf()

    /**
     * Avoid concurrent executions.
     */
    private val jsMutex = Mutex()

    @PublishedApi
    internal actual val typeConverters = TypeConverters()

    actual var isClosed: Boolean = false
        private set

    actual val version: String
        get() {
            ensureNotClosed()
            return quickjs_version()!!.toKStringFromUtf8()
        }

    actual var memoryLimit: Long = -1
        set(value) {
            ensureNotClosed()
            field = value
            JS_UpdateStackTop(runtime)
            JS_SetMemoryLimit(runtime, value.toULong())
        }

    actual var maxStackSize: Long = 256 * 1024L
        set(value) {
            ensureNotClosed()
            field = value
            JS_UpdateStackTop(runtime)
            JS_SetMaxStackSize(runtime, value.toULong())
        }

    actual val memoryUsage: MemoryUsage
        get() {
            ensureNotClosed()
            JS_UpdateStackTop(runtime)
            return runtime.ktMemoryUsage()
        }

    init {
        setPromiseRejectionHandler(ref, runtime)
    }

    actual fun addTypeConverters(vararg converters: TypeConverter<*, *>) {
        typeConverters.addConverters(*converters)
    }

    actual fun defineBinding(
        name: String,
        binding: ObjectBinding,
        parent: JsObjectHandle
    ): JsObjectHandle {
        ensureNotClosed()
        val handle = context.defineObject(
            quickJsRef = ref,
            parentHandle = parent.nativeHandle,
            name = name,
            binding = binding,
        )
        objectBindings[handle] = binding
        return JsObjectHandle(handle)
    }

    actual fun  defineBinding(
        name: String,
        binding: FunctionBinding
    ) {
        ensureNotClosed()
        context.defineFunction(
            quickJsRef = ref,
            parent = null,
            parentHandle = JsObjectHandle.globalThis.nativeHandle,
            name = name,
            isAsync = false,
        )
        globalFunctions[name] = binding
    }

    actual fun  defineBinding(
        name: String,
        binding: AsyncFunctionBinding
    ) {
        ensureNotClosed()
        context.defineFunction(
            quickJsRef = ref,
            parent = null,
            parentHandle = JsObjectHandle.globalThis.nativeHandle,
            name = name,
            isAsync = true,
        )
        globalFunctions[name] = binding
    }

    @Throws(QuickJsException::class)
    actual fun addModule(name: String, code: String) {
        ensureNotClosed()
        modules.add(compile(code = code, filename = name, asModule = true))
    }

    actual fun addModule(bytecode: ByteArray) {
        ensureNotClosed()
        modules.add(bytecode)
    }

    @Throws(QuickJsException::class)
    actual fun compile(
        code: String,
        filename: String,
        asModule: Boolean
    ): ByteArray {
        ensureNotClosed()
        jsMutex.withLockSync {
            return context.compile(code = code, filename = filename, asModule = asModule)
        }
    }

    @Throws(QuickJsException::class, CancellationException::class)
    actual suspend inline fun  evaluate(bytecode: ByteArray): T {
        return castValueOr(evalInternal(bytecode = bytecode), typeOf()) {
            typeConverters.convert(
                source = it,
                sourceType = typeOfInstance(typeConverters, it),
                targetType = typeOf()
            )
        }
    }

    @Throws(QuickJsException::class, CancellationException::class)
    actual suspend inline fun  evaluate(
        code: String,
        filename: String,
        asModule: Boolean
    ): T {
        return castValueOr(
            evalInternal(code = code, filename = filename, asModule = asModule),
            typeOf()
        ) {
            typeConverters.convert(
                source = it,
                sourceType = typeOfInstance(typeConverters, it),
                targetType = typeOf()
            )
        }
    }

    @PublishedApi
    @Throws(QuickJsException::class, CancellationException::class)
    internal suspend fun evalInternal(bytecode: ByteArray): Any? = evalAndAwait {
        context.evaluate(bytecode = bytecode)
    }

    @PublishedApi
    @Throws(QuickJsException::class, CancellationException::class)
    internal suspend fun evalInternal(
        code: String,
        filename: String,
        asModule: Boolean
    ): Any? = evalAndAwait {
        context.evaluate(code = code, filename = filename, asModule = asModule)
    }

    actual fun gc() {
        ensureNotClosed()
        jsMutex.withLockSync {
            JS_UpdateStackTop(runtime)
            JS_RunGC(runtime)
        }
    }

    actual fun close() {
        if (isClosed) return
        isClosed = true
        evalException = null
        jobsMutex.withLockSync {
            asyncJobs.forEach { it.cancel() }
            asyncJobs.clear()
        }
        jsMutex.withLockSync {}
        modules.clear()
        managedJsValues.forEach { JS_FreeValue(context, it) }
        managedJsValues.clear()
        // Dispose stable refs
        objectBindings.keys.forEach { objectHandleToStableRef(it)?.dispose() }
        objectBindings.clear()
        globalFunctions.clear()
        JS_FreeContext(context)
        JS_FreeRuntime(runtime)
        ref.dispose()
    }

    @Suppress("UNCHECKED_CAST")
    internal actual fun invokeAsyncFunction(
        args: Array,
        block: suspend (bindingArgs: Array) -> Any?
    ) {
        ensureNotClosed()
        val resolveFunc = args[0] as CValue
        val rejectFunc = args[1] as CValue
        val job = coroutineScope.launch {
            try {
                val result = block(args.sliceArray(2.. JsPromise
    ): Any? {
        ensureNotClosed()
        evalException = null
        loadModules()
        var resultPromise: JsPromise? = null
        try {
            resultPromise = jsMutex.withLock { block() }
            awaitAsyncJobs()
            checkException()
            jsMutex.withLock {
                JS_UpdateStackTop(JS_GetRuntime(context))
                return resultPromise.result(context)
            }
        } finally {
            jsMutex.withLock { resultPromise?.free(context) }
        }
    }

    private suspend fun awaitAsyncJobs() {
        jsMutex.withLock {
            do {
                // Execute JS Promises, putting this in while(true) is unnecessary
                // since we have the same loop after every asyncFunction call
                val execResult = executePendingJob(runtime)
                if (execResult is ExecuteJobResult.Failure) {
                    throw execResult.error
                }
            } while (execResult == ExecuteJobResult.Success)
        }
        while (true) {
            val jobs = jobsMutex.withLock { asyncJobs.filter { it.isActive } }
            if (jobs.isEmpty()) {
                // No jobs to run
                break
            }
            jobs.joinAll()
        }
    }

    private suspend fun loadModules() = jsMutex.withLock {
        for (module in modules) {
            context.evaluate(module).free(context)
        }
        modules.clear()
    }

    private fun ensureNotClosed() {
        if (isClosed) {
            qjsError("Already closed.")
        }
    }

    private fun checkException() {
        val exception = evalException ?: return
        throw exception
    }

    internal fun addManagedJsValues(vararg value: CValue) {
        managedJsValues.addAll(value)
    }

    internal fun onCallBindingGetter(
        parentHandle: Long,
        name: String,
    ): Any? {
        val binding = objectBindings[parentHandle] ?: qjsError("Parent not found.")
        return binding.getter(name)
    }

    internal fun onCallBindingSetter(
        parentHandle: Long,
        name: String,
        value: Any?
    ) {
        val binding = objectBindings[parentHandle] ?: qjsError("Parent not found.")
        binding.setter(name, value)
    }

    internal fun onCallBindingFunction(
        parentHandle: Long,
        name: String,
        args: Array,
    ): Any? {
        return if (parentHandle == JsObjectHandle.globalThis.nativeHandle) {
            val binding = globalFunctions[name] ?: qjsError("Global function '$name' not found.")
            when (binding) {
                is AsyncFunctionBinding<*> -> invokeAsyncFunction(args) { binding.invoke(it) }
                is FunctionBinding<*> -> binding.invoke(args)
                is ObjectBinding -> qjsError("Unexpected object binding, require a function binding.")
            }
        } else {
            val binding = objectBindings[parentHandle] ?: qjsError("Parent not found.")
            binding.invoke(name, args)
        }
    }

    internal fun setUnhandledPromiseRejection(reason: Any?) {
        ensureNotClosed()
        if (evalException == null) {
            evalException = reason as? Throwable ?: QuickJsException(reason.toString())
        }
        jobsMutex.withLockSync { asyncJobs.forEach { it.cancel() } }
    }

    actual companion object {
        @Throws(QuickJsException::class)
        actual fun create(jobDispatcher: CoroutineDispatcher): QuickJs {
            return QuickJs(jobDispatcher = jobDispatcher)
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy