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

jniMain.com.dokar.quickjs.binding.Bindings.reflect.kt Maven / Gradle / Ivy

The newest version!
package com.dokar.quickjs.binding

import com.dokar.quickjs.QuickJs
import com.dokar.quickjs.QuickJsException
import com.dokar.quickjs.converter.castValueOr
import com.dokar.quickjs.converter.typeOfClass
import com.dokar.quickjs.converter.typeOfInstance
import com.dokar.quickjs.qjsError
import kotlinx.coroutines.suspendCancellableCoroutine
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.resume

/**
 * Define a binding for an instance.
 * Reflection will be used to read fields and call methods, property setters are not supported.
 */
inline fun  QuickJs.define(
    name: String,
    instance: Any,
    parent: JsObjectHandle = JsObjectHandle.globalThis,
) {
    define(
        name = name,
        type = T::class.java,
        instance = instance,
        parent = parent,
    )
}

/**
 * Define a binding for an instance.
 * Reflection will be used to read fields and call methods, property setters are not supported.
 */
fun  QuickJs.define(
    name: String,
    type: Class,
    instance: Any,
    parent: JsObjectHandle = JsObjectHandle.globalThis,
) {
    val fields = type.declaredFields
        .filter { Modifier.isPublic(it.modifiers) }
        .associateBy {
            it.isAccessible = true
            it.name
        }
    val jsFields = fields.values.map(Field::toJsProperty)
    val methods = type.declaredMethods
        .filter { Modifier.isPublic(it.modifiers) }
        .associateBy {
            it.isAccessible = true
            it.name
        }

    val binding = object : ObjectBinding {
        override val properties: List = jsFields
        override val functions: List = methods.values.map {
            JsFunction(name = it.name, isAsync = it.canBeCalledAsSuspend())
        }

        override fun getter(name: String): Any? {
            val field = fields[name] ?: throw QuickJsException(
                "Field '$name' not found in instance $instance"
            )
            return field.get(instance)
        }

        override fun setter(name: String, value: Any?) {
            throw QuickJsException("Setters are not available on reflection bindings.")
        }

        override fun invoke(name: String, args: Array): Any? {
            val method = methods[name] ?: throw QuickJsException(
                "Method '$name' not found in instance $instance"
            )
            return if (method.canBeCalledAsSuspend()) {
                invokeSuspend(method, args)
            } else {
                invokeNormal(method, args)
            }
        }

        private fun invokeNormal(method: Method, args: Array): Any? {
            if (method.parameterCount != args.size) {
                qjsError(
                    "Parameter count mismatched on method '$name', " +
                            "js: ${args.size}, java: ${method.parameterCount}"
                )
            }
            val parameterTypes = method.parameterTypes
            val parameters = args
                .mapIndexed { index, param ->
                    val targetType = typeOfClass(typeConverters, parameterTypes[index].kotlin)
                    castValueOr(param, targetType) {
                        typeConverters.convert(
                            source = it,
                            sourceType = typeOfInstance(typeConverters, it),
                            targetType = targetType
                        )
                    }
                }
                .toTypedArray()
            try {
                return method.invoke(instance, *parameters)
            } catch (e: InvocationTargetException) {
                throw e.targetException
            }
        }

        private fun invokeSuspend(method: Method, args: Array): Any? {
            if (args.size < 2) {
                qjsError(
                    "Unexpected internal parameter count: ${args.size}, no promise " +
                            "handles are provided."
                )
            }
            val funcArgs = args.slice(2..args.lastIndex)
            if (method.parameterCount != funcArgs.size + 1) {
                qjsError(
                    "Parameter count mismatched on method '$name', " +
                            "js: ${funcArgs.size}, java: ${method.parameterCount - 1}"
                )
            }
            val end = method.parameterTypes.lastIndex - 1
            val parameterTypes = method.parameterTypes.slice(0..end)
            val parameters = funcArgs
                .mapIndexed { index, param ->
                    val targetType = typeOfClass(typeConverters, parameterTypes[index].kotlin)
                    castValueOr(param, targetType) {
                        typeConverters.convert(
                            source = it,
                            sourceType = typeOfInstance(typeConverters, it),
                            targetType = targetType
                        )
                    }
                }
                .toTypedArray()
            try {
                invokeAsyncFunction(args) {
                    suspendCancellableCoroutine { continuation ->
                        val ret = method.invoke(instance, *parameters, continuation)
                        if (ret != COROUTINE_SUSPENDED) {
                            continuation.resume(ret)
                        }
                    }
                }
                return null
            } catch (e: InvocationTargetException) {
                throw e.targetException
            }
        }
    }

    defineBinding(name = name, binding = binding, parent = parent)
}

private fun Field.toJsProperty(): JsProperty = JsProperty(
    name = this.name,
    configurable = true,
    writable = false,
    enumerable = true,
)

internal fun Method.canBeCalledAsSuspend(): Boolean {
    val lastArg = this.parameterTypes.lastOrNull() ?: return false
    return Continuation::class.java.isAssignableFrom(lastArg) &&
            this.returnType == Any::class.java
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy