From 0b90bed917a7384149c85bf0c2e6a76284940941 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:54:17 +0200 Subject: [PATCH 01/47] Minimum implementation of method accessors --- .../classfile/build.gradle.kts | 23 ++++ .../ClassFileMethodAccessorFactory.kt | 27 ++++ .../ClassFileMethodAccessorGenerator.kt | 127 ++++++++++++++++++ .../internal/codegen/utils/ClassDescs.kt | 15 +++ .../internal/codegen/utils/CodeBuilder.kt | 45 +++++++ .../accessors/internal/utils/Reflection.kt | 9 ++ ...d.accessors.internal.FunctionCallerFactory | 1 + .../ClassFileMethodAccessorGeneratorTest.kt | 79 +++++++++++ .../core/build.gradle.kts | 11 ++ .../accessors/internal/MethodAccessor.kt | 9 ++ .../internal/MethodAccessorFactory.kt | 19 +++ .../kotlin-reflect/build.gradle.kts | 10 ++ .../internal/KotlinReflectMethodAccessor.kt | 22 +++ .../KotlinReflectMethodAccessorFactory.kt | 15 +++ ...d.accessors.internal.FunctionCallerFactory | 1 + settings.gradle.kts | 5 + 16 files changed, 418 insertions(+) create mode 100644 BotCommands-method-accessors/classfile/build.gradle.kts create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/utils/Reflection.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory create mode 100644 BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt create mode 100644 BotCommands-method-accessors/core/build.gradle.kts create mode 100644 BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt create mode 100644 BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt create mode 100644 BotCommands-method-accessors/kotlin-reflect/build.gradle.kts create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory diff --git a/BotCommands-method-accessors/classfile/build.gradle.kts b/BotCommands-method-accessors/classfile/build.gradle.kts new file mode 100644 index 000000000..4519b930b --- /dev/null +++ b/BotCommands-method-accessors/classfile/build.gradle.kts @@ -0,0 +1,23 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("BotCommands-conventions") + id("BotCommands-publish-conventions") +} + +dependencies { + implementation(projects.botCommandsMethodAccessors.core) +} + +java { + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_24 + } +} + +configurePublishedArtifact(artifactId = "BotCommands-method-accessors-classfile") diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt new file mode 100644 index 000000000..ad73b7d74 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt @@ -0,0 +1,27 @@ +package dev.freya02.botcommands.method.accessors.internal + +import dev.freya02.botcommands.method.accessors.internal.codegen.ClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory +import java.lang.invoke.MethodHandles +import kotlin.reflect.KFunction + +internal class ClassFileMethodAccessorFactory : MethodAccessorFactory { + + private val lookup = MethodHandles.lookup() + + override val priority: Int get() = 100 + + override fun create( + instance: Any, + function: KFunction<*>, + ): MethodAccessor { + val executable = function.javaExecutable + require(executable.declaringClass.isAssignableFrom(instance.javaClass)) { + "Function is not from the instance's class, function: ${executable.declaringClass.name}, instance: ${instance.javaClass.name}" + } + + return ClassFileMethodAccessorGenerator.generate(instance, function, lookup) + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt new file mode 100644 index 000000000..b3c9d1246 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -0,0 +1,127 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.* +import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassFile.* +import java.lang.classfile.TypeKind +import java.lang.constant.ClassDesc +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.invoke.MethodHandles +import java.lang.reflect.AccessFlag +import java.lang.reflect.Method +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.jvmErasure + +internal object ClassFileMethodAccessorGenerator { + + internal fun generate( + instance: Any, + function: KFunction<*>, + lookup: MethodHandles.Lookup, + ): MethodAccessor { + // TODO support constructors? unsure if it will be beneficial for services, they run once, see what's the diff in stack traces + val executable = function.javaExecutable + require(executable is Method) { "Constructors are not supported yet" } + + val hasOptionals = function.parameters.any { it.isOptional } + require(!hasOptionals) { "Optionals are not supported yet" } + + val isSuspend = function.isSuspend + require(!isSuspend) { "Suspending functions are not supported yet" } + + function.parameters.forEach { parameter -> + require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { + "Unsupported parameter kind: $parameter" + } + } + + val instanceDesc = instance.javaClass.describeConstable().get() + val methodTypeDesc = run { + val returnTypeDesc = executable.returnType.describeConstable().get() + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + MethodTypeDesc.of(returnTypeDesc, parameterDescs) + } + + // The class must be unique per function, which is why we don't cache the class + // Also "duplicate" definitions are allowed for hidden classes + val thisClass = ClassDesc.of("${lookup.lookupClass().packageName}.ClassFileMethodAccessor") + val bytes = ClassFile.of().build(thisClass) { classBuilder -> + classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) + classBuilder.withInterfaceSymbols(CD_MethodAccessor) + + // TODO replace with class data of hidden class + classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) + classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) + + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + val instanceSlot = codeBuilder.parameterSlot(0) + val functionSlot = codeBuilder.parameterSlot(1) + + // this.super() + codeBuilder.aload(thisSlot) + codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) + + // this.instance = instance; + codeBuilder.aload(thisSlot) + codeBuilder.aload(instanceSlot) + codeBuilder.putfield(thisClass, "instance", instanceDesc) + + // this.function = function; + codeBuilder.aload(thisSlot) + codeBuilder.aload(functionSlot) + codeBuilder.putfield(thisClass, "function", CD_KFunction) + + codeBuilder.return_() + } + + classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + val continuationSlot = codeBuilder.parameterSlot(1) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // this.instance.[methodName]([params]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + function.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed + + // var parameter = function.getParameters().get([index]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "function", CD_KFunction) + codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) + codeBuilder.loadConstant(index) + codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) + codeBuilder.checkcast(CD_KParameter) + codeBuilder.astore(parameterSlot) + + // = args.get(parameter) + codeBuilder.aload(argsSlot) + codeBuilder.aload(parameterSlot) + codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) + codeBuilder.castTo(target = parameter.type.jvmErasure.java) + } + // TODO other method types could be called + codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + // Discard invoked method return value + if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() + + codeBuilder.aload(continuationSlot) + codeBuilder.areturn() + } + } + + val clazz = lookup + .defineHiddenClass(bytes, true) + .lookupClass() + return clazz + .getDeclaredConstructor(instance.javaClass, KFunction::class.java) + .newInstance(instance, function) as MethodAccessor + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt new file mode 100644 index 000000000..33a8568a9 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -0,0 +1,15 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.utils + +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import java.lang.constant.ClassDesc +import kotlin.coroutines.Continuation +import kotlin.reflect.KCallable +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +internal val CD_Continuation = ClassDesc.of(Continuation::class.java.name) +internal val CD_KCallable = ClassDesc.of(KCallable::class.java.name) +internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) +internal val CD_KParameter = ClassDesc.of(KParameter::class.java.name) + +internal val CD_MethodAccessor = ClassDesc.of(MethodAccessor::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt new file mode 100644 index 000000000..aca50c5fc --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -0,0 +1,45 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.utils + +import java.lang.classfile.CodeBuilder +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc + +internal fun CodeBuilder.castTo(target: Class<*>) { + when (target) { + Boolean::class.javaPrimitiveType -> { + checkcast(CD_Boolean) + invokevirtual(CD_Boolean, "booleanValue", MethodTypeDesc.of(CD_boolean)) + } + Byte::class.javaPrimitiveType -> { + checkcast(CD_Byte) + invokevirtual(CD_Byte, "byteValue", MethodTypeDesc.of(CD_byte)) + } + Char::class.javaPrimitiveType -> { + checkcast(CD_Character) + invokevirtual(CD_Character, "charValue", MethodTypeDesc.of(CD_char)) + } + Short::class.javaPrimitiveType -> { + checkcast(CD_Short) + invokevirtual(CD_Short, "shortValue", MethodTypeDesc.of(CD_short)) + } + Int::class.javaPrimitiveType -> { + checkcast(CD_Integer) + invokevirtual(CD_Integer, "intValue", MethodTypeDesc.of(CD_int)) + } + Long::class.javaPrimitiveType -> { + checkcast(CD_Long) + invokevirtual(CD_Long, "longValue", MethodTypeDesc.of(CD_long)) + } + Float::class.javaPrimitiveType -> { + checkcast(CD_Float) + invokevirtual(CD_Float, "floatValue", MethodTypeDesc.of(CD_float)) + } + Double::class.javaPrimitiveType -> { + checkcast(CD_Double) + invokevirtual(CD_Double, "doubleValue", MethodTypeDesc.of(CD_double)) + } + else -> { + checkcast(target.describeConstable().get()) + } + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/utils/Reflection.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/utils/Reflection.kt new file mode 100644 index 000000000..562844511 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/utils/Reflection.kt @@ -0,0 +1,9 @@ +package dev.freya02.botcommands.method.accessors.internal.utils + +import java.lang.reflect.Executable +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.javaMethod + +internal val KFunction<*>.javaExecutable: Executable + get() = javaMethod ?: javaConstructor ?: error("Could not get executable of $this") diff --git a/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory b/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory new file mode 100644 index 000000000..6663b5197 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory @@ -0,0 +1 @@ +dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt new file mode 100644 index 000000000..6b5656ea2 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -0,0 +1,79 @@ +package dev.freya02.botcommands.method.accessors + +import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.argumentSet +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KFunction +import kotlin.reflect.full.valueParameters + +interface TestInterface { + + fun run() { + + } +} + +object TestStatic { + + @JvmStatic + fun run() { + + } +} + +class TestConstructor(arg: Int = 2) + +class TestClass { + + fun run() { + + } + + fun runWithArgs(arg: String) { + + } + + fun runWithUnboxing(boolean: Boolean, byte: Byte, char: Char, short: Short, int: Int, long: Long, float: Float, double: Double) { + + } + + fun runWithDefaults(arg: Int = 2) { + + } + + fun runWithReturnType(): Int { + return 1 + } + + fun runWithReturnTypeWithDefaults(arg: Int = 2): Int { + return arg + } +} + +object ClassFileMethodAccessorGeneratorTest { + + @MethodSource("testCallers") + @ParameterizedTest + fun `Generate method accessors and call them`(instance: Any, function: KFunction<*>, args: List) { + runBlocking { + val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) + methodAccessor.call(buildMap { + args.forEachIndexed { index, arg -> + this[function.valueParameters[index]] = arg + } + }) + } + } + + @JvmStatic + fun testCallers(): List = listOf( + argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), + argumentSet("1-arg method", TestClass(), TestClass::runWithArgs, listOf("foobar")), + argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), + argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), + argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), + ) +} diff --git a/BotCommands-method-accessors/core/build.gradle.kts b/BotCommands-method-accessors/core/build.gradle.kts new file mode 100644 index 000000000..5d7339e34 --- /dev/null +++ b/BotCommands-method-accessors/core/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("BotCommands-conventions") + id("BotCommands-publish-conventions") +} + +dependencies { + api(libs.kotlin.reflect) + api(libs.kotlinx.coroutines.core) +} + +configurePublishedArtifact(artifactId = "BotCommands-method-accessors-core") diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt new file mode 100644 index 000000000..dae88c114 --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt @@ -0,0 +1,9 @@ +package io.github.freya022.botcommands.method.accessors.internal + +import kotlin.reflect.KParameter + +interface MethodAccessor { + + // Return type is not specified due to some intricacies described in KCallable.callSuspendBy + suspend fun call(args: Map) +} diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt new file mode 100644 index 000000000..cba4c075a --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt @@ -0,0 +1,19 @@ +package io.github.freya022.botcommands.method.accessors.internal + +import java.util.* +import kotlin.reflect.KFunction + +interface MethodAccessorFactory { + + // TODO is there a better way to prioritize the classfile implementation? + val priority: Int + + fun create(instance: Any, function: KFunction<*>): MethodAccessor + + companion object { + + fun findAll(): List { + return ServiceLoader.load(MethodAccessorFactory::class.java).toList() + } + } +} diff --git a/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts new file mode 100644 index 000000000..c815467d0 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("BotCommands-conventions") + id("BotCommands-publish-conventions") +} + +dependencies { + implementation(projects.botCommandsMethodAccessors.core) +} + +configurePublishedArtifact(artifactId = "BotCommands-method-accessors-kotlin-reflect") diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt new file mode 100644 index 000000000..e19d1fc09 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -0,0 +1,22 @@ +package dev.freya02.botcommands.method.accessors.internal + +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.full.callSuspendBy +import kotlin.reflect.full.instanceParameter + +internal class KotlinReflectMethodAccessor internal constructor( + private val instance: Any, + private val function: KFunction<*>, +) : MethodAccessor { + + private val instanceParameter = function.instanceParameter + + override suspend fun call(args: Map) { + val args = args.toMutableMap() + if (instanceParameter != null) args.putIfAbsent(instanceParameter, instance) + + function.callSuspendBy(args) + } +} diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt new file mode 100644 index 000000000..6a6c70752 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -0,0 +1,15 @@ +package dev.freya02.botcommands.method.accessors.internal + +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory +import kotlin.reflect.KFunction + +internal class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { + + override val priority: Int = 0 + + override fun create( + instance: Any, + function: KFunction<*>, + ): MethodAccessor = KotlinReflectMethodAccessor(instance, function) +} diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory b/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory new file mode 100644 index 000000000..4ee1ea297 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory @@ -0,0 +1 @@ +dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory diff --git a/settings.gradle.kts b/settings.gradle.kts index a81053e8b..87cd43d43 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,11 @@ rootProject.name = "BotCommands" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":BotCommands-core") +include( + ":BotCommands-method-accessors:core", + ":BotCommands-method-accessors:classfile", + ":BotCommands-method-accessors:kotlin-reflect", +) include(":BotCommands-jda-ktx") include(":jda-ktx-deprecation-processor") include(":spring-properties-processor") From 799ca0d1d139f08abdbe35846671f3fcfb0ebaba Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:06:12 +0200 Subject: [PATCH 02/47] Support static methods --- .../internal/codegen/ClassFileMethodAccessorGenerator.kt | 8 ++++++-- .../accessors/ClassFileMethodAccessorGeneratorTest.kt | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index b3c9d1246..f4fd3a9d7 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -12,6 +12,7 @@ import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles import java.lang.reflect.AccessFlag import java.lang.reflect.Method +import java.lang.reflect.Modifier import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure @@ -107,8 +108,11 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) codeBuilder.castTo(target = parameter.type.jvmErasure.java) } - // TODO other method types could be called - codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + if (Modifier.isStatic(executable.modifiers)) { + codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) + } else { + codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + } // Discard invoked method return value if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 6b5656ea2..522a96dac 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -75,5 +75,6 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), + argumentSet("With static modifier", TestStatic, TestStatic::run, listOf()), ) } From 25433b6b5bb4cb0281e68370ab28b25ee748eb56 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:00:41 +0200 Subject: [PATCH 03/47] Support default arguments --- .../ClassFileMethodAccessorGenerator.kt | 220 +++++++++++++++--- .../ClassFileMethodAccessorGeneratorTest.kt | 2 + 2 files changed, 184 insertions(+), 38 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index f4fd3a9d7..dd5048925 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -5,6 +5,7 @@ import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* +import java.lang.classfile.CodeBuilder import java.lang.classfile.TypeKind import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.* @@ -28,9 +29,6 @@ internal object ClassFileMethodAccessorGenerator { val executable = function.javaExecutable require(executable is Method) { "Constructors are not supported yet" } - val hasOptionals = function.parameters.any { it.isOptional } - require(!hasOptionals) { "Optionals are not supported yet" } - val isSuspend = function.isSuspend require(!isSuspend) { "Suspending functions are not supported yet" } @@ -41,11 +39,6 @@ internal object ClassFileMethodAccessorGenerator { } val instanceDesc = instance.javaClass.describeConstable().get() - val methodTypeDesc = run { - val returnTypeDesc = executable.returnType.describeConstable().get() - val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } - MethodTypeDesc.of(returnTypeDesc, parameterDescs) - } // The class must be unique per function, which is why we don't cache the class // Also "duplicate" definitions are allowed for hidden classes @@ -81,40 +74,13 @@ internal object ClassFileMethodAccessorGenerator { } classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> - val thisSlot = codeBuilder.receiverSlot() - val argsSlot = codeBuilder.parameterSlot(0) val continuationSlot = codeBuilder.parameterSlot(1) - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - - // this.instance.[methodName]([params]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "instance", instanceDesc) - function.parameters.forEachIndexed { index, parameter -> - if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed - - // var parameter = function.getParameters().get([index]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "function", CD_KFunction) - codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) - codeBuilder.loadConstant(index) - codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) - codeBuilder.checkcast(CD_KParameter) - codeBuilder.astore(parameterSlot) - - // = args.get(parameter) - codeBuilder.aload(argsSlot) - codeBuilder.aload(parameterSlot) - codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - codeBuilder.castTo(target = parameter.type.jvmErasure.java) - } - if (Modifier.isStatic(executable.modifiers)) { - codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) + if (function.parameters.any { it.isOptional }) { + writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) } else { - codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + writeInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) } - // Discard invoked method return value - if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() codeBuilder.aload(continuationSlot) codeBuilder.areturn() @@ -128,4 +94,182 @@ internal object ClassFileMethodAccessorGenerator { .getDeclaredConstructor(instance.javaClass, KFunction::class.java) .newInstance(instance, function) as MethodAccessor } + + private fun writeInvokeInstructions( + thisClass: ClassDesc, + instanceDesc: ClassDesc, + function: KFunction<*>, + executable: Method, + codeBuilder: CodeBuilder, + ) { + val methodTypeDesc = run { + val returnTypeDesc = executable.returnType.describeConstable().get() + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + MethodTypeDesc.of(returnTypeDesc, parameterDescs) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // this.instance.[methodName]([params]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + function.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed + + // var parameter = function.getParameters().get([index]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "function", CD_KFunction) + codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) + codeBuilder.loadConstant(index) + codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) + codeBuilder.checkcast(CD_KParameter) + codeBuilder.astore(parameterSlot) + + // = args.get(parameter) + codeBuilder.aload(argsSlot) + codeBuilder.aload(parameterSlot) + codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) + codeBuilder.castTo(target = parameter.type.jvmErasure.java) + } + if (Modifier.isStatic(executable.modifiers)) { + codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) + } else { + codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + } + + // Discard invoked method return value + if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() + } + + private fun writeDefaultInvokeInstructions( + thisClass: ClassDesc, + instanceDesc: ClassDesc, + function: KFunction<*>, + executable: Method, + codeBuilder: CodeBuilder, + ) { + val methodTypeDesc = run { + val returnTypeDesc = executable.returnType.describeConstable().get() + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + MethodTypeDesc.of( + returnTypeDesc, + listOf(instanceDesc) + parameterDescs + listOf(CD_int, CD_Object) + ) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val boxedArgSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) + + // maskSlot = 0 + codeBuilder.iconst_0() + codeBuilder.istore(maskSlot) + + // InstanceClass.[methodName]$default(instance, [params], mask, null) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + var valueParameterIndex = 0 + function.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed + + val paramJavaType = parameter.type.jvmErasure.java + val readyArgSlot = codeBuilder.allocateLocal(TypeKind.from(paramJavaType)) + + // var parameter = function.getParameters().get([index]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "function", CD_KFunction) + codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) + codeBuilder.loadConstant(index) + codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) + codeBuilder.checkcast(CD_KParameter) + codeBuilder.astore(parameterSlot) + + // = args.get(parameter) + codeBuilder.aload(argsSlot) + codeBuilder.aload(parameterSlot) + codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) + if (parameter.isOptional) { + // This will cast only if the value is non-null + codeBuilder.astore(boxedArgSlot) + codeBuilder.orLoadDefaultConstant(inputSlot = boxedArgSlot, type = paramJavaType, outputSlot = readyArgSlot, maskSlot, valueParameterIndex) + codeBuilder.loadLocal(TypeKind.from(paramJavaType), readyArgSlot) + } else { + // Cast non-null value into primitive/ref + codeBuilder.castTo(target = parameter.type.jvmErasure.java) + } + + valueParameterIndex++ + } + codeBuilder.iload(maskSlot) + codeBuilder.aconst_null() + codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) + + // Discard invoked method return value + if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() + } +} + +private fun CodeBuilder.orLoadDefaultConstant( + inputSlot: Int, + type: Class<*>, + outputSlot: Int, + maskSlot: Int, + valueParameterIndex: Int, +) { + val ifNullLabel = newLabel() + val resumeLabel = newLabel() + + aload(inputSlot) + // If stack top value is null then load default + // Here we go to the default loading if null + ifnull(ifNullLabel) + // At this point the value is non-null, set boolean to false, move to if/then/else + // Value is non-null, unbox if necessary + aload(inputSlot) + castTo(type) + storeLocal(TypeKind.from(type), outputSlot) + goto_(resumeLabel) + + labelBinding(ifNullLabel) + // At this point the value is null, set boolean to true, move to if/then/else + // Value is null, load default + // = default_value + when (type) { + Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> { + iconst_0() + istore(outputSlot) + } + + Long::class.javaPrimitiveType -> { + lconst_0() + lstore(outputSlot) + } + + Float::class.javaPrimitiveType -> { + fconst_0() + fstore(outputSlot) + } + + Double::class.javaPrimitiveType -> { + dconst_0() + dstore(outputSlot) + } + + else -> error("Unmatched $type") + } + + // Also set our mask bit so the placeholder gets replaced by the default + // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] + iload(maskSlot) + loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) + ior() + istore(maskSlot) + + labelBinding(resumeLabel) } diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 522a96dac..424087811 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -76,5 +76,7 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), argumentSet("With static modifier", TestStatic, TestStatic::run, listOf()), + argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), + argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), ) } From f063fb2349f4bb422cb7c07751c0bc03c54d6305 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:18:49 +0200 Subject: [PATCH 04/47] Simplify unboxing or loading default value for optional parameters --- .../ClassFileMethodAccessorGenerator.kt | 42 +++++-------------- .../internal/codegen/utils/CodeBuilder.kt | 2 +- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index dd5048925..8121747c5 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -132,7 +132,7 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.aload(argsSlot) codeBuilder.aload(parameterSlot) codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - codeBuilder.castTo(target = parameter.type.jvmErasure.java) + codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } if (Modifier.isStatic(executable.modifiers)) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) @@ -179,7 +179,6 @@ internal object ClassFileMethodAccessorGenerator { if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed val paramJavaType = parameter.type.jvmErasure.java - val readyArgSlot = codeBuilder.allocateLocal(TypeKind.from(paramJavaType)) // var parameter = function.getParameters().get([index]) codeBuilder.aload(thisSlot) @@ -196,12 +195,10 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) if (parameter.isOptional) { // This will cast only if the value is non-null - codeBuilder.astore(boxedArgSlot) - codeBuilder.orLoadDefaultConstant(inputSlot = boxedArgSlot, type = paramJavaType, outputSlot = readyArgSlot, maskSlot, valueParameterIndex) - codeBuilder.loadLocal(TypeKind.from(paramJavaType), readyArgSlot) + codeBuilder.unboxOrLoadDefaultIfNull(paramJavaType, maskSlot, valueParameterIndex) } else { // Cast non-null value into primitive/ref - codeBuilder.castTo(target = parameter.type.jvmErasure.java) + codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } valueParameterIndex++ @@ -215,52 +212,35 @@ internal object ClassFileMethodAccessorGenerator { } } -private fun CodeBuilder.orLoadDefaultConstant( - inputSlot: Int, +private fun CodeBuilder.unboxOrLoadDefaultIfNull( type: Class<*>, - outputSlot: Int, maskSlot: Int, valueParameterIndex: Int, ) { val ifNullLabel = newLabel() val resumeLabel = newLabel() - aload(inputSlot) + dup() // So we can use the reference again after the ifnull // If stack top value is null then load default // Here we go to the default loading if null ifnull(ifNullLabel) // At this point the value is non-null, set boolean to false, move to if/then/else // Value is non-null, unbox if necessary - aload(inputSlot) - castTo(type) - storeLocal(TypeKind.from(type), outputSlot) + unboxOrCastTo(type) goto_(resumeLabel) labelBinding(ifNullLabel) // At this point the value is null, set boolean to true, move to if/then/else // Value is null, load default // = default_value + pop() // We don't need the reference in that branch when (type) { - Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> { + Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> iconst_0() - istore(outputSlot) - } - - Long::class.javaPrimitiveType -> { - lconst_0() - lstore(outputSlot) - } - - Float::class.javaPrimitiveType -> { - fconst_0() - fstore(outputSlot) - } - - Double::class.javaPrimitiveType -> { - dconst_0() - dstore(outputSlot) - } + Long::class.javaPrimitiveType -> lconst_0() + Float::class.javaPrimitiveType -> fconst_0() + Double::class.javaPrimitiveType -> dconst_0() else -> error("Unmatched $type") } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt index aca50c5fc..aea388c0c 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -4,7 +4,7 @@ import java.lang.classfile.CodeBuilder import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc -internal fun CodeBuilder.castTo(target: Class<*>) { +internal fun CodeBuilder.unboxOrCastTo(target: Class<*>) { when (target) { Boolean::class.javaPrimitiveType -> { checkcast(CD_Boolean) From edafc50cee26ef4b4cbdbf5f17f1628f2cddf9bd Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:28:51 +0200 Subject: [PATCH 05/47] Always return `Unit.INSTANCE` in generated `MethodAccessor#call` --- .../internal/codegen/ClassFileMethodAccessorGenerator.kt | 5 ++--- .../method/accessors/internal/codegen/utils/ClassDescs.kt | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 8121747c5..db924499e 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -74,15 +74,14 @@ internal object ClassFileMethodAccessorGenerator { } classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> - val continuationSlot = codeBuilder.parameterSlot(1) - if (function.parameters.any { it.isOptional }) { writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) } else { writeInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) } - codeBuilder.aload(continuationSlot) + // Return Unit as the implemented method has no return type but must return something + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) codeBuilder.areturn() } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 33a8568a9..86a795277 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -7,6 +7,7 @@ import kotlin.reflect.KCallable import kotlin.reflect.KFunction import kotlin.reflect.KParameter +internal val CD_Unit = ClassDesc.of(Unit::class.java.name) internal val CD_Continuation = ClassDesc.of(Continuation::class.java.name) internal val CD_KCallable = ClassDesc.of(KCallable::class.java.name) internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) From 799c58058e112f6493842a3b5eb622e9d6865abb Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:04:49 +0200 Subject: [PATCH 06/47] Refactor ifnull branching out --- .../ClassFileMethodAccessorGenerator.kt | 60 +++++++++---------- .../internal/codegen/utils/CodeBuilder.kt | 20 +++++++ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index db924499e..945668da9 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -216,39 +216,33 @@ private fun CodeBuilder.unboxOrLoadDefaultIfNull( maskSlot: Int, valueParameterIndex: Int, ) { - val ifNullLabel = newLabel() - val resumeLabel = newLabel() - dup() // So we can use the reference again after the ifnull - // If stack top value is null then load default - // Here we go to the default loading if null - ifnull(ifNullLabel) - // At this point the value is non-null, set boolean to false, move to if/then/else - // Value is non-null, unbox if necessary - unboxOrCastTo(type) - goto_(resumeLabel) - - labelBinding(ifNullLabel) - // At this point the value is null, set boolean to true, move to if/then/else - // Value is null, load default - // = default_value - pop() // We don't need the reference in that branch - when (type) { - Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> - iconst_0() - - Long::class.javaPrimitiveType -> lconst_0() - Float::class.javaPrimitiveType -> fconst_0() - Double::class.javaPrimitiveType -> dconst_0() - else -> error("Unmatched $type") - } - - // Also set our mask bit so the placeholder gets replaced by the default - // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] - iload(maskSlot) - loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) - ior() - istore(maskSlot) + ifNull( + onNull = { + // Value is null, load default + // We don't need the reference in that branch + // this is also important to have the same amount of stack data in and out of the branch + pop() + when (type) { + Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> + iconst_0() + + Long::class.javaPrimitiveType -> lconst_0() + Float::class.javaPrimitiveType -> fconst_0() + Double::class.javaPrimitiveType -> dconst_0() + else -> error("Unmatched $type") + } - labelBinding(resumeLabel) + // Also set our mask bit so the placeholder gets replaced by the default + // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] + iload(maskSlot) + loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) + ior() + istore(maskSlot) + }, + onNonNull = { + // Value is non-null, unbox if necessary + unboxOrCastTo(type) + } + ) } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt index aea388c0c..1f71f8997 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -4,6 +4,26 @@ import java.lang.classfile.CodeBuilder import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc +/** + * Consumes the top stack value + */ +internal fun CodeBuilder.ifNull(onNull: () -> Unit, onNonNull: () -> Unit) { + val ifNullLabel = newLabel() + val resumeLabel = newLabel() + + // If stack top value is null then jump + ifnull(ifNullLabel) + // At this point the value is non-null + onNonNull() + goto_(resumeLabel) // Skip null case + + labelBinding(ifNullLabel) + // At this point the value is null + onNull() + + labelBinding(resumeLabel) +} + internal fun CodeBuilder.unboxOrCastTo(target: Class<*>) { when (target) { Boolean::class.javaPrimitiveType -> { From 7e9c74f06bf10da7af7f4873f9fde5a13b76e094 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:05:55 +0200 Subject: [PATCH 07/47] Cleanup --- .../ClassFileMethodAccessorGenerator.kt | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 945668da9..e7818bd60 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -119,13 +119,7 @@ internal object ClassFileMethodAccessorGenerator { if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed // var parameter = function.getParameters().get([index]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "function", CD_KFunction) - codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) - codeBuilder.loadConstant(index) - codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) - codeBuilder.checkcast(CD_KParameter) - codeBuilder.astore(parameterSlot) + codeBuilder.loadParameter(thisSlot, thisClass, index, parameterSlot) // = args.get(parameter) codeBuilder.aload(argsSlot) @@ -163,7 +157,6 @@ internal object ClassFileMethodAccessorGenerator { val argsSlot = codeBuilder.parameterSlot(0) val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - val boxedArgSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) // maskSlot = 0 @@ -180,13 +173,7 @@ internal object ClassFileMethodAccessorGenerator { val paramJavaType = parameter.type.jvmErasure.java // var parameter = function.getParameters().get([index]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "function", CD_KFunction) - codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) - codeBuilder.loadConstant(index) - codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) - codeBuilder.checkcast(CD_KParameter) - codeBuilder.astore(parameterSlot) + codeBuilder.loadParameter(thisSlot, thisClass, index, parameterSlot) // = args.get(parameter) codeBuilder.aload(argsSlot) @@ -197,7 +184,7 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.unboxOrLoadDefaultIfNull(paramJavaType, maskSlot, valueParameterIndex) } else { // Cast non-null value into primitive/ref - codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) + codeBuilder.unboxOrCastTo(target = paramJavaType) } valueParameterIndex++ @@ -211,6 +198,17 @@ internal object ClassFileMethodAccessorGenerator { } } +private fun CodeBuilder.loadParameter(thisSlot: Int, thisClass: ClassDesc, index: Int, parameterSlot: Int) { + // var parameter = function.getParameters().get([index]) + aload(thisSlot) + getfield(thisClass, "function", CD_KFunction) + invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) + loadConstant(index) + invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) + checkcast(CD_KParameter) + astore(parameterSlot) +} + private fun CodeBuilder.unboxOrLoadDefaultIfNull( type: Class<*>, maskSlot: Int, From 24f01c6d5ee077626d07dcc3af66469952502738 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Aug 2025 13:51:08 +0200 Subject: [PATCH 08/47] Support return values --- .../ClassFileMethodAccessorGenerator.kt | 14 ++++----- .../internal/codegen/utils/CodeBuilder.kt | 30 +++++++++++++++++++ .../accessors/internal/MethodAccessor.kt | 2 +- .../internal/KotlinReflectMethodAccessor.kt | 4 +-- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index e7818bd60..05a981dba 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -80,8 +80,12 @@ internal object ClassFileMethodAccessorGenerator { writeInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) } - // Return Unit as the implemented method has no return type but must return something - codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + // Return value as Object, or return Unit as the implemented method must return something + if (executable.returnType != Void.TYPE) { + codeBuilder.boxIfPrimitive(type = executable.returnType) + } else { + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } codeBuilder.areturn() } } @@ -132,9 +136,6 @@ internal object ClassFileMethodAccessorGenerator { } else { codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) } - - // Discard invoked method return value - if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() } private fun writeDefaultInvokeInstructions( @@ -192,9 +193,6 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.iload(maskSlot) codeBuilder.aconst_null() codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) - - // Discard invoked method return value - if (methodTypeDesc.returnType() != CD_void) codeBuilder.pop() } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt index 1f71f8997..7f46d543f 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -63,3 +63,33 @@ internal fun CodeBuilder.unboxOrCastTo(target: Class<*>) { } } } + +internal fun CodeBuilder.boxIfPrimitive(type: Class<*>) { + when (type) { + Boolean::class.javaPrimitiveType -> { + invokestatic(CD_Boolean, "valueOf", MethodTypeDesc.of(CD_Boolean, CD_boolean)) + } + Byte::class.javaPrimitiveType -> { + invokestatic(CD_Byte, "valueOf", MethodTypeDesc.of(CD_Byte, CD_byte)) + } + Char::class.javaPrimitiveType -> { + invokestatic(CD_Character, "valueOf", MethodTypeDesc.of(CD_Character, CD_char)) + } + Short::class.javaPrimitiveType -> { + invokestatic(CD_Short, "valueOf", MethodTypeDesc.of(CD_Short, CD_short)) + } + Int::class.javaPrimitiveType -> { + invokestatic(CD_Integer, "valueOf", MethodTypeDesc.of(CD_Integer, CD_int)) + } + Long::class.javaPrimitiveType -> { + invokestatic(CD_Long, "valueOf", MethodTypeDesc.of(CD_Long, CD_long)) + } + Float::class.javaPrimitiveType -> { + invokestatic(CD_Float, "valueOf", MethodTypeDesc.of(CD_Float, CD_float)) + } + Double::class.javaPrimitiveType -> { + invokestatic(CD_Double, "valueOf", MethodTypeDesc.of(CD_Double, CD_double)) + } + else -> {} + } +} diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt index dae88c114..0b618cf46 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt @@ -5,5 +5,5 @@ import kotlin.reflect.KParameter interface MethodAccessor { // Return type is not specified due to some intricacies described in KCallable.callSuspendBy - suspend fun call(args: Map) + suspend fun call(args: Map): Any? } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index e19d1fc09..02490fc04 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -13,10 +13,10 @@ internal class KotlinReflectMethodAccessor internal constructor( private val instanceParameter = function.instanceParameter - override suspend fun call(args: Map) { + override suspend fun call(args: Map): Any? { val args = args.toMutableMap() if (instanceParameter != null) args.putIfAbsent(instanceParameter, instance) - function.callSuspendBy(args) + return function.callSuspendBy(args) } } From f3276c73ec88e17b6c79df60f44dc75234c1f1b0 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Aug 2025 21:27:14 +0200 Subject: [PATCH 09/47] Start adding support for coroutines --- .../internal/MethodAccessorContinuation.java | 33 ++++++++++ .../ClassFileMethodAccessorGenerator.kt | 60 +++++++++++++++++-- .../internal/codegen/utils/ClassDescs.kt | 2 + .../ClassFileMethodAccessorGeneratorTest.kt | 53 ++++++++++++++++ 4 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java diff --git a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java new file mode 100644 index 000000000..278cc5dc3 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java @@ -0,0 +1,33 @@ +package dev.freya02.botcommands.method.accessors.internal; + +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.jvm.internal.ContinuationImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MethodAccessorContinuation extends ContinuationImpl { + private final MethodAccessor methodAccessor; + + public Object result; + public int label; + + public MethodAccessorContinuation(@Nullable Continuation completion, MethodAccessor methodAccessor) { + super(completion); + this.methodAccessor = methodAccessor; + } + + @SuppressWarnings("unused") // dynamic call + public boolean isResumeLabel() { + return (label & Integer.MIN_VALUE) != 0; + } + + @Nullable + @Override + @SuppressWarnings("DataFlowIssue") // The next continuation label does not need any data, it will only return the result + protected Object invokeSuspend(@NotNull Object result) { + this.result = result; + this.label |= Integer.MIN_VALUE; + return methodAccessor.call(null, this); + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 05a981dba..d5804c454 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -29,9 +29,6 @@ internal object ClassFileMethodAccessorGenerator { val executable = function.javaExecutable require(executable is Method) { "Constructors are not supported yet" } - val isSuspend = function.isSuspend - require(!isSuspend) { "Suspending functions are not supported yet" } - function.parameters.forEach { parameter -> require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { "Unsupported parameter kind: $parameter" @@ -74,10 +71,17 @@ internal object ClassFileMethodAccessorGenerator { } classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + // TODO would be interesting to see if the ClassFile API can eliminate unused variables + val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + if (function.isSuspend) { + codeBuilder.assignOrCreateContinuation(continuationSlot) + } + if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) + writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) } else { - writeInvokeInstructions(thisClass, instanceDesc, function, executable, codeBuilder) + writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) } // Return value as Object, or return Unit as the implemented method must return something @@ -98,11 +102,54 @@ internal object ClassFileMethodAccessorGenerator { .newInstance(instance, function) as MethodAccessor } + private fun CodeBuilder.assignOrCreateContinuation(continuationSlot: Int) { + val thisSlot = receiverSlot() + val completionSlot = parameterSlot(1) + + block { blockCodeBuilder -> + // if (completion instanceof MethodAccessorContinuation) { ... } + aload(completionSlot) + instanceOf(CD_MethodAccessorContinuation) + ifThen { instanceOfCodeBuilder -> + // continuation = (MethodAccessorContinuation) completion; + instanceOfCodeBuilder.aload(completionSlot) + instanceOfCodeBuilder.checkcast(CD_MethodAccessorContinuation) + instanceOfCodeBuilder.astore(continuationSlot) + + // if (continuation.isResumeLabel()) { ... } + instanceOfCodeBuilder.aload(continuationSlot) + instanceOfCodeBuilder.invokevirtual(CD_MethodAccessorContinuation, "isResumeLabel", MethodTypeDesc.of(CD_boolean)) + instanceOfCodeBuilder.ifThen { isResumeCodeBuilder -> + // continuation.label = continuation.label - Integer.MIN_VALUE + isResumeCodeBuilder.aload(continuationSlot) + isResumeCodeBuilder.dup() // So we can reassign it + isResumeCodeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) + isResumeCodeBuilder.loadConstant(Integer.MIN_VALUE) + isResumeCodeBuilder.isub() + isResumeCodeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) + + // break + instanceOfCodeBuilder.goto_(blockCodeBuilder.breakLabel()) + } + } + + // If we're here, the continuation either isn't ours, or it is (what I assume) a resumed one + // continuation = new MethodAccessorContinuation(completion, this); + new_(CD_MethodAccessorContinuation) + dup() // To assign after + aload(completionSlot) + aload(thisSlot) + invokespecial(CD_MethodAccessorContinuation, INIT_NAME, MethodTypeDesc.of(CD_void, CD_Continuation, CD_MethodAccessor)) + astore(continuationSlot) + } + } + private fun writeInvokeInstructions( thisClass: ClassDesc, instanceDesc: ClassDesc, function: KFunction<*>, executable: Method, + continuationSlot: Int, codeBuilder: CodeBuilder, ) { val methodTypeDesc = run { @@ -131,6 +178,7 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } + if (function.isSuspend) codeBuilder.aload(continuationSlot) if (Modifier.isStatic(executable.modifiers)) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) } else { @@ -143,6 +191,7 @@ internal object ClassFileMethodAccessorGenerator { instanceDesc: ClassDesc, function: KFunction<*>, executable: Method, + continuationSlot: Int, codeBuilder: CodeBuilder, ) { val methodTypeDesc = run { @@ -190,6 +239,7 @@ internal object ClassFileMethodAccessorGenerator { valueParameterIndex++ } + if (function.isSuspend) codeBuilder.aload(continuationSlot) codeBuilder.iload(maskSlot) codeBuilder.aconst_null() codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 86a795277..4b88984a5 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.utils +import dev.freya02.botcommands.method.accessors.internal.MethodAccessorContinuation import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.constant.ClassDesc import kotlin.coroutines.Continuation @@ -14,3 +15,4 @@ internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) internal val CD_KParameter = ClassDesc.of(KParameter::class.java.name) internal val CD_MethodAccessor = ClassDesc.of(MethodAccessor::class.java.name) +internal val CD_MethodAccessorContinuation = ClassDesc.of(MethodAccessorContinuation::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 424087811..1bc02260f 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -51,6 +51,14 @@ class TestClass { fun runWithReturnTypeWithDefaults(arg: Int = 2): Int { return arg } + + suspend fun coRun() { + + } + + suspend fun coRunWithDefaults(int: Int = 2) { + + } } object ClassFileMethodAccessorGeneratorTest { @@ -68,6 +76,47 @@ object ClassFileMethodAccessorGeneratorTest { } } +// @Test +// fun idk() { +// val outerClass = ClassDesc.of(ClassFileMethodAccessorGeneratorTest::class.java.packageName, "Outer") +// val innerClass = ClassDesc.of(ClassFileMethodAccessorGeneratorTest::class.java.packageName, $$"Outer$Inner") +// +// val outerBytes = of().build(outerClass) { classBuilder -> +// classBuilder.with(InnerClassesAttribute.of(InnerClassInfo.of(innerClass, Optional.of(outerClass), Optional.empty(), 0))) +// classBuilder.with(NestMembersAttribute.ofSymbols(innerClass)) +// +// classBuilder.withMethodBody("", MethodTypeDesc.of(CD_void), 0) { codeBuilder -> +// val thisSlot = codeBuilder.receiverSlot() +// +// codeBuilder.aload(thisSlot) +// codeBuilder.invokespecial(CD_Object, "", MethodTypeDesc.of(CD_void)) +// +// codeBuilder.return_() +// } +// } +// val innerBytes = of().build(innerClass) { classBuilder -> +// classBuilder.with(NestHostAttribute.of(outerClass)) +// classBuilder.withField("outer", outerClass, ACC_PRIVATE or ACC_FINAL) +// +// classBuilder.withMethodBody("", MethodTypeDesc.of(CD_void, outerClass), 0) { codeBuilder -> +// val thisSlot = codeBuilder.receiverSlot() +// +// codeBuilder.aload(thisSlot) +// codeBuilder.invokespecial(CD_Object, "", MethodTypeDesc.of(CD_void)) +// +// codeBuilder.return_() +// } +// } +// +// val outerLookup = MethodHandles.lookup().defineHiddenClass(outerBytes, true) +// val outerClazz = outerLookup.lookupClass() +// +// val innerLookup = outerLookup.defineHiddenClass(innerBytes, true, MethodHandles.Lookup.ClassOption.NESTMATE) +// val innerClazz = innerLookup.lookupClass() +// +// println() +// } + @JvmStatic fun testCallers(): List = listOf( argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), @@ -78,5 +127,9 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("With static modifier", TestStatic, TestStatic::run, listOf()), argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), + + argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), + argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), + argumentSet("Suspend with overridden defaults", TestClass(), TestClass::coRunWithDefaults, listOf(3)), ) } From c4362cbaffb6f7747d69bc249758cd4f7a6c3a82 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:57:57 +0200 Subject: [PATCH 10/47] Finish support for coroutines --- .../ClassFileMethodAccessorGenerator.kt | 128 +++++++++++++++--- .../internal/codegen/utils/ClassDescs.kt | 5 + .../ClassFileMethodAccessorGeneratorTest.kt | 11 ++ 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index d5804c454..29c4ace86 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -7,6 +7,7 @@ import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* import java.lang.classfile.CodeBuilder import java.lang.classfile.TypeKind +import java.lang.classfile.instruction.SwitchCase import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc @@ -71,26 +72,11 @@ internal object ClassFileMethodAccessorGenerator { } classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> - // TODO would be interesting to see if the ClassFile API can eliminate unused variables - val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - if (function.isSuspend) { - codeBuilder.assignOrCreateContinuation(continuationSlot) - } - - if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) - } else { - writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) - } - - // Return value as Object, or return Unit as the implemented method must return something - if (executable.returnType != Void.TYPE) { - codeBuilder.boxIfPrimitive(type = executable.returnType) + writeSuspendingCallerInstructions(function, thisClass, instanceDesc, executable, codeBuilder) } else { - codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + writeBlockingCallerInstructions(function, thisClass, instanceDesc, executable, codeBuilder) } - codeBuilder.areturn() } } @@ -102,6 +88,106 @@ internal object ClassFileMethodAccessorGenerator { .newInstance(instance, function) as MethodAccessor } + private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { + if (function.parameters.any { it.isOptional }) { + writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) + } else { + writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) + } + + // Return value as Object, or return Unit as the implemented method must return something + if (executable.returnType != Void.TYPE) { + codeBuilder.boxIfPrimitive(type = executable.returnType) + } else { + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } + codeBuilder.areturn() + } + + private fun writeSuspendingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { + val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val callReturnValueSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + codeBuilder.assignOrCreateContinuation(continuationSlot) + + val firstRunLabel = codeBuilder.newLabel() + val firstResumeLabel = codeBuilder.newLabel() + val defaultResumeLabel = codeBuilder.newLabel() + /** Skips right to the return statement */ + val returnResultLabel = codeBuilder.newLabel() + + // var callReturnValue = continuation.result; + codeBuilder.aload(continuationSlot) + codeBuilder.getfield(CD_MethodAccessorContinuation, "result", CD_Object) + codeBuilder.astore(callReturnValueSlot) + + // switch (continuation.label) { ... } + codeBuilder.aload(continuationSlot) + codeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) + codeBuilder.tableswitch( + defaultResumeLabel, + listOf( + SwitchCase.of(0, firstRunLabel), + SwitchCase.of(1, firstResumeLabel) + ) + ) + + + codeBuilder.labelBinding(firstRunLabel) + // continuation.label = 1 + codeBuilder.aload(continuationSlot) + codeBuilder.iconst_1() + codeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) + + if (function.parameters.any { it.isOptional }) { + writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) + } else { + writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) + } + codeBuilder.astore(callReturnValueSlot) + + // if (callReturnValue == IntrinsicsKt.getCOROUTINE_SUSPENDED()) { ... } + codeBuilder.aload(callReturnValueSlot) + codeBuilder.invokestatic(CD_IntrinsicsKt, "getCOROUTINE_SUSPENDED", MethodTypeDesc.of(CD_Object)) + codeBuilder.if_acmpne(returnResultLabel) // If not COROUTINE_SUSPENDED, go return real value + // At this point the result is equal to COROUTINE_SUSPENDED + // DebugProbesKt.probeCoroutineSuspended(continuation) + codeBuilder.aload(continuationSlot) + codeBuilder.invokestatic(CD_DebugProbesKt, "probeCoroutineSuspended", MethodTypeDesc.of(CD_void, CD_Continuation)) + // return callReturnValue (always COROUTINE_SUSPENDED) + codeBuilder.aload(callReturnValueSlot) + codeBuilder.areturn() + + + codeBuilder.labelBinding(firstResumeLabel) + // After the first suspension point (i.e. the call to the user function), return result + // ResultKt.throwOnFailure(callReturnValue) + codeBuilder.aload(callReturnValueSlot) + codeBuilder.invokestatic(CD_ResultKt, "throwOnFailure", MethodTypeDesc.of(CD_void, CD_Object)) + // return result + codeBuilder.goto_(returnResultLabel) + + + codeBuilder.labelBinding(defaultResumeLabel) + // throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine") + codeBuilder.new_(CD_IllegalStateException) + codeBuilder.dup() + codeBuilder.ldc("call to 'resume' before 'invoke' with coroutine" as java.lang.String) + codeBuilder.invokespecial(CD_IllegalStateException, INIT_NAME, MethodTypeDesc.of(CD_void, CD_String)) + codeBuilder.athrow() + + + codeBuilder.labelBinding(returnResultLabel) + // As per KCallable#callSuspendBy, Unit functions may not return Unit in some cases + if (function.returnType.classifier == Unit::class && !function.returnType.isMarkedNullable) { + // In those cases, force return Unit + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } else { + codeBuilder.aload(callReturnValueSlot) + } + codeBuilder.areturn() + } + private fun CodeBuilder.assignOrCreateContinuation(continuationSlot: Int) { val thisSlot = receiverSlot() val completionSlot = parameterSlot(1) @@ -149,7 +235,7 @@ internal object ClassFileMethodAccessorGenerator { instanceDesc: ClassDesc, function: KFunction<*>, executable: Method, - continuationSlot: Int, + continuationSlot: Int?, codeBuilder: CodeBuilder, ) { val methodTypeDesc = run { @@ -178,7 +264,7 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } - if (function.isSuspend) codeBuilder.aload(continuationSlot) + if (continuationSlot != null) codeBuilder.aload(continuationSlot) if (Modifier.isStatic(executable.modifiers)) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) } else { @@ -191,7 +277,7 @@ internal object ClassFileMethodAccessorGenerator { instanceDesc: ClassDesc, function: KFunction<*>, executable: Method, - continuationSlot: Int, + continuationSlot: Int?, codeBuilder: CodeBuilder, ) { val methodTypeDesc = run { @@ -239,7 +325,7 @@ internal object ClassFileMethodAccessorGenerator { valueParameterIndex++ } - if (function.isSuspend) codeBuilder.aload(continuationSlot) + if (continuationSlot != null) codeBuilder.aload(continuationSlot) codeBuilder.iload(maskSlot) codeBuilder.aconst_null() codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 4b88984a5..4a5f742ed 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -8,11 +8,16 @@ import kotlin.reflect.KCallable import kotlin.reflect.KFunction import kotlin.reflect.KParameter +internal val CD_IllegalStateException = ClassDesc.of(IllegalStateException::class.java.name) + internal val CD_Unit = ClassDesc.of(Unit::class.java.name) internal val CD_Continuation = ClassDesc.of(Continuation::class.java.name) internal val CD_KCallable = ClassDesc.of(KCallable::class.java.name) internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) internal val CD_KParameter = ClassDesc.of(KParameter::class.java.name) +internal val CD_ResultKt = ClassDesc.of("kotlin.ResultKt") +internal val CD_IntrinsicsKt = ClassDesc.of("kotlin.coroutines.intrinsics.IntrinsicsKt") +internal val CD_DebugProbesKt = ClassDesc.of("kotlin.coroutines.jvm.internal.DebugProbesKt") internal val CD_MethodAccessor = ClassDesc.of(MethodAccessor::class.java.name) internal val CD_MethodAccessorContinuation = ClassDesc.of(MethodAccessorContinuation::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 1bc02260f..83d1c1b23 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -1,6 +1,7 @@ package dev.freya02.botcommands.method.accessors import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -8,6 +9,7 @@ import org.junit.jupiter.params.provider.Arguments.argumentSet import org.junit.jupiter.params.provider.MethodSource import kotlin.reflect.KFunction import kotlin.reflect.full.valueParameters +import kotlin.time.Duration.Companion.milliseconds interface TestInterface { @@ -59,6 +61,14 @@ class TestClass { suspend fun coRunWithDefaults(int: Int = 2) { } + + suspend fun coRunWithSuspensionPoints(a: Int, b: Int): Int { + delay(10.milliseconds) + val c = a + b + delay(10.milliseconds) + println("$a + $b = $c") + return c + } } object ClassFileMethodAccessorGeneratorTest { @@ -131,5 +141,6 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), argumentSet("Suspend with overridden defaults", TestClass(), TestClass::coRunWithDefaults, listOf(3)), + argumentSet("Suspend with suspension points", TestClass(), TestClass::coRunWithSuspensionPoints, listOf(1, 1)), ) } From 230aaa81392135c4156ae13f87cc91ea09ecc7d2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:31:23 +0200 Subject: [PATCH 11/47] Use Map#containsKey to determine if an optional parameter was set As null values are allowed --- .../ClassFileMethodAccessorGenerator.kt | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 29c4ace86..afdf1ff4e 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -311,14 +311,13 @@ internal object ClassFileMethodAccessorGenerator { // var parameter = function.getParameters().get([index]) codeBuilder.loadParameter(thisSlot, thisClass, index, parameterSlot) - // = args.get(parameter) - codeBuilder.aload(argsSlot) - codeBuilder.aload(parameterSlot) - codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) if (parameter.isOptional) { - // This will cast only if the value is non-null - codeBuilder.unboxOrLoadDefaultIfNull(paramJavaType, maskSlot, valueParameterIndex) + codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, parameterSlot, maskSlot, valueParameterIndex) } else { + // = args.get(parameter) + codeBuilder.aload(argsSlot) + codeBuilder.aload(parameterSlot) + codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) // Cast non-null value into primitive/ref codeBuilder.unboxOrCastTo(target = paramJavaType) } @@ -343,18 +342,30 @@ private fun CodeBuilder.loadParameter(thisSlot: Int, thisClass: ClassDesc, index astore(parameterSlot) } -private fun CodeBuilder.unboxOrLoadDefaultIfNull( +private fun CodeBuilder.loadUnboxedOptional( type: Class<*>, + argsSlot: Int, + parameterSlot: Int, maskSlot: Int, valueParameterIndex: Int, ) { - dup() // So we can use the reference again after the ifnull - ifNull( - onNull = { - // Value is null, load default - // We don't need the reference in that branch - // this is also important to have the same amount of stack data in and out of the branch - pop() + aload(argsSlot) + aload(parameterSlot) + invokeinterface(CD_Map, "containsKey", MethodTypeDesc.of(CD_boolean, CD_Object)) + + // NOTE: Remember to have the same amount of stack data in and out of the branch + ifThenElse( + { + // Key exists, unbox or cast + // <- () args.get(parameter) + aload(argsSlot) + aload(parameterSlot) + invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) + // The value may be null, but null can always be cast to any object type + unboxOrCastTo(type) + }, + { + // Key does not exist, load default when (type) { Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> iconst_0() @@ -371,10 +382,6 @@ private fun CodeBuilder.unboxOrLoadDefaultIfNull( loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) ior() istore(maskSlot) - }, - onNonNull = { - // Value is non-null, unbox if necessary - unboxOrCastTo(type) } ) } From 3a4833a102cac9fa6a4b7e477eefeb7c9af20173 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:32:05 +0200 Subject: [PATCH 12/47] Push `null` if optional parameter is an absent reference --- .../internal/codegen/ClassFileMethodAccessorGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index afdf1ff4e..6a17d2361 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -373,7 +373,7 @@ private fun CodeBuilder.loadUnboxedOptional( Long::class.javaPrimitiveType -> lconst_0() Float::class.javaPrimitiveType -> fconst_0() Double::class.javaPrimitiveType -> dconst_0() - else -> error("Unmatched $type") + else -> aconst_null() } // Also set our mask bit so the placeholder gets replaced by the default From 8428c6d6dbe9839c8fc023a02cf53d8bc24e0ce2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:33:45 +0200 Subject: [PATCH 13/47] More tests --- .../accessors/ClassFileMethodAccessorGeneratorTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 83d1c1b23..6c1b1c5a0 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -46,6 +46,14 @@ class TestClass { } + fun runWithMoreDefaults(a: String, b: Int = 42, c: Double = 3.14159) { + + } + + fun runWithNullOptional(arg: Int? = 2) { + require(arg == null) { "The expected argument was null but the accessor replaced it with a value ($arg)" } + } + fun runWithReturnType(): Int { return 1 } @@ -136,7 +144,9 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), argumentSet("With static modifier", TestStatic, TestStatic::run, listOf()), argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), + argumentSet("With more defaults", TestClass(), TestClass::runWithMoreDefaults, listOf("foobar")), argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), + argumentSet("With optional parameter set to null", TestClass(), TestClass::runWithNullOptional, listOf(null)), argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), From 66d0c57afc819644bd006a09b15c50aff8091d6a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:35:26 +0200 Subject: [PATCH 14/47] Add simple benchmark --- .../classfile/.gitignore | 2 + .../classfile/build.gradle.kts | 9 ++ .../accessors/MethodAccessorBenchmark.kt | 110 ++++++++++++++++++ gradle/libs.versions.toml | 1 + 4 files changed, 122 insertions(+) create mode 100644 BotCommands-method-accessors/classfile/.gitignore create mode 100644 BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt diff --git a/BotCommands-method-accessors/classfile/.gitignore b/BotCommands-method-accessors/classfile/.gitignore new file mode 100644 index 000000000..f9d4ad1cc --- /dev/null +++ b/BotCommands-method-accessors/classfile/.gitignore @@ -0,0 +1,2 @@ +/reports +*.class diff --git a/BotCommands-method-accessors/classfile/build.gradle.kts b/BotCommands-method-accessors/classfile/build.gradle.kts index 4519b930b..690038c91 100644 --- a/BotCommands-method-accessors/classfile/build.gradle.kts +++ b/BotCommands-method-accessors/classfile/build.gradle.kts @@ -3,12 +3,21 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("BotCommands-conventions") id("BotCommands-publish-conventions") + + alias(libs.plugins.jmh) } dependencies { implementation(projects.botCommandsMethodAccessors.core) } +jmh { + // See https://github.com/melix/jmh-gradle-plugin?tab=readme-ov-file#configuration-options + failOnError = true // Should JMH fail immediately if any benchmark had experienced the unrecoverable error? + humanOutputFile = project.file("reports/jmh/human.txt") // human-readable output file + resultsFile = project.file("reports/jmh/results.txt") // results file +} + java { sourceCompatibility = JavaVersion.VERSION_24 targetCompatibility = JavaVersion.VERSION_24 diff --git a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt new file mode 100644 index 000000000..a3c426b93 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -0,0 +1,110 @@ +package dev.freya02.botcommands.method.accessors + +import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import kotlinx.coroutines.runBlocking +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit +import kotlin.reflect.KFunction +import kotlin.reflect.full.callSuspendBy +import kotlin.reflect.full.instanceParameter +import kotlin.reflect.full.valueParameters + +@Suppress("FunctionName") +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +open class MethodAccessorBenchmark { + + private lateinit var instance: MyClass + + private lateinit var simpleMethodAccessor: MethodAccessor + private lateinit var methodWithDefaultsAccessor: MethodAccessor + private lateinit var suspendingMethodWithDefaultsAccessor: MethodAccessor + + private lateinit var simpleMethodKotlin: KFunction<*> + private lateinit var methodWithDefaultsKotlin: KFunction<*> + private lateinit var suspendingMethodWithDefaultsKotlin: KFunction<*> + + @Param("foobar") + lateinit var sampleString: String + + @Setup + fun setup() { + instance = MyClass() + + // The generated accessors won't matter as the benchmark calls 'callSuspendBy' and not 'invoke' + simpleMethodKotlin = MyClass::simpleMethod + methodWithDefaultsKotlin = MyClass::methodWithDefaults + suspendingMethodWithDefaultsKotlin = MyClass::suspendingMethodWithDefaults + + simpleMethodAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::simpleMethod) + methodWithDefaultsAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::methodWithDefaults) + suspendingMethodWithDefaultsAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::suspendingMethodWithDefaults) + } + + @Benchmark + fun simpleMethod_Baseline(): String = runBlocking { + instance.simpleMethod() + } + + @Benchmark + fun methodWithDefaults_Baseline(): String = runBlocking { + instance.methodWithDefaults(sampleString) + } + + @Benchmark + fun suspendingMethodWithDefaults_Baseline(): String = runBlocking { + instance.suspendingMethodWithDefaults(sampleString) + } + + @Benchmark + fun simpleMethod_Accessor(): String = runBlocking { + simpleMethodAccessor.call(mapOf()) as String + } + + @Benchmark + fun methodWithDefaults_Accessor(): String = runBlocking { + methodWithDefaultsAccessor.call(mapOf(methodWithDefaultsKotlin.valueParameters[0] to sampleString)) as String + } + + @Benchmark + fun suspendingMethodWithDefaults_Accessor(): String = runBlocking { + suspendingMethodWithDefaultsAccessor.call(mapOf(suspendingMethodWithDefaultsKotlin.valueParameters[0] to sampleString)) as String + } + + @Benchmark + fun simpleMethod_Kotlin(): String = runBlocking { + val function = simpleMethodKotlin + function.callSuspendBy(mapOf(function.instanceParameter!! to instance)) as String + } + + @Benchmark + fun methodWithDefaults_Kotlin(): String = runBlocking { + val function = methodWithDefaultsKotlin + function.callSuspendBy(mapOf(function.instanceParameter!! to instance, function.valueParameters[0] to sampleString)) as String + } + + @Benchmark + fun suspendingMethodWithDefaults_Kotlin(): String = runBlocking { + val function = suspendingMethodWithDefaultsKotlin + function.callSuspendBy(mapOf(function.instanceParameter!! to instance, function.valueParameters[0] to sampleString)) as String + } + + class MyClass { + + fun simpleMethod(): String { + return "abc" + } + + fun methodWithDefaults(a: String, b: Int = 42, c: Double = 3.14159): String { + return "$a$b$c" + } + + suspend fun suspendingMethodWithDefaults(a: String, b: Int = 42, c: Double = 3.14159): String { + return "$a$b$c" + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31196d895..d94a73cbb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ trove4j-core = "3.1.0" kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } version-catalog-update = "nl.littlerobots.version-catalog-update:1.0.0" +jmh = "me.champeau.jmh:0.7.3" [libraries] bucket4j-jdk17-core = { module = "com.bucket4j:bucket4j_jdk17-core", version.ref = "bucket4j" } From 4dd97a24fcd1905176dee1668be1b6fd6603704b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:09:02 +0200 Subject: [PATCH 15/47] Integrate method accessors --- BotCommands-core/build.gradle.kts | 10 +++++ .../botcommands/api/core/BotCommands.kt | 27 ++++++++++++ .../context/message/MessageCommandInfoImpl.kt | 3 +- .../context/user/UserCommandInfoImpl.kt | 3 +- .../application/slash/SlashCommandInfoImpl.kt | 3 +- .../slash/autocomplete/AutocompleteHandler.kt | 3 +- .../commands/text/TextCommandVariationImpl.kt | 3 +- .../handler/ComponentHandlerExecutor.kt | 5 +-- .../handler/ComponentTimeoutExecutor.kt | 5 +-- .../internal/core/ClassPathFunction.kt | 7 +++- .../core/hooks/EventDispatcherImpl.kt | 21 +++++++--- .../MethodAccessorFactoryProvider.kt | 42 +++++++++++++++++++ .../core/reflection/AggregatorFunction.kt | 3 +- .../core/reflection/MemberFunction.kt | 5 ++- .../internal/modals/ModalHandlerInfo.kt | 3 +- .../internal/MethodAccessorContinuation.java | 4 +- .../ClassFileMethodAccessorFactory.kt | 10 ++--- .../ClassFileMethodAccessorGenerator.kt | 9 ++-- ...d.accessors.internal.FunctionCallerFactory | 1 - .../ExperimentalMethodAccessorsApi.kt | 31 ++++++++++++++ .../accessors/internal/MethodAccessor.kt | 5 +-- .../internal/MethodAccessorFactory.kt | 13 +----- .../internal/KotlinReflectMethodAccessor.kt | 8 ++-- .../KotlinReflectMethodAccessorFactory.kt | 10 ++--- ...d.accessors.internal.FunctionCallerFactory | 1 - 25 files changed, 170 insertions(+), 65 deletions(-) create mode 100644 BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt delete mode 100644 BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory create mode 100644 BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt delete mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory diff --git a/BotCommands-core/build.gradle.kts b/BotCommands-core/build.gradle.kts index abc78c57d..83eee63fb 100644 --- a/BotCommands-core/build.gradle.kts +++ b/BotCommands-core/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { // Classpath scanning api(libs.classgraph) + api(projects.botCommandsMethodAccessors.core) // API due to opt-in annotation + implementation(projects.botCommandsMethodAccessors.kotlinReflect) + implementation(projects.botCommandsMethodAccessors.classfile) + // -------------------- GLOBAL DEPENDENCIES -------------------- api(libs.kotlinx.datetime) @@ -146,6 +150,12 @@ dokka { } } +java { + // ClassFile-based method accessors require Java 24+ + // but the class is conditionally loaded + disableAutoTargetJvm() +} + kotlin { compilerOptions { freeCompilerArgs.addAll( diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt index 96955e6c4..1e4eb3bc3 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt @@ -8,6 +8,7 @@ import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.internal.core.service.BCBotCommandsBootstrap +import io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import io.github.oshai.kotlinlogging.KotlinLogging import kotlin.time.DurationUnit import kotlin.time.measureTimedValue @@ -28,6 +29,32 @@ import kotlin.time.measureTimedValue object BotCommands { private val logger = KotlinLogging.logger { } + /** + * If enabled, instructs the framework to prefer using improved reflection calls, with the following benefits: + * - Shorter stack traces in exceptions and the debugger + * - No [InvocationTargetExceptions][java.lang.reflect.InvocationTargetException] + * - Better performance + * + * This feature requires *running* on Java 24+, if your bot doesn't, this method has no effect. + */ + @ExperimentalMethodAccessorsApi + @get:JvmName("isPreferClassFileAccessors") + var preferClassFileAccessors: Boolean = false + private set + + /** + * Instructs the framework to prefer using improved reflection calls, with the following benefits: + * - Shorter stack traces in exceptions and the debugger + * - No [InvocationTargetExceptions][java.lang.reflect.InvocationTargetException] + * - Better performance + * + * This feature requires *running* on Java 24+, if your bot doesn't, this method has no effect. + */ + @ExperimentalMethodAccessorsApi + fun preferClassFileAccessors() { + preferClassFileAccessors = true + } + /** * Creates a new instance of the framework. * diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt index 4af5d5019..c699b7090 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt @@ -25,7 +25,6 @@ import io.github.freya022.botcommands.internal.utils.* import net.dv8tion.jda.api.interactions.IntegrationType import net.dv8tion.jda.api.interactions.InteractionContextType import net.dv8tion.jda.api.interactions.commands.Command -import kotlin.reflect.full.callSuspendBy internal class MessageCommandInfoImpl internal constructor( override val context: BContext, @@ -67,7 +66,7 @@ internal class MessageCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - function.callSuspendBy(finalParameters) + eventFunction.methodAccessor.call(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt index 0582ba068..16a919b70 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt @@ -25,7 +25,6 @@ import io.github.freya022.botcommands.internal.utils.* import net.dv8tion.jda.api.interactions.IntegrationType import net.dv8tion.jda.api.interactions.InteractionContextType import net.dv8tion.jda.api.interactions.commands.Command -import kotlin.reflect.full.callSuspendBy internal class UserCommandInfoImpl internal constructor( override val context: BContext, @@ -67,7 +66,7 @@ internal class UserCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - function.callSuspendBy(finalParameters) + eventFunction.methodAccessor.call(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt index d01ae00db..7f4099d14 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt @@ -33,7 +33,6 @@ import net.dv8tion.jda.api.interactions.Interaction import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload import net.dv8tion.jda.api.interactions.commands.OptionMapping import kotlin.reflect.KParameter -import kotlin.reflect.full.callSuspendBy import kotlin.reflect.jvm.jvmErasure private val logger = KotlinLogging.logger { } @@ -77,7 +76,7 @@ internal sealed class SlashCommandInfoImpl( internal suspend fun execute(event: GlobalSlashEvent): Boolean { val objects = getSlashOptions(event, parameters) ?: return false - function.callSuspendBy(objects) + eventFunction.methodAccessor.call(objects) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt index be341db6d..f6e658e3d 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt @@ -25,7 +25,6 @@ import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInterac import net.dv8tion.jda.api.interactions.commands.Command import net.dv8tion.jda.api.interactions.commands.OptionType as JDAOptionType import net.dv8tion.jda.api.interactions.commands.build.OptionData -import kotlin.reflect.full.callSuspendBy import kotlin.reflect.jvm.jvmErasure /** @@ -97,7 +96,7 @@ internal class AutocompleteHandler( ?: return emptyList() //Autocomplete was triggered without all the required parameters being present val actualChoices: MutableList = arrayOfSize(25) - val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.function.callSuspendBy(objects)) + val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.eventFunction.methodAccessor.call(objects)) val autoCompleteQuery = event.focusedOption //If something is typed but there are no choices, don't display user input diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt index 5410516ed..32fd56ea3 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt @@ -24,7 +24,6 @@ import io.github.freya022.botcommands.internal.parameters.CustomMethodOption import io.github.freya022.botcommands.internal.parameters.ServiceMethodOption import io.github.freya022.botcommands.internal.utils.* import net.dv8tion.jda.api.events.message.MessageReceivedEvent -import kotlin.reflect.full.callSuspendBy import kotlin.reflect.jvm.jvmErasure internal class TextCommandVariationImpl internal constructor( @@ -90,7 +89,7 @@ internal class TextCommandVariationImpl internal constructor( internal suspend fun execute(event: BaseCommandEvent, optionValues: Map) { val finalParameters = parameters.mapFinalParameters(event, optionValues) - function.callSuspendBy(finalParameters) + eventFunction.methodAccessor.call(finalParameters) } /** diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt index 682efb3ce..ebbfc3736 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt @@ -24,7 +24,6 @@ import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionE import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent import net.dv8tion.jda.api.interactions.components.selections.SelectMenuInteraction -import kotlin.reflect.full.callSuspendBy import kotlin.reflect.jvm.jvmErasure private val logger = KotlinLogging.logger { } @@ -88,7 +87,7 @@ internal class ComponentHandlerExecutor internal constructor( return false } - function.callSuspendBy(parameters.mapFinalParameters(event, optionValues)) + eventFunction.methodAccessor.call(parameters.mapFinalParameters(event, optionValues)) } return true } @@ -135,4 +134,4 @@ internal class ComponentHandlerExecutor internal constructor( return tryInsertNullableOption(value, option, optionMap) } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt index 16afe1d8d..bf37c19b1 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt @@ -21,7 +21,6 @@ import io.github.freya022.botcommands.internal.core.options.OptionType import io.github.freya022.botcommands.internal.parameters.ServiceMethodOption import io.github.freya022.botcommands.internal.utils.* import io.github.oshai.kotlinlogging.KotlinLogging -import kotlin.reflect.full.callSuspendBy private val logger = KotlinLogging.logger { } @@ -79,7 +78,7 @@ internal class ComponentTimeoutExecutor internal constructor( return false } - function.callSuspendBy(parameters.mapFinalParameters(firstArgument, optionValues)) + eventFunction.methodAccessor.call(parameters.mapFinalParameters(firstArgument, optionValues)) } return true } @@ -102,4 +101,4 @@ internal class ComponentTimeoutExecutor internal constructor( return tryInsertNullableOption(value, option, optionMap) } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt index 35ae9598e..0049ab880 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt @@ -1,8 +1,10 @@ package io.github.freya022.botcommands.internal.core import io.github.freya022.botcommands.api.core.service.lazy +import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.utils.FunctionFilter import io.github.freya022.botcommands.internal.utils.ReflectionUtils.asKFunction +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.reflect.Method import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -14,6 +16,7 @@ internal sealed class ClassPathFunction { abstract val instance: Any abstract val function: KFunction<*> + abstract val methodAccessor: MethodAccessor<*> operator fun component1() = instance operator fun component2() = function @@ -37,6 +40,7 @@ internal class LazyClassPathFunction internal constructor( ) : ClassPathFunction() { override val function: KFunction<*> by lazy { method.asKFunction() } override val instance by context.serviceContainer.lazy(clazz) + override val methodAccessor: MethodAccessor<*> by lazy { MethodAccessorFactoryProvider.getAccessorFactory().create(instance, function) } } internal fun ClassPathFunction(context: BContextImpl, clazz: KClass<*>, function: Method): ClassPathFunction { @@ -48,6 +52,7 @@ internal class InstanceClassPathFunction internal constructor( override val function: KFunction<*> ) : ClassPathFunction() { override val clazz: KClass<*> get() = instance::class + override val methodAccessor = MethodAccessorFactoryProvider.getAccessorFactory().create(instance, function) } internal fun Iterable>.toClassPathFunctions(instance: Any) = map { ClassPathFunction(instance, it) } @@ -57,4 +62,4 @@ internal fun ClassPathFunction(instance: Any, function: KFunction<*>): ClassPath } internal fun C.withFilter(filter: FunctionFilter) = this.filter { filter(it.function, false) } -internal fun C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it.function, true) } \ No newline at end of file +internal fun C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it.function, true) } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt index 7812c1d09..d5e8e4789 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt @@ -13,7 +13,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.* import net.dv8tion.jda.api.events.GenericEvent import java.lang.reflect.InvocationTargetException -import kotlin.reflect.full.callSuspend +import kotlin.reflect.KParameter +import kotlin.reflect.full.valueParameters private val logger = KotlinLogging.logger { } @@ -94,19 +95,29 @@ internal class EventDispatcherImpl internal constructor( private suspend fun runEventHandler(eventHandlerFunction: EventHandlerFunction, event: Any) { try { - val (instance, function) = eventHandlerFunction.classPathFunction + val classPathFunction = eventHandlerFunction.classPathFunction + val methodAccessor = classPathFunction.methodAccessor + val args: Map = buildMap { + classPathFunction.function.valueParameters.forEachIndexed { index, param -> + if (index == 0) { + this[param] = event + } else { + this[param] = eventHandlerFunction.parameters[index - 1] + } + } + } val timeout = eventHandlerFunction.timeout if (timeout != null) { // Timeout only works when the continuations implement a cancellation handler val result = withTimeoutOrNull(timeout) { - function.callSuspend(instance, event, *eventHandlerFunction.parameters) + methodAccessor.call(args) } if (result == null) { - logger.debug { "Event listener ${function.shortSignatureNoSrc} timed out" } + logger.debug { "Event listener ${classPathFunction.function.shortSignatureNoSrc} timed out" } } } else { - function.callSuspend(instance, event, *eventHandlerFunction.parameters) + methodAccessor.call(args) } } catch (e: InvocationTargetException) { if (event is InitializationEvent) { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt new file mode 100644 index 000000000..2e38e0471 --- /dev/null +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt @@ -0,0 +1,42 @@ +package io.github.freya022.botcommands.internal.core.method.accessors + +import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory +import io.github.freya022.botcommands.api.core.BotCommands +import io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory +import io.github.oshai.kotlinlogging.KotlinLogging + +internal object MethodAccessorFactoryProvider { + + private var logged = false + + private val kotlinReflectAccessorFactory: MethodAccessorFactory = KotlinReflectMethodAccessorFactory() + private val classFileAccessorFactory: MethodAccessorFactory? by lazy { + if (Runtime.version().feature() >= 24) { + ClassFileMethodAccessorFactory() + } else { + null + } + } + + @OptIn(ExperimentalMethodAccessorsApi::class) + internal fun getAccessorFactory(): MethodAccessorFactory { + fun logUsage(msg: String) { + if (logged) return + synchronized(this) { + if (logged) return + logged = true + } + KotlinLogging.logger { }.info { msg } + } + + return if (BotCommands.preferClassFileAccessors && classFileAccessorFactory != null) { + logUsage("Using ClassFile-based method accessor factory") + classFileAccessorFactory!! + } else { + logUsage("Using kotlin-reflect method accessor factory") + kotlinReflectAccessorFactory + } + } +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index 913cca425..f75381555 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt @@ -60,9 +60,10 @@ internal class AggregatorFunction private constructor( aggregatorArguments[eventParameter] = firstParam } + // TODO replace with MethodAccessor once it supports constructors/static return aggregator.callSuspendBy(aggregatorArguments) } } internal fun KFunction<*>.toAggregatorFunction(context: BContext, firstParamType: KClass<*>) = - AggregatorFunction(context, this, firstParamType) \ No newline at end of file + AggregatorFunction(context, this, firstParamType) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt index 725869a6a..09d2c5a54 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt @@ -1,8 +1,10 @@ package io.github.freya022.botcommands.internal.core.reflection import io.github.freya022.botcommands.internal.core.ClassPathFunction +import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters import io.github.freya022.botcommands.internal.utils.throwInternal +import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import kotlin.reflect.KFunction import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters @@ -12,6 +14,7 @@ internal open class MemberFunction internal constructor( instanceSupplier: () -> Any ) : Function(boundFunction) { val instance by lazy(instanceSupplier) + val methodAccessor: MethodAccessor by lazy { MethodAccessorFactoryProvider.getAccessorFactory().create(instance, kFunction) } val resolvableParameters = kFunction.valueParameters.drop(1) //Drop the first parameter val instanceParameter = kFunction.instanceParameter @@ -20,4 +23,4 @@ internal open class MemberFunction internal constructor( ?: throwInternal(kFunction, "The function should have been checked to have at least one parameter") } -internal fun ClassPathFunction.toMemberFunction() = MemberFunction(function, instanceSupplier = { this.instance }) \ No newline at end of file +internal fun ClassPathFunction.toMemberFunction() = MemberFunction(function, instanceSupplier = { this.instance }) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt index 4f446421a..cb831553f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt @@ -25,7 +25,6 @@ import io.github.freya022.botcommands.internal.parameters.* import io.github.freya022.botcommands.internal.requireUser import io.github.freya022.botcommands.internal.throwUser import io.github.freya022.botcommands.internal.utils.* -import kotlin.reflect.full.callSuspendBy import kotlin.reflect.jvm.jvmErasure internal class ModalHandlerInfo internal constructor( @@ -90,7 +89,7 @@ internal class ModalHandlerInfo internal constructor( throwInternal(::tryInsertOption, "Insertion function shouldn't have been aborted") } - function.callSuspendBy(parameters.mapFinalParameters(event, optionValues)) + eventFunction.methodAccessor.call(parameters.mapFinalParameters(event, optionValues)) } private suspend fun tryInsertOption( diff --git a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java index 278cc5dc3..625d0704d 100644 --- a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java +++ b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java @@ -7,12 +7,12 @@ import org.jetbrains.annotations.Nullable; public class MethodAccessorContinuation extends ContinuationImpl { - private final MethodAccessor methodAccessor; + private final MethodAccessor methodAccessor; public Object result; public int label; - public MethodAccessorContinuation(@Nullable Continuation completion, MethodAccessor methodAccessor) { + public MethodAccessorContinuation(@Nullable Continuation completion, MethodAccessor methodAccessor) { super(completion); this.methodAccessor = methodAccessor; } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt index ad73b7d74..e57575de7 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt @@ -7,16 +7,14 @@ import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFa import java.lang.invoke.MethodHandles import kotlin.reflect.KFunction -internal class ClassFileMethodAccessorFactory : MethodAccessorFactory { +class ClassFileMethodAccessorFactory : MethodAccessorFactory { private val lookup = MethodHandles.lookup() - override val priority: Int get() = 100 - - override fun create( + override fun create( instance: Any, - function: KFunction<*>, - ): MethodAccessor { + function: KFunction, + ): MethodAccessor { val executable = function.javaExecutable require(executable.declaringClass.isAssignableFrom(instance.javaClass)) { "Function is not from the instance's class, function: ${executable.declaringClass.name}, instance: ${instance.javaClass.name}" diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 6a17d2361..bbfa2cc21 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -21,11 +21,11 @@ import kotlin.reflect.jvm.jvmErasure internal object ClassFileMethodAccessorGenerator { - internal fun generate( + internal fun generate( instance: Any, - function: KFunction<*>, + function: KFunction, lookup: MethodHandles.Lookup, - ): MethodAccessor { + ): MethodAccessor { // TODO support constructors? unsure if it will be beneficial for services, they run once, see what's the diff in stack traces val executable = function.javaExecutable require(executable is Method) { "Constructors are not supported yet" } @@ -83,9 +83,10 @@ internal object ClassFileMethodAccessorGenerator { val clazz = lookup .defineHiddenClass(bytes, true) .lookupClass() + @Suppress("UNCHECKED_CAST") return clazz .getDeclaredConstructor(instance.javaClass, KFunction::class.java) - .newInstance(instance, function) as MethodAccessor + .newInstance(instance, function) as MethodAccessor } private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { diff --git a/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory b/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory deleted file mode 100644 index 6663b5197..000000000 --- a/BotCommands-method-accessors/classfile/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory +++ /dev/null @@ -1 +0,0 @@ -dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt new file mode 100644 index 000000000..207d641d3 --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt @@ -0,0 +1,31 @@ +package io.github.freya022.botcommands.method.accessors.api.annotations + +import kotlin.annotation.AnnotationTarget.* + +/** + * Opt-in marker annotation for "method accessors" APIs that are considered experimental and are not subject to compatibility guarantees: + * The behavior of such API may be changed or the API may be removed completely in any further release. + * + * Please create an issue or join the Discord server if you encounter a problem or want to submit feedback. + * + * Any usage of a declaration annotated with `@ExperimentalMethodAccessorsApi` must be accepted either by + * annotating that usage with the [@OptIn][OptIn] annotation, e.g. `@OptIn(ExperimentalMethodAccessorsApi::class)`, + * or by using the compiler argument `-opt-in=io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi`. + */ +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Retention(AnnotationRetention.BINARY) +@Target( + CLASS, + ANNOTATION_CLASS, + PROPERTY, + FIELD, + LOCAL_VARIABLE, + VALUE_PARAMETER, + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + TYPEALIAS +) +@MustBeDocumented +annotation class ExperimentalMethodAccessorsApi diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt index 0b618cf46..590a987a9 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt @@ -2,8 +2,7 @@ package io.github.freya022.botcommands.method.accessors.internal import kotlin.reflect.KParameter -interface MethodAccessor { +interface MethodAccessor { - // Return type is not specified due to some intricacies described in KCallable.callSuspendBy - suspend fun call(args: Map): Any? + suspend fun call(args: Map): R } diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt index cba4c075a..8c987c7f3 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt @@ -1,19 +1,8 @@ package io.github.freya022.botcommands.method.accessors.internal -import java.util.* import kotlin.reflect.KFunction interface MethodAccessorFactory { - // TODO is there a better way to prioritize the classfile implementation? - val priority: Int - - fun create(instance: Any, function: KFunction<*>): MethodAccessor - - companion object { - - fun findAll(): List { - return ServiceLoader.load(MethodAccessorFactory::class.java).toList() - } - } + fun create(instance: Any, function: KFunction): MethodAccessor } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index 02490fc04..6a218d04a 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -6,14 +6,14 @@ import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter -internal class KotlinReflectMethodAccessor internal constructor( +internal class KotlinReflectMethodAccessor internal constructor( private val instance: Any, - private val function: KFunction<*>, -) : MethodAccessor { + private val function: KFunction, +) : MethodAccessor { private val instanceParameter = function.instanceParameter - override suspend fun call(args: Map): Any? { + override suspend fun call(args: Map): R { val args = args.toMutableMap() if (instanceParameter != null) args.putIfAbsent(instanceParameter, instance) diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt index 6a6c70752..1c5c33af4 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -4,12 +4,10 @@ import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory import kotlin.reflect.KFunction -internal class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { +class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { - override val priority: Int = 0 - - override fun create( + override fun create( instance: Any, - function: KFunction<*>, - ): MethodAccessor = KotlinReflectMethodAccessor(instance, function) + function: KFunction, + ): MethodAccessor = KotlinReflectMethodAccessor(instance, function) } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory b/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory deleted file mode 100644 index 4ee1ea297..000000000 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/resources/META-INF/services/io.github.freya022.botcommands.method.accessors.internal.FunctionCallerFactory +++ /dev/null @@ -1 +0,0 @@ -dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory From 8d8018b31cae669df88a0fc5cd09a2b5e41093e3 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:12:00 +0200 Subject: [PATCH 16/47] Update core module package --- .../io/github/freya022/botcommands/api/core/BotCommands.kt | 2 +- .../freya022/botcommands/internal/core/ClassPathFunction.kt | 2 +- .../core/method/accessors/MethodAccessorFactoryProvider.kt | 4 ++-- .../botcommands/internal/core/reflection/MemberFunction.kt | 2 +- .../botcommands/method/accessors/MethodAccessorBenchmark.kt | 2 +- .../method/accessors/internal/MethodAccessorContinuation.java | 1 - .../accessors/internal/ClassFileMethodAccessorFactory.kt | 2 -- .../internal/codegen/ClassFileMethodAccessorGenerator.kt | 2 +- .../method/accessors/internal/codegen/utils/ClassDescs.kt | 2 +- .../api/annotations/ExperimentalMethodAccessorsApi.kt | 4 ++-- .../botcommands/method/accessors/internal/MethodAccessor.kt | 2 +- .../method/accessors/internal/MethodAccessorFactory.kt | 2 +- .../method/accessors/internal/KotlinReflectMethodAccessor.kt | 1 - .../accessors/internal/KotlinReflectMethodAccessorFactory.kt | 2 -- 14 files changed, 12 insertions(+), 18 deletions(-) rename BotCommands-method-accessors/core/src/main/kotlin/{io/github/freya022 => dev/freya02}/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt (82%) rename BotCommands-method-accessors/core/src/main/kotlin/{io/github/freya022 => dev/freya02}/botcommands/method/accessors/internal/MethodAccessor.kt (65%) rename BotCommands-method-accessors/core/src/main/kotlin/{io/github/freya022 => dev/freya02}/botcommands/method/accessors/internal/MethodAccessorFactory.kt (69%) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt index 1e4eb3bc3..e6128debe 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.api.core +import dev.freya02.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import io.github.freya022.botcommands.api.ReceiverConsumer import io.github.freya022.botcommands.api.commands.annotations.Command import io.github.freya022.botcommands.api.core.config.BConfig @@ -8,7 +9,6 @@ import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.internal.core.service.BCBotCommandsBootstrap -import io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import io.github.oshai.kotlinlogging.KotlinLogging import kotlin.time.DurationUnit import kotlin.time.measureTimedValue diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt index 0049ab880..0e3507389 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/ClassPathFunction.kt @@ -1,10 +1,10 @@ package io.github.freya022.botcommands.internal.core +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import io.github.freya022.botcommands.api.core.service.lazy import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.utils.FunctionFilter import io.github.freya022.botcommands.internal.utils.ReflectionUtils.asKFunction -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.reflect.Method import kotlin.reflect.KClass import kotlin.reflect.KFunction diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt index 2e38e0471..0d1ee69da 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt @@ -1,10 +1,10 @@ package io.github.freya022.botcommands.internal.core.method.accessors +import dev.freya02.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory import dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.MethodAccessorFactory import io.github.freya022.botcommands.api.core.BotCommands -import io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory import io.github.oshai.kotlinlogging.KotlinLogging internal object MethodAccessorFactoryProvider { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt index 09d2c5a54..22630fc6e 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt @@ -1,10 +1,10 @@ package io.github.freya022.botcommands.internal.core.reflection +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import io.github.freya022.botcommands.internal.core.ClassPathFunction import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters import io.github.freya022.botcommands.internal.utils.throwInternal -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import kotlin.reflect.KFunction import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters diff --git a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt index a3c426b93..0f52d9c80 100644 --- a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt +++ b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -1,7 +1,7 @@ package dev.freya02.botcommands.method.accessors import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import kotlinx.coroutines.runBlocking import org.openjdk.jmh.annotations.* import java.util.concurrent.TimeUnit diff --git a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java index 625d0704d..ea50ed80d 100644 --- a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java +++ b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java @@ -1,6 +1,5 @@ package dev.freya02.botcommands.method.accessors.internal; -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor; import kotlin.coroutines.Continuation; import kotlin.coroutines.jvm.internal.ContinuationImpl; import org.jetbrains.annotations.NotNull; diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt index e57575de7..22aabaea5 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt @@ -2,8 +2,6 @@ package dev.freya02.botcommands.method.accessors.internal import dev.freya02.botcommands.method.accessors.internal.codegen.ClassFileMethodAccessorGenerator import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory import java.lang.invoke.MethodHandles import kotlin.reflect.KFunction diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index bbfa2cc21..f6762799a 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -1,8 +1,8 @@ package dev.freya02.botcommands.method.accessors.internal.codegen +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import dev.freya02.botcommands.method.accessors.internal.codegen.utils.* import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* import java.lang.classfile.CodeBuilder diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 4a5f742ed..d313d7437 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -1,7 +1,7 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.utils +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import dev.freya02.botcommands.method.accessors.internal.MethodAccessorContinuation -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import java.lang.constant.ClassDesc import kotlin.coroutines.Continuation import kotlin.reflect.KCallable diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt similarity index 82% rename from BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt rename to BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt index 207d641d3..9ea14373c 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt @@ -1,4 +1,4 @@ -package io.github.freya022.botcommands.method.accessors.api.annotations +package dev.freya02.botcommands.method.accessors.api.annotations import kotlin.annotation.AnnotationTarget.* @@ -10,7 +10,7 @@ import kotlin.annotation.AnnotationTarget.* * * Any usage of a declaration annotated with `@ExperimentalMethodAccessorsApi` must be accepted either by * annotating that usage with the [@OptIn][OptIn] annotation, e.g. `@OptIn(ExperimentalMethodAccessorsApi::class)`, - * or by using the compiler argument `-opt-in=io.github.freya022.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi`. + * or by using the compiler argument `-opt-in=dev.freya02.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi`. */ @RequiresOptIn(level = RequiresOptIn.Level.ERROR) @Retention(AnnotationRetention.BINARY) diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt similarity index 65% rename from BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt rename to BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt index 590a987a9..e29b21407 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt @@ -1,4 +1,4 @@ -package io.github.freya022.botcommands.method.accessors.internal +package dev.freya02.botcommands.method.accessors.internal import kotlin.reflect.KParameter diff --git a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt similarity index 69% rename from BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt rename to BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt index 8c987c7f3..0cba4286b 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/io/github/freya022/botcommands/method/accessors/internal/MethodAccessorFactory.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt @@ -1,4 +1,4 @@ -package io.github.freya022.botcommands.method.accessors.internal +package dev.freya02.botcommands.method.accessors.internal import kotlin.reflect.KFunction diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index 6a218d04a..6367a237a 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -1,6 +1,5 @@ package dev.freya02.botcommands.method.accessors.internal -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt index 1c5c33af4..b387305b4 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -1,7 +1,5 @@ package dev.freya02.botcommands.method.accessors.internal -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessor -import io.github.freya022.botcommands.method.accessors.internal.MethodAccessorFactory import kotlin.reflect.KFunction class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { From 453f604ca6dab15a0beb2b71cdf1500f9766fa73 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:58:53 +0200 Subject: [PATCH 17/47] Add README.md --- BotCommands-method-accessors/README.md | 35 ++++++++++++++++++ .../assets/stack-trace-classfile.avif | Bin 0 -> 15026 bytes .../assets/stack-trace-kotlin-reflect.avif | Bin 0 -> 28122 bytes 3 files changed, 35 insertions(+) create mode 100644 BotCommands-method-accessors/README.md create mode 100644 BotCommands-method-accessors/assets/stack-trace-classfile.avif create mode 100644 BotCommands-method-accessors/assets/stack-trace-kotlin-reflect.avif diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md new file mode 100644 index 000000000..05e1c1d54 --- /dev/null +++ b/BotCommands-method-accessors/README.md @@ -0,0 +1,35 @@ +# BotCommands module - Method accessors +This module provides abstractions to call user methods, it should not be included manually. + +## Implementations + +### kotlin-reflect +This is the default implementation, as is what the framework originally used, +it only delegates to `callSuspendBy` with pairs of parameter -> value. + +### ClassFile-based +This newer implementation takes advantage of [hidden classes](https://www.baeldung.com/java-hidden-classes) and the [Class-File API](https://openjdk.org/jeps/484), +which, for each function, generates a hidden class with instructions optimized to directly call the user method. + +This allows for shorter stack traces in exceptions and the debugger, no `InvocationTargetException`s, and better performance. + +This can be enabled before starting your bot, by calling `BotCommands.preferClassFileAccessors()`, +note that this will only have an effect if your bot runs on Java 24+. + +### Stack trace comparison + +#### kotlin-reflect +![img.png](./assets/stack-trace-kotlin-reflect.avif) + +#### ClassFile +![img_1.png](./assets/stack-trace-classfile.avif) + +### Performance comparison + +Performance numbers from [MethodAccessorBenchmark](./classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt) + +| Function type | Baseline | ClassFile | kotlin-reflect | +|-----------------------------------------------------------------|-------------|-------------|----------------| +| () -> String | 0.110 µs/op | 0.115 µs/op | 0.227 µs/op | +| (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.177 µs/op | 0.287 µs/op | 0.514 µs/op | +| suspend (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.180 µs/op | 0.288 µs/op | 0.535 µs/op | diff --git a/BotCommands-method-accessors/assets/stack-trace-classfile.avif b/BotCommands-method-accessors/assets/stack-trace-classfile.avif new file mode 100644 index 0000000000000000000000000000000000000000..69437d28df75827e77587e65606fd41dbb354974 GIT binary patch literal 15026 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 literal 0 HcmV?d00001 From 3329feeb625766d5033e5524d9a5266b4f6231f9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:00:29 +0200 Subject: [PATCH 18/47] Clean up --- .../ClassFileMethodAccessorGeneratorTest.kt | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 6c1b1c5a0..d1d433ced 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -94,47 +94,6 @@ object ClassFileMethodAccessorGeneratorTest { } } -// @Test -// fun idk() { -// val outerClass = ClassDesc.of(ClassFileMethodAccessorGeneratorTest::class.java.packageName, "Outer") -// val innerClass = ClassDesc.of(ClassFileMethodAccessorGeneratorTest::class.java.packageName, $$"Outer$Inner") -// -// val outerBytes = of().build(outerClass) { classBuilder -> -// classBuilder.with(InnerClassesAttribute.of(InnerClassInfo.of(innerClass, Optional.of(outerClass), Optional.empty(), 0))) -// classBuilder.with(NestMembersAttribute.ofSymbols(innerClass)) -// -// classBuilder.withMethodBody("", MethodTypeDesc.of(CD_void), 0) { codeBuilder -> -// val thisSlot = codeBuilder.receiverSlot() -// -// codeBuilder.aload(thisSlot) -// codeBuilder.invokespecial(CD_Object, "", MethodTypeDesc.of(CD_void)) -// -// codeBuilder.return_() -// } -// } -// val innerBytes = of().build(innerClass) { classBuilder -> -// classBuilder.with(NestHostAttribute.of(outerClass)) -// classBuilder.withField("outer", outerClass, ACC_PRIVATE or ACC_FINAL) -// -// classBuilder.withMethodBody("", MethodTypeDesc.of(CD_void, outerClass), 0) { codeBuilder -> -// val thisSlot = codeBuilder.receiverSlot() -// -// codeBuilder.aload(thisSlot) -// codeBuilder.invokespecial(CD_Object, "", MethodTypeDesc.of(CD_void)) -// -// codeBuilder.return_() -// } -// } -// -// val outerLookup = MethodHandles.lookup().defineHiddenClass(outerBytes, true) -// val outerClazz = outerLookup.lookupClass() -// -// val innerLookup = outerLookup.defineHiddenClass(innerBytes, true, MethodHandles.Lookup.ClassOption.NESTMATE) -// val innerClazz = innerLookup.lookupClass() -// -// println() -// } - @JvmStatic fun testCallers(): List = listOf( argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), From 4e4cb65e177679682ef45e4bec30a67f374459d4 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:00:48 +0200 Subject: [PATCH 19/47] Use `BotCommands.preferClassFileAccessors()` --- .../test/kotlin/io/github/freya022/botcommands/test/Main.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/Main.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/Main.kt index b16c97ac3..c95cf44f5 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/Main.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/Main.kt @@ -1,6 +1,7 @@ package io.github.freya022.botcommands.test import ch.qos.logback.classic.ClassicConstants +import dev.freya02.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import dev.reformator.stacktracedecoroutinator.jvm.DecoroutinatorJvmApi import io.github.freya022.botcommands.api.core.BotCommands import io.github.freya022.botcommands.api.core.config.DevConfig @@ -36,6 +37,9 @@ object Main { DecoroutinatorJvmApi.install() } + @OptIn(ExperimentalMethodAccessorsApi::class) + BotCommands.preferClassFileAccessors() + BotCommands.create { disableExceptionsInDMs = true From de666d0419941a4e7d45a735ccb42d69f0299117 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:02:06 +0200 Subject: [PATCH 20/47] Support static methods with `null` instances --- .../ClassFileMethodAccessorFactory.kt | 10 +++- .../ClassFileMethodAccessorGenerator.kt | 60 +++++++++++++------ .../ClassFileMethodAccessorGeneratorTest.kt | 5 +- .../internal/MethodAccessorFactory.kt | 2 +- .../internal/KotlinReflectMethodAccessor.kt | 4 +- .../KotlinReflectMethodAccessorFactory.kt | 13 +++- .../KotlinReflectStaticMethodAccessor.kt | 14 +++++ 7 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt index 22aabaea5..3f92f8bb3 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/ClassFileMethodAccessorFactory.kt @@ -4,18 +4,22 @@ import dev.freya02.botcommands.method.accessors.internal.codegen.ClassFileMethod import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable import java.lang.invoke.MethodHandles import kotlin.reflect.KFunction +import kotlin.reflect.full.instanceParameter class ClassFileMethodAccessorFactory : MethodAccessorFactory { private val lookup = MethodHandles.lookup() override fun create( - instance: Any, + instance: Any?, function: KFunction, ): MethodAccessor { val executable = function.javaExecutable - require(executable.declaringClass.isAssignableFrom(instance.javaClass)) { - "Function is not from the instance's class, function: ${executable.declaringClass.name}, instance: ${instance.javaClass.name}" + if (function.instanceParameter != null) { + requireNotNull(instance) + require(executable.declaringClass.isAssignableFrom(instance.javaClass)) { + "Function is not from the instance's class, function: ${executable.declaringClass.name}, instance: ${instance.javaClass.name}" + } } return ClassFileMethodAccessorGenerator.generate(instance, function, lookup) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index f6762799a..ff7089ba8 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -22,7 +22,7 @@ import kotlin.reflect.jvm.jvmErasure internal object ClassFileMethodAccessorGenerator { internal fun generate( - instance: Any, + instance: Any?, function: KFunction, lookup: MethodHandles.Lookup, ): MethodAccessor { @@ -36,7 +36,9 @@ internal object ClassFileMethodAccessorGenerator { } } - val instanceDesc = instance.javaClass.describeConstable().get() + val instanceClass = executable.declaringClass + val instanceDesc = instanceClass.describeConstable().get() + val isStatic = Modifier.isStatic(executable.modifiers) // The class must be unique per function, which is why we don't cache the class // Also "duplicate" definitions are allowed for hidden classes @@ -46,22 +48,28 @@ internal object ClassFileMethodAccessorGenerator { classBuilder.withInterfaceSymbols(CD_MethodAccessor) // TODO replace with class data of hidden class - classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) + if (!isStatic) classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) - classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + val ctorType = when { + isStatic -> MethodTypeDesc.of(CD_void, CD_KFunction) + else -> MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction) + } + classBuilder.withMethodBody(INIT_NAME, ctorType, ACC_PUBLIC) { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() - val instanceSlot = codeBuilder.parameterSlot(0) - val functionSlot = codeBuilder.parameterSlot(1) + val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) // this.super() codeBuilder.aload(thisSlot) codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) - // this.instance = instance; - codeBuilder.aload(thisSlot) - codeBuilder.aload(instanceSlot) - codeBuilder.putfield(thisClass, "instance", instanceDesc) + if (!isStatic) { + // this.instance = instance; + val instanceSlot = codeBuilder.parameterSlot(0) + codeBuilder.aload(thisSlot) + codeBuilder.aload(instanceSlot) + codeBuilder.putfield(thisClass, "instance", instanceDesc) + } // this.function = function; codeBuilder.aload(thisSlot) @@ -84,9 +92,15 @@ internal object ClassFileMethodAccessorGenerator { .defineHiddenClass(bytes, true) .lookupClass() @Suppress("UNCHECKED_CAST") - return clazz - .getDeclaredConstructor(instance.javaClass, KFunction::class.java) - .newInstance(instance, function) as MethodAccessor + return if (isStatic) { + clazz + .getDeclaredConstructor(KFunction::class.java) + .newInstance(function) as MethodAccessor + } else { + clazz + .getDeclaredConstructor(instanceClass, KFunction::class.java) + .newInstance(instance, function) as MethodAccessor + } } private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { @@ -239,6 +253,7 @@ internal object ClassFileMethodAccessorGenerator { continuationSlot: Int?, codeBuilder: CodeBuilder, ) { + val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { val returnTypeDesc = executable.returnType.describeConstable().get() val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } @@ -251,8 +266,10 @@ internal object ClassFileMethodAccessorGenerator { val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) // this.instance.[methodName]([params]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "instance", instanceDesc) + if (!isStatic) { + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + } function.parameters.forEachIndexed { index, parameter -> if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed @@ -266,8 +283,10 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } if (continuationSlot != null) codeBuilder.aload(continuationSlot) - if (Modifier.isStatic(executable.modifiers)) { + if (isStatic) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) + } else if (executable.declaringClass.isInterface) { + codeBuilder.invokeinterface(instanceDesc, executable.name, methodTypeDesc) } else { codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) } @@ -281,12 +300,13 @@ internal object ClassFileMethodAccessorGenerator { continuationSlot: Int?, codeBuilder: CodeBuilder, ) { + val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { val returnTypeDesc = executable.returnType.describeConstable().get() val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } MethodTypeDesc.of( returnTypeDesc, - listOf(instanceDesc) + parameterDescs + listOf(CD_int, CD_Object) + (if (isStatic) listOf() else listOf(instanceDesc)) + parameterDescs + listOf(CD_int, CD_Object) ) } @@ -301,8 +321,10 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.istore(maskSlot) // InstanceClass.[methodName]$default(instance, [params], mask, null) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "instance", instanceDesc) + if (!isStatic) { + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + } var valueParameterIndex = 0 function.parameters.forEachIndexed { index, parameter -> if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index d1d433ced..faf7b9f10 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -83,7 +83,7 @@ object ClassFileMethodAccessorGeneratorTest { @MethodSource("testCallers") @ParameterizedTest - fun `Generate method accessors and call them`(instance: Any, function: KFunction<*>, args: List) { + fun `Generate method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { runBlocking { val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) methodAccessor.call(buildMap { @@ -101,7 +101,8 @@ object ClassFileMethodAccessorGeneratorTest { argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), - argumentSet("With static modifier", TestStatic, TestStatic::run, listOf()), + argumentSet("With static modifier", null, TestStatic::run, listOf()), + argumentSet("With static modifier and instance", TestStatic, TestStatic::run, listOf()), argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), argumentSet("With more defaults", TestClass(), TestClass::runWithMoreDefaults, listOf("foobar")), argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), diff --git a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt index 0cba4286b..875000219 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt @@ -4,5 +4,5 @@ import kotlin.reflect.KFunction interface MethodAccessorFactory { - fun create(instance: Any, function: KFunction): MethodAccessor + fun create(instance: Any?, function: KFunction): MethodAccessor } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index 6367a237a..ae0b30728 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -10,11 +10,11 @@ internal class KotlinReflectMethodAccessor internal constructor( private val function: KFunction, ) : MethodAccessor { - private val instanceParameter = function.instanceParameter + private val instanceParameter = function.instanceParameter!! override suspend fun call(args: Map): R { val args = args.toMutableMap() - if (instanceParameter != null) args.putIfAbsent(instanceParameter, instance) + args.putIfAbsent(instanceParameter, instance) return function.callSuspendBy(args) } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt index b387305b4..10c32cde8 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -1,11 +1,20 @@ package dev.freya02.botcommands.method.accessors.internal import kotlin.reflect.KFunction +import kotlin.reflect.full.instanceParameter class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { override fun create( - instance: Any, + instance: Any?, function: KFunction, - ): MethodAccessor = KotlinReflectMethodAccessor(instance, function) + ): MethodAccessor { + return if (function.instanceParameter != null) { + requireNotNull(instance) + + KotlinReflectMethodAccessor(instance, function) + } else { + KotlinReflectStaticMethodAccessor(function) + } + } } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt new file mode 100644 index 000000000..5ea632469 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt @@ -0,0 +1,14 @@ +package dev.freya02.botcommands.method.accessors.internal + +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.full.callSuspendBy + +internal class KotlinReflectStaticMethodAccessor internal constructor( + private val function: KFunction, +) : MethodAccessor { + + override suspend fun call(args: Map): R { + return function.callSuspendBy(args) + } +} From e22c4c4598f8152c6d28349f59e2be3d34888ada Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:29:13 +0200 Subject: [PATCH 21/47] Support constructors --- .../ClassFileMethodAccessorGenerator.kt | 65 +++++++++++-------- .../internal/codegen/utils/ClassDescs.kt | 1 + .../ClassFileMethodAccessorGeneratorTest.kt | 6 +- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index ff7089ba8..37a80f996 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -12,9 +12,7 @@ import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles -import java.lang.reflect.AccessFlag -import java.lang.reflect.Method -import java.lang.reflect.Modifier +import java.lang.reflect.* import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure @@ -26,16 +24,13 @@ internal object ClassFileMethodAccessorGenerator { function: KFunction, lookup: MethodHandles.Lookup, ): MethodAccessor { - // TODO support constructors? unsure if it will be beneficial for services, they run once, see what's the diff in stack traces - val executable = function.javaExecutable - require(executable is Method) { "Constructors are not supported yet" } - function.parameters.forEach { parameter -> require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { "Unsupported parameter kind: $parameter" } } + val executable = function.javaExecutable val instanceClass = executable.declaringClass val instanceDesc = instanceClass.describeConstable().get() val isStatic = Modifier.isStatic(executable.modifiers) @@ -103,23 +98,27 @@ internal object ClassFileMethodAccessorGenerator { } } - private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { + private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Executable, codeBuilder: CodeBuilder) { if (function.parameters.any { it.isOptional }) { writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) } else { writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) } - // Return value as Object, or return Unit as the implemented method must return something - if (executable.returnType != Void.TYPE) { - codeBuilder.boxIfPrimitive(type = executable.returnType) - } else { - codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + if (executable is Method) { + // Return value as Object, or return Unit as the implemented method must return something + if (executable.returnType != Void.TYPE) { + codeBuilder.boxIfPrimitive(type = executable.returnType) + } else { + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } } codeBuilder.areturn() } - private fun writeSuspendingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Method, codeBuilder: CodeBuilder) { + private fun writeSuspendingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Executable, codeBuilder: CodeBuilder) { + require(executable is Method) + val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val callReturnValueSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) @@ -249,13 +248,13 @@ internal object ClassFileMethodAccessorGenerator { thisClass: ClassDesc, instanceDesc: ClassDesc, function: KFunction<*>, - executable: Method, + executable: Executable, continuationSlot: Int?, codeBuilder: CodeBuilder, ) { val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { - val returnTypeDesc = executable.returnType.describeConstable().get() + val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } MethodTypeDesc.of(returnTypeDesc, parameterDescs) } @@ -266,7 +265,10 @@ internal object ClassFileMethodAccessorGenerator { val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) // this.instance.[methodName]([params]) - if (!isStatic) { + if (executable is Constructor<*>) { + codeBuilder.new_(instanceDesc) + codeBuilder.dup() // So we can return it + } else if (!isStatic) { codeBuilder.aload(thisSlot) codeBuilder.getfield(thisClass, "instance", instanceDesc) } @@ -283,7 +285,9 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) } if (continuationSlot != null) codeBuilder.aload(continuationSlot) - if (isStatic) { + if (executable is Constructor<*>) { + codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) + } else if (isStatic) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) } else if (executable.declaringClass.isInterface) { codeBuilder.invokeinterface(instanceDesc, executable.name, methodTypeDesc) @@ -296,18 +300,20 @@ internal object ClassFileMethodAccessorGenerator { thisClass: ClassDesc, instanceDesc: ClassDesc, function: KFunction<*>, - executable: Method, + executable: Executable, continuationSlot: Int?, codeBuilder: CodeBuilder, ) { val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { - val returnTypeDesc = executable.returnType.describeConstable().get() + val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } - MethodTypeDesc.of( - returnTypeDesc, - (if (isStatic) listOf() else listOf(instanceDesc)) + parameterDescs + listOf(CD_int, CD_Object) - ) + val effectiveParameters = when { + executable is Constructor<*> -> parameterDescs + listOf(CD_int, CD_DefaultConstructorMarker) + isStatic -> parameterDescs + listOf(CD_int, CD_Object) + else -> listOf(instanceDesc) + parameterDescs + listOf(CD_int, CD_Object) + } + MethodTypeDesc.of(returnTypeDesc, effectiveParameters) } val thisSlot = codeBuilder.receiverSlot() @@ -321,7 +327,10 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.istore(maskSlot) // InstanceClass.[methodName]$default(instance, [params], mask, null) - if (!isStatic) { + if (executable is Constructor<*>) { + codeBuilder.new_(instanceDesc) + codeBuilder.dup() // So we can return it + } else if (!isStatic) { codeBuilder.aload(thisSlot) codeBuilder.getfield(thisClass, "instance", instanceDesc) } @@ -350,7 +359,11 @@ internal object ClassFileMethodAccessorGenerator { if (continuationSlot != null) codeBuilder.aload(continuationSlot) codeBuilder.iload(maskSlot) codeBuilder.aconst_null() - codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) + if (executable is Constructor<*>) { + codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) + } else { + codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) + } } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index d313d7437..0ae2d8445 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -11,6 +11,7 @@ import kotlin.reflect.KParameter internal val CD_IllegalStateException = ClassDesc.of(IllegalStateException::class.java.name) internal val CD_Unit = ClassDesc.of(Unit::class.java.name) +internal val CD_DefaultConstructorMarker = ClassDesc.of("kotlin.jvm.internal.DefaultConstructorMarker") internal val CD_Continuation = ClassDesc.of(Continuation::class.java.name) internal val CD_KCallable = ClassDesc.of(KCallable::class.java.name) internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index faf7b9f10..2e5d9961e 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -26,7 +26,9 @@ object TestStatic { } } -class TestConstructor(arg: Int = 2) +class TestConstructor(arg: Int) + +class TestConstructorWithDefaults(arg: Int = 2) class TestClass { @@ -98,6 +100,8 @@ object ClassFileMethodAccessorGeneratorTest { fun testCallers(): List = listOf( argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), argumentSet("1-arg method", TestClass(), TestClass::runWithArgs, listOf("foobar")), + argumentSet("1-arg constructor", null, ::TestConstructor, listOf(1)), + argumentSet("Constructor with defaults", null, ::TestConstructorWithDefaults, listOf()), argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), From a893c98786f704f707aa6052844200533d910bef Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:34:43 +0200 Subject: [PATCH 22/47] Use method accessors when calling aggregators --- .../core/reflection/AggregatorFunction.kt | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index f75381555..174ddd0af 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt @@ -4,18 +4,15 @@ import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.utils.isConstructor import io.github.freya022.botcommands.api.core.utils.isStatic import io.github.freya022.botcommands.api.core.utils.isSubclassOf -import io.github.freya022.botcommands.api.core.utils.simpleNestedName +import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.core.options.builder.InternalAggregators.isSingleAggregator import io.github.freya022.botcommands.internal.core.service.getFunctionServiceOrNull import io.github.freya022.botcommands.internal.utils.ReflectionUtils.declaringClass import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters import io.github.freya022.botcommands.internal.utils.checkAt -import io.github.freya022.botcommands.internal.utils.throwInternal import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter -import kotlin.reflect.full.callSuspendBy -import kotlin.reflect.full.instanceParameter import kotlin.reflect.jvm.jvmErasure internal class AggregatorFunction private constructor( @@ -37,9 +34,9 @@ internal class AggregatorFunction private constructor( } } - private val instanceParameter = aggregator.instanceParameter private val eventParameter = aggregator.nonInstanceParameters.first().takeIf { it.type.jvmErasure.isSubclassOf(firstParamType) } + internal val methodAccessor = MethodAccessorFactoryProvider.getAccessorFactory().create(aggregatorInstance, kFunction) internal val aggregator get() = this.kFunction internal val isSingleAggregator get() = aggregator.isSingleAggregator() @@ -51,17 +48,11 @@ internal class AggregatorFunction private constructor( ) : this(aggregator, context.serviceContainer.getFunctionServiceOrNull(aggregator), firstParamType) internal suspend fun aggregate(firstParam: Any, aggregatorArguments: MutableMap): Any? { - if (instanceParameter != null) { - aggregatorArguments[instanceParameter] = aggregatorInstance - ?: throwInternal(aggregator, "Aggregator's instance parameter (${instanceParameter.type.jvmErasure.simpleNestedName}) was not retrieved but was necessary") - } - if (eventParameter != null) { aggregatorArguments[eventParameter] = firstParam } - // TODO replace with MethodAccessor once it supports constructors/static - return aggregator.callSuspendBy(aggregatorArguments) + return methodAccessor.call(aggregatorArguments) } } From be9d8f3a569de3e398bf86380878c05fe487d08a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:52:36 +0200 Subject: [PATCH 23/47] Resize stack trace comparisons, update `alt` --- BotCommands-method-accessors/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md index 05e1c1d54..2c7ab0585 100644 --- a/BotCommands-method-accessors/README.md +++ b/BotCommands-method-accessors/README.md @@ -19,10 +19,12 @@ note that this will only have an effect if your bot runs on Java 24+. ### Stack trace comparison #### kotlin-reflect -![img.png](./assets/stack-trace-kotlin-reflect.avif) + +Stack trace of kotlin-reflect call #### ClassFile -![img_1.png](./assets/stack-trace-classfile.avif) + +Stack trace of custom caller ### Performance comparison From c461c95312c20a01c4d9ac7e066a5e1dab2e9831 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 21:33:32 +0200 Subject: [PATCH 24/47] Refactor ClassFileMethodAccessorGenerator to use properties instead of passing values --- .../ClassFileMethodAccessorGenerator.kt | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 37a80f996..5a849652d 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -12,32 +12,30 @@ import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles -import java.lang.reflect.* +import java.lang.reflect.AccessFlag +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import java.lang.reflect.Modifier import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure -internal object ClassFileMethodAccessorGenerator { +internal class ClassFileMethodAccessorGenerator private constructor( + private val instance: Any?, + private val function: KFunction, + private val lookup: MethodHandles.Lookup, +) { - internal fun generate( - instance: Any?, - function: KFunction, - lookup: MethodHandles.Lookup, - ): MethodAccessor { - function.parameters.forEach { parameter -> - require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { - "Unsupported parameter kind: $parameter" - } - } + private val executable = function.javaExecutable + private val instanceClass = executable.declaringClass + private val instanceDesc = instanceClass.describeConstable().get() + private val isStatic = Modifier.isStatic(executable.modifiers) - val executable = function.javaExecutable - val instanceClass = executable.declaringClass - val instanceDesc = instanceClass.describeConstable().get() - val isStatic = Modifier.isStatic(executable.modifiers) + private val thisClass = ClassDesc.of("${lookup.lookupClass().packageName}.ClassFileMethodAccessor") - // The class must be unique per function, which is why we don't cache the class - // Also "duplicate" definitions are allowed for hidden classes - val thisClass = ClassDesc.of("${lookup.lookupClass().packageName}.ClassFileMethodAccessor") + // The class must be unique per function, which is why we don't cache the class + // Also "duplicate" definitions are allowed for hidden classes + private fun generate(): MethodAccessor { val bytes = ClassFile.of().build(thisClass) { classBuilder -> classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) classBuilder.withInterfaceSymbols(CD_MethodAccessor) @@ -76,9 +74,9 @@ internal object ClassFileMethodAccessorGenerator { classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> if (function.isSuspend) { - writeSuspendingCallerInstructions(function, thisClass, instanceDesc, executable, codeBuilder) + writeSuspendingCallerInstructions(codeBuilder) } else { - writeBlockingCallerInstructions(function, thisClass, instanceDesc, executable, codeBuilder) + writeBlockingCallerInstructions(codeBuilder) } } } @@ -98,11 +96,11 @@ internal object ClassFileMethodAccessorGenerator { } } - private fun writeBlockingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Executable, codeBuilder: CodeBuilder) { + private fun writeBlockingCallerInstructions(codeBuilder: CodeBuilder) { if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) + writeDefaultInvokeInstructions(continuationSlot = null, codeBuilder) } else { - writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot = null, codeBuilder) + writeInvokeInstructions(continuationSlot = null, codeBuilder) } if (executable is Method) { @@ -116,7 +114,7 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.areturn() } - private fun writeSuspendingCallerInstructions(function: KFunction<*>, thisClass: ClassDesc, instanceDesc: ClassDesc, executable: Executable, codeBuilder: CodeBuilder) { + private fun writeSuspendingCallerInstructions(codeBuilder: CodeBuilder) { require(executable is Method) val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) @@ -154,9 +152,9 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) + writeDefaultInvokeInstructions(continuationSlot, codeBuilder) } else { - writeInvokeInstructions(thisClass, instanceDesc, function, executable, continuationSlot, codeBuilder) + writeInvokeInstructions(continuationSlot, codeBuilder) } codeBuilder.astore(callReturnValueSlot) @@ -244,14 +242,7 @@ internal object ClassFileMethodAccessorGenerator { } } - private fun writeInvokeInstructions( - thisClass: ClassDesc, - instanceDesc: ClassDesc, - function: KFunction<*>, - executable: Executable, - continuationSlot: Int?, - codeBuilder: CodeBuilder, - ) { + private fun writeInvokeInstructions(continuationSlot: Int?, codeBuilder: CodeBuilder) { val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void @@ -296,14 +287,7 @@ internal object ClassFileMethodAccessorGenerator { } } - private fun writeDefaultInvokeInstructions( - thisClass: ClassDesc, - instanceDesc: ClassDesc, - function: KFunction<*>, - executable: Executable, - continuationSlot: Int?, - codeBuilder: CodeBuilder, - ) { + private fun writeDefaultInvokeInstructions(continuationSlot: Int?, codeBuilder: CodeBuilder) { val isStatic = Modifier.isStatic(executable.modifiers) val methodTypeDesc = run { val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void @@ -365,6 +349,22 @@ internal object ClassFileMethodAccessorGenerator { codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) } } + + internal companion object { + internal fun generate( + instance: Any?, + function: KFunction, + lookup: MethodHandles.Lookup, + ): MethodAccessor { + function.parameters.forEach { parameter -> + require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { + "Unsupported parameter kind: $parameter" + } + } + + return ClassFileMethodAccessorGenerator(instance, function, lookup).generate() + } + } } private fun CodeBuilder.loadParameter(thisSlot: Int, thisClass: ClassDesc, index: Int, parameterSlot: Int) { From 24f84b6fa8ee154e631b3865d059633159d82f95 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 23:43:03 +0200 Subject: [PATCH 25/47] Split `ClassFileMethodAccessorGenerator` to better represent the code generated by each "modifier type" This slightly increases duplication but makes it easier to read overall Should also help later when generating the same function but forcing without coroutines (for blocking call) --- ...bstractClassFileMethodAccessorGenerator.kt | 79 ++++ .../ClassFileMemberMethodAccessorGenerator.kt | 52 +++ .../ClassFileMethodAccessorGenerator.kt | 420 +----------------- .../ClassFileStaticMethodAccessorGenerator.kt | 50 +++ .../invoker/AbstractInvokerGenerator.kt | 77 ++++ .../codegen/invoker/InvokerGenerator.kt | 9 + .../AbstractDefaultInvokerGenerator.kt | 41 ++ .../DefaultConstructorInvokerGenerator.kt | 44 ++ .../default/DefaultInvokerGenerator.kt | 22 + .../default/DefaultMethodInvokerGenerator.kt | 47 ++ .../direct/AbstractDirectInvokerGenerator.kt | 27 ++ .../DirectConstructorInvokerGenerator.kt | 38 ++ .../invoker/direct/DirectInvokerGenerator.kt | 22 + .../direct/DirectMethodInvokerGenerator.kt | 43 ++ .../modality/BlockingInvokerGenerator.kt | 28 ++ .../modality/ModalityAwareInvokerGenerator.kt | 13 + .../modality/SuspendingInvokerGenerator.kt | 145 ++++++ 17 files changed, 750 insertions(+), 407 deletions(-) create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/InvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/BlockingInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/ModalityAwareInvokerGenerator.kt create mode 100644 BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/SuspendingInvokerGenerator.kt diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt new file mode 100644 index 000000000..fae0eb172 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -0,0 +1,79 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.default.DefaultInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct.DirectInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.modality.BlockingInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.modality.SuspendingInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_Continuation +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassFile.ACC_FINAL +import java.lang.classfile.ClassFile.ACC_PUBLIC +import java.lang.constant.ClassDesc +import java.lang.constant.ConstantDescs.CD_Map +import java.lang.constant.ConstantDescs.CD_Object +import java.lang.constant.MethodTypeDesc +import java.lang.invoke.MethodHandles +import java.lang.reflect.AccessFlag +import java.lang.reflect.Executable +import java.lang.reflect.Modifier +import kotlin.reflect.KFunction + +internal abstract class AbstractClassFileMethodAccessorGenerator( + internal val instance: Any?, + internal val function: KFunction, + internal val lookup: MethodHandles.Lookup, +) { + + internal val executable: Executable = function.javaExecutable + internal val instanceClass: Class<*> = executable.declaringClass + internal val isInterface: Boolean get() = instanceClass.isInterface + internal val instanceDesc: ClassDesc = instanceClass.describeConstable().get() + internal val isStatic: Boolean = Modifier.isStatic(executable.modifiers) + + internal val thisClass: ClassDesc = ClassDesc.of("${lookup.lookupClass().packageName}.ClassFileMethodAccessor") + + // The class must be unique per function, which is why we don't cache the class + // Also "duplicate" definitions are allowed for hidden classes + internal fun generate(): MethodAccessor { + val bytes = ClassFile.of().build(thisClass) { classBuilder -> + classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) + classBuilder.withInterfaceSymbols(CD_MethodAccessor) + + // TODO replace with class data of hidden class + addFields(classBuilder) + + addConstructor(classBuilder) + + classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + val modalityGenerator = when { + function.isSuspend -> SuspendingInvokerGenerator + else -> BlockingInvokerGenerator + } + with(modalityGenerator) { + val invokerGenerator = when { + function.parameters.any { it.isOptional } -> DefaultInvokerGenerator + else -> DirectInvokerGenerator + } + generate(invokerGenerator, codeBuilder) + } + } + } + + val clazz = lookup + .defineHiddenClass(bytes, true) + .lookupClass() + + @Suppress("UNCHECKED_CAST") + return createInstance(clazz) as MethodAccessor + } + + protected abstract fun addFields(classBuilder: ClassBuilder) + + protected abstract fun addConstructor(classBuilder: ClassBuilder) + + protected abstract fun createInstance(clazz: Class<*>): MethodAccessor<*> +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt new file mode 100644 index 000000000..806e7d69d --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt @@ -0,0 +1,52 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassFile.* +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.invoke.MethodHandles +import kotlin.reflect.KFunction + +internal class ClassFileMemberMethodAccessorGenerator( + instance: Any?, + function: KFunction, + lookup: MethodHandles.Lookup, +) : AbstractClassFileMethodAccessorGenerator(instance, function, lookup) { + + override fun addFields(classBuilder: ClassBuilder) { + classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) + classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) + } + + override fun addConstructor(classBuilder: ClassBuilder) { + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) + + // this.super() + codeBuilder.aload(thisSlot) + codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) + + // this.instance = instance; + val instanceSlot = codeBuilder.parameterSlot(0) + codeBuilder.aload(thisSlot) + codeBuilder.aload(instanceSlot) + codeBuilder.putfield(thisClass, "instance", instanceDesc) + + // this.function = function; + codeBuilder.aload(thisSlot) + codeBuilder.aload(functionSlot) + codeBuilder.putfield(thisClass, "function", CD_KFunction) + + codeBuilder.return_() + } + } + + override fun createInstance(clazz: Class<*>): MethodAccessor<*> { + return clazz + .getDeclaredConstructor(instanceClass, KFunction::class.java) + .newInstance(instance, function) as MethodAccessor<*> + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt index 5a849652d..4b5a4b3ef 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -1,423 +1,29 @@ package dev.freya02.botcommands.method.accessors.internal.codegen import dev.freya02.botcommands.method.accessors.internal.MethodAccessor -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.* import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable -import java.lang.classfile.ClassFile -import java.lang.classfile.ClassFile.* -import java.lang.classfile.CodeBuilder -import java.lang.classfile.TypeKind -import java.lang.classfile.instruction.SwitchCase -import java.lang.constant.ClassDesc -import java.lang.constant.ConstantDescs.* -import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles -import java.lang.reflect.AccessFlag -import java.lang.reflect.Constructor -import java.lang.reflect.Method import java.lang.reflect.Modifier import kotlin.reflect.KFunction import kotlin.reflect.KParameter -import kotlin.reflect.jvm.jvmErasure -internal class ClassFileMethodAccessorGenerator private constructor( - private val instance: Any?, - private val function: KFunction, - private val lookup: MethodHandles.Lookup, -) { +internal object ClassFileMethodAccessorGenerator { - private val executable = function.javaExecutable - private val instanceClass = executable.declaringClass - private val instanceDesc = instanceClass.describeConstable().get() - private val isStatic = Modifier.isStatic(executable.modifiers) - - private val thisClass = ClassDesc.of("${lookup.lookupClass().packageName}.ClassFileMethodAccessor") - - // The class must be unique per function, which is why we don't cache the class - // Also "duplicate" definitions are allowed for hidden classes - private fun generate(): MethodAccessor { - val bytes = ClassFile.of().build(thisClass) { classBuilder -> - classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) - classBuilder.withInterfaceSymbols(CD_MethodAccessor) - - // TODO replace with class data of hidden class - if (!isStatic) classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) - classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) - - val ctorType = when { - isStatic -> MethodTypeDesc.of(CD_void, CD_KFunction) - else -> MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction) - } - classBuilder.withMethodBody(INIT_NAME, ctorType, ACC_PUBLIC) { codeBuilder -> - val thisSlot = codeBuilder.receiverSlot() - val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) - - // this.super() - codeBuilder.aload(thisSlot) - codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) - - if (!isStatic) { - // this.instance = instance; - val instanceSlot = codeBuilder.parameterSlot(0) - codeBuilder.aload(thisSlot) - codeBuilder.aload(instanceSlot) - codeBuilder.putfield(thisClass, "instance", instanceDesc) - } - - // this.function = function; - codeBuilder.aload(thisSlot) - codeBuilder.aload(functionSlot) - codeBuilder.putfield(thisClass, "function", CD_KFunction) - - codeBuilder.return_() - } - - classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> - if (function.isSuspend) { - writeSuspendingCallerInstructions(codeBuilder) - } else { - writeBlockingCallerInstructions(codeBuilder) - } + internal fun generate( + instance: Any?, + function: KFunction, + lookup: MethodHandles.Lookup, + ): MethodAccessor { + function.parameters.forEach { parameter -> + require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { + "Unsupported parameter kind: $parameter" } } - val clazz = lookup - .defineHiddenClass(bytes, true) - .lookupClass() - @Suppress("UNCHECKED_CAST") - return if (isStatic) { - clazz - .getDeclaredConstructor(KFunction::class.java) - .newInstance(function) as MethodAccessor - } else { - clazz - .getDeclaredConstructor(instanceClass, KFunction::class.java) - .newInstance(instance, function) as MethodAccessor + val generator = when { + Modifier.isStatic(function.javaExecutable.modifiers) -> ClassFileStaticMethodAccessorGenerator(instance, function, lookup) + else -> ClassFileMemberMethodAccessorGenerator(instance, function, lookup) } + return generator.generate() } - - private fun writeBlockingCallerInstructions(codeBuilder: CodeBuilder) { - if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(continuationSlot = null, codeBuilder) - } else { - writeInvokeInstructions(continuationSlot = null, codeBuilder) - } - - if (executable is Method) { - // Return value as Object, or return Unit as the implemented method must return something - if (executable.returnType != Void.TYPE) { - codeBuilder.boxIfPrimitive(type = executable.returnType) - } else { - codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) - } - } - codeBuilder.areturn() - } - - private fun writeSuspendingCallerInstructions(codeBuilder: CodeBuilder) { - require(executable is Method) - - val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - val callReturnValueSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - - codeBuilder.assignOrCreateContinuation(continuationSlot) - - val firstRunLabel = codeBuilder.newLabel() - val firstResumeLabel = codeBuilder.newLabel() - val defaultResumeLabel = codeBuilder.newLabel() - /** Skips right to the return statement */ - val returnResultLabel = codeBuilder.newLabel() - - // var callReturnValue = continuation.result; - codeBuilder.aload(continuationSlot) - codeBuilder.getfield(CD_MethodAccessorContinuation, "result", CD_Object) - codeBuilder.astore(callReturnValueSlot) - - // switch (continuation.label) { ... } - codeBuilder.aload(continuationSlot) - codeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) - codeBuilder.tableswitch( - defaultResumeLabel, - listOf( - SwitchCase.of(0, firstRunLabel), - SwitchCase.of(1, firstResumeLabel) - ) - ) - - - codeBuilder.labelBinding(firstRunLabel) - // continuation.label = 1 - codeBuilder.aload(continuationSlot) - codeBuilder.iconst_1() - codeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) - - if (function.parameters.any { it.isOptional }) { - writeDefaultInvokeInstructions(continuationSlot, codeBuilder) - } else { - writeInvokeInstructions(continuationSlot, codeBuilder) - } - codeBuilder.astore(callReturnValueSlot) - - // if (callReturnValue == IntrinsicsKt.getCOROUTINE_SUSPENDED()) { ... } - codeBuilder.aload(callReturnValueSlot) - codeBuilder.invokestatic(CD_IntrinsicsKt, "getCOROUTINE_SUSPENDED", MethodTypeDesc.of(CD_Object)) - codeBuilder.if_acmpne(returnResultLabel) // If not COROUTINE_SUSPENDED, go return real value - // At this point the result is equal to COROUTINE_SUSPENDED - // DebugProbesKt.probeCoroutineSuspended(continuation) - codeBuilder.aload(continuationSlot) - codeBuilder.invokestatic(CD_DebugProbesKt, "probeCoroutineSuspended", MethodTypeDesc.of(CD_void, CD_Continuation)) - // return callReturnValue (always COROUTINE_SUSPENDED) - codeBuilder.aload(callReturnValueSlot) - codeBuilder.areturn() - - - codeBuilder.labelBinding(firstResumeLabel) - // After the first suspension point (i.e. the call to the user function), return result - // ResultKt.throwOnFailure(callReturnValue) - codeBuilder.aload(callReturnValueSlot) - codeBuilder.invokestatic(CD_ResultKt, "throwOnFailure", MethodTypeDesc.of(CD_void, CD_Object)) - // return result - codeBuilder.goto_(returnResultLabel) - - - codeBuilder.labelBinding(defaultResumeLabel) - // throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine") - codeBuilder.new_(CD_IllegalStateException) - codeBuilder.dup() - codeBuilder.ldc("call to 'resume' before 'invoke' with coroutine" as java.lang.String) - codeBuilder.invokespecial(CD_IllegalStateException, INIT_NAME, MethodTypeDesc.of(CD_void, CD_String)) - codeBuilder.athrow() - - - codeBuilder.labelBinding(returnResultLabel) - // As per KCallable#callSuspendBy, Unit functions may not return Unit in some cases - if (function.returnType.classifier == Unit::class && !function.returnType.isMarkedNullable) { - // In those cases, force return Unit - codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) - } else { - codeBuilder.aload(callReturnValueSlot) - } - codeBuilder.areturn() - } - - private fun CodeBuilder.assignOrCreateContinuation(continuationSlot: Int) { - val thisSlot = receiverSlot() - val completionSlot = parameterSlot(1) - - block { blockCodeBuilder -> - // if (completion instanceof MethodAccessorContinuation) { ... } - aload(completionSlot) - instanceOf(CD_MethodAccessorContinuation) - ifThen { instanceOfCodeBuilder -> - // continuation = (MethodAccessorContinuation) completion; - instanceOfCodeBuilder.aload(completionSlot) - instanceOfCodeBuilder.checkcast(CD_MethodAccessorContinuation) - instanceOfCodeBuilder.astore(continuationSlot) - - // if (continuation.isResumeLabel()) { ... } - instanceOfCodeBuilder.aload(continuationSlot) - instanceOfCodeBuilder.invokevirtual(CD_MethodAccessorContinuation, "isResumeLabel", MethodTypeDesc.of(CD_boolean)) - instanceOfCodeBuilder.ifThen { isResumeCodeBuilder -> - // continuation.label = continuation.label - Integer.MIN_VALUE - isResumeCodeBuilder.aload(continuationSlot) - isResumeCodeBuilder.dup() // So we can reassign it - isResumeCodeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) - isResumeCodeBuilder.loadConstant(Integer.MIN_VALUE) - isResumeCodeBuilder.isub() - isResumeCodeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) - - // break - instanceOfCodeBuilder.goto_(blockCodeBuilder.breakLabel()) - } - } - - // If we're here, the continuation either isn't ours, or it is (what I assume) a resumed one - // continuation = new MethodAccessorContinuation(completion, this); - new_(CD_MethodAccessorContinuation) - dup() // To assign after - aload(completionSlot) - aload(thisSlot) - invokespecial(CD_MethodAccessorContinuation, INIT_NAME, MethodTypeDesc.of(CD_void, CD_Continuation, CD_MethodAccessor)) - astore(continuationSlot) - } - } - - private fun writeInvokeInstructions(continuationSlot: Int?, codeBuilder: CodeBuilder) { - val isStatic = Modifier.isStatic(executable.modifiers) - val methodTypeDesc = run { - val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void - val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } - MethodTypeDesc.of(returnTypeDesc, parameterDescs) - } - - val thisSlot = codeBuilder.receiverSlot() - val argsSlot = codeBuilder.parameterSlot(0) - - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - - // this.instance.[methodName]([params]) - if (executable is Constructor<*>) { - codeBuilder.new_(instanceDesc) - codeBuilder.dup() // So we can return it - } else if (!isStatic) { - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "instance", instanceDesc) - } - function.parameters.forEachIndexed { index, parameter -> - if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed - - // var parameter = function.getParameters().get([index]) - codeBuilder.loadParameter(thisSlot, thisClass, index, parameterSlot) - - // = args.get(parameter) - codeBuilder.aload(argsSlot) - codeBuilder.aload(parameterSlot) - codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - codeBuilder.unboxOrCastTo(target = parameter.type.jvmErasure.java) - } - if (continuationSlot != null) codeBuilder.aload(continuationSlot) - if (executable is Constructor<*>) { - codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) - } else if (isStatic) { - codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) - } else if (executable.declaringClass.isInterface) { - codeBuilder.invokeinterface(instanceDesc, executable.name, methodTypeDesc) - } else { - codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) - } - } - - private fun writeDefaultInvokeInstructions(continuationSlot: Int?, codeBuilder: CodeBuilder) { - val isStatic = Modifier.isStatic(executable.modifiers) - val methodTypeDesc = run { - val returnTypeDesc = if (executable is Method) executable.returnType.describeConstable().get() else CD_void - val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } - val effectiveParameters = when { - executable is Constructor<*> -> parameterDescs + listOf(CD_int, CD_DefaultConstructorMarker) - isStatic -> parameterDescs + listOf(CD_int, CD_Object) - else -> listOf(instanceDesc) + parameterDescs + listOf(CD_int, CD_Object) - } - MethodTypeDesc.of(returnTypeDesc, effectiveParameters) - } - - val thisSlot = codeBuilder.receiverSlot() - val argsSlot = codeBuilder.parameterSlot(0) - - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) - - // maskSlot = 0 - codeBuilder.iconst_0() - codeBuilder.istore(maskSlot) - - // InstanceClass.[methodName]$default(instance, [params], mask, null) - if (executable is Constructor<*>) { - codeBuilder.new_(instanceDesc) - codeBuilder.dup() // So we can return it - } else if (!isStatic) { - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "instance", instanceDesc) - } - var valueParameterIndex = 0 - function.parameters.forEachIndexed { index, parameter -> - if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed - - val paramJavaType = parameter.type.jvmErasure.java - - // var parameter = function.getParameters().get([index]) - codeBuilder.loadParameter(thisSlot, thisClass, index, parameterSlot) - - if (parameter.isOptional) { - codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, parameterSlot, maskSlot, valueParameterIndex) - } else { - // = args.get(parameter) - codeBuilder.aload(argsSlot) - codeBuilder.aload(parameterSlot) - codeBuilder.invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - // Cast non-null value into primitive/ref - codeBuilder.unboxOrCastTo(target = paramJavaType) - } - - valueParameterIndex++ - } - if (continuationSlot != null) codeBuilder.aload(continuationSlot) - codeBuilder.iload(maskSlot) - codeBuilder.aconst_null() - if (executable is Constructor<*>) { - codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) - } else { - codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) - } - } - - internal companion object { - internal fun generate( - instance: Any?, - function: KFunction, - lookup: MethodHandles.Lookup, - ): MethodAccessor { - function.parameters.forEach { parameter -> - require(parameter.kind == KParameter.Kind.INSTANCE || parameter.kind == KParameter.Kind.VALUE) { - "Unsupported parameter kind: $parameter" - } - } - - return ClassFileMethodAccessorGenerator(instance, function, lookup).generate() - } - } -} - -private fun CodeBuilder.loadParameter(thisSlot: Int, thisClass: ClassDesc, index: Int, parameterSlot: Int) { - // var parameter = function.getParameters().get([index]) - aload(thisSlot) - getfield(thisClass, "function", CD_KFunction) - invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) - loadConstant(index) - invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) - checkcast(CD_KParameter) - astore(parameterSlot) -} - -private fun CodeBuilder.loadUnboxedOptional( - type: Class<*>, - argsSlot: Int, - parameterSlot: Int, - maskSlot: Int, - valueParameterIndex: Int, -) { - aload(argsSlot) - aload(parameterSlot) - invokeinterface(CD_Map, "containsKey", MethodTypeDesc.of(CD_boolean, CD_Object)) - - // NOTE: Remember to have the same amount of stack data in and out of the branch - ifThenElse( - { - // Key exists, unbox or cast - // <- () args.get(parameter) - aload(argsSlot) - aload(parameterSlot) - invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - // The value may be null, but null can always be cast to any object type - unboxOrCastTo(type) - }, - { - // Key does not exist, load default - when (type) { - Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> - iconst_0() - - Long::class.javaPrimitiveType -> lconst_0() - Float::class.javaPrimitiveType -> fconst_0() - Double::class.javaPrimitiveType -> dconst_0() - else -> aconst_null() - } - - // Also set our mask bit so the placeholder gets replaced by the default - // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] - iload(maskSlot) - loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) - ior() - istore(maskSlot) - } - ) } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt new file mode 100644 index 000000000..85352c10a --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt @@ -0,0 +1,50 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodAccessor +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassFile.* +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.invoke.MethodHandles +import java.lang.reflect.AccessFlag +import kotlin.reflect.KFunction + +internal class ClassFileStaticMethodAccessorGenerator( + instance: Any?, + function: KFunction, + lookup: MethodHandles.Lookup, +) : AbstractClassFileMethodAccessorGenerator(instance, function, lookup) { + + override fun addFields(classBuilder: ClassBuilder) { + classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) + classBuilder.withInterfaceSymbols(CD_MethodAccessor) + + classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) + } + + override fun addConstructor(classBuilder: ClassBuilder) { + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) + + // this.super() + codeBuilder.aload(thisSlot) + codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) + + // this.function = function; + codeBuilder.aload(thisSlot) + codeBuilder.aload(functionSlot) + codeBuilder.putfield(thisClass, "function", CD_KFunction) + + codeBuilder.return_() + } + } + + override fun createInstance(clazz: Class<*>): MethodAccessor<*> { + return clazz + .getDeclaredConstructor(KFunction::class.java) + .newInstance(function) as MethodAccessor<*> + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt new file mode 100644 index 000000000..e75257d56 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt @@ -0,0 +1,77 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KCallable +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KParameter +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.unboxOrCastTo +import java.lang.classfile.CodeBuilder +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc + +internal abstract class AbstractInvokerGenerator : InvokerGenerator { + + protected fun AbstractClassFileMethodAccessorGenerator<*>.loadParameter( + codeBuilder: CodeBuilder, + thisSlot: Int, + index: Int, + parameterSlot: Int + ) { + // var parameter = function.getParameters().get([index]) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "function", CD_KFunction) + codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) + codeBuilder.loadConstant(index) + codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) + codeBuilder.checkcast(CD_KParameter) + codeBuilder.astore(parameterSlot) + } + + protected fun CodeBuilder.loadUnboxedOptional( + type: Class<*>, + argsSlot: Int, + parameterSlot: Int, + maskSlot: Int, + valueParameterIndex: Int, + ) { + aload(argsSlot) + aload(parameterSlot) + invokeinterface(CD_Map, "containsKey", MethodTypeDesc.of(CD_boolean, CD_Object)) + + // NOTE: Remember to have the same amount of stack data in and out of the branch + ifThenElse( + { + // Key exists, unbox or cast + // <- () args.get(parameter) + loadArg(argsSlot, parameterSlot, type) + }, + { + // Key does not exist, load default + when (type) { + Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> + iconst_0() + + Long::class.javaPrimitiveType -> lconst_0() + Float::class.javaPrimitiveType -> fconst_0() + Double::class.javaPrimitiveType -> dconst_0() + else -> aconst_null() + } + + // Also set our mask bit so the placeholder gets replaced by the default + // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] + iload(maskSlot) + loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) + ior() + istore(maskSlot) + } + ) + } + + protected fun CodeBuilder.loadArg(argsSlot: Int, parameterSlot: Int, type: Class<*>) { + aload(argsSlot) + aload(parameterSlot) + invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) + // The value may be null, but null can always be cast to any object type + unboxOrCastTo(type) + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/InvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/InvokerGenerator.kt new file mode 100644 index 000000000..2297f860b --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/InvokerGenerator.kt @@ -0,0 +1,9 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import java.lang.classfile.CodeBuilder + +internal interface InvokerGenerator { + + fun AbstractClassFileMethodAccessorGenerator<*>.generate(continuationSlot: Int?, codeBuilder: CodeBuilder) +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt new file mode 100644 index 000000000..33bd44512 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt @@ -0,0 +1,41 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.default + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import java.lang.classfile.CodeBuilder +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.jvmErasure + +internal abstract class AbstractDefaultInvokerGenerator : AbstractInvokerGenerator() { + + protected fun AbstractClassFileMethodAccessorGenerator<*>.loadDefaultParameters( + thisSlot: Int, + parameterSlot: Int, + argsSlot: Int, + maskSlot: Int, + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + var valueParameterIndex = 0 + function.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed + + val paramJavaType = parameter.type.jvmErasure.java + + // var parameter = function.getParameters().get([index]) + loadParameter(codeBuilder, thisSlot, index, parameterSlot) + + if (parameter.isOptional) { + codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, parameterSlot, maskSlot, valueParameterIndex) + } else { + // = args.get(parameter) + codeBuilder.loadArg(argsSlot, parameterSlot, paramJavaType) + } + + valueParameterIndex++ + } + if (continuationSlot != null) codeBuilder.aload(continuationSlot) + codeBuilder.iload(maskSlot) + codeBuilder.aconst_null() + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt new file mode 100644 index 000000000..4232cc7fb --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt @@ -0,0 +1,44 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.default + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_DefaultConstructorMarker +import java.lang.classfile.CodeBuilder +import java.lang.classfile.TypeKind +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.Constructor + +internal object DefaultConstructorInvokerGenerator : AbstractDefaultInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + require(executable is Constructor<*>) + require(continuationSlot == null) { "Constructors cannot be suspending" } + + val methodTypeDesc = run { + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + val effectiveParameters = parameterDescs + listOf(CD_int, CD_DefaultConstructorMarker) + MethodTypeDesc.of(CD_void, effectiveParameters) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) + + // maskSlot = 0 + codeBuilder.iconst_0() + codeBuilder.istore(maskSlot) + + // new [className] + codeBuilder.new_(instanceDesc) + codeBuilder.dup() // So we can return it + + // .""([params], mask, null) + loadDefaultParameters(thisSlot, parameterSlot, argsSlot, maskSlot, continuationSlot, codeBuilder) + codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultInvokerGenerator.kt new file mode 100644 index 000000000..3ab207439 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultInvokerGenerator.kt @@ -0,0 +1,22 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.default + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import java.lang.classfile.CodeBuilder +import java.lang.reflect.Constructor +import java.lang.reflect.Method + +internal object DefaultInvokerGenerator : AbstractInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + val generator = when (executable) { + is Method -> DefaultMethodInvokerGenerator + is Constructor<*> -> DefaultConstructorInvokerGenerator + } + + with(generator) { generate(continuationSlot, codeBuilder) } + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt new file mode 100644 index 000000000..e0214ee4e --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt @@ -0,0 +1,47 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.default + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import java.lang.classfile.CodeBuilder +import java.lang.classfile.TypeKind +import java.lang.constant.ConstantDescs.CD_Object +import java.lang.constant.ConstantDescs.CD_int +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.Method + +internal object DefaultMethodInvokerGenerator : AbstractDefaultInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + require(executable is Method) + + val methodTypeDesc = run { + val returnTypeDesc = executable.returnType.describeConstable().get() + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + val effectiveParameters = when { + isStatic -> parameterDescs + listOf(CD_int, CD_Object) + else -> listOf(instanceDesc) + parameterDescs + listOf(CD_int, CD_Object) + } + MethodTypeDesc.of(returnTypeDesc, effectiveParameters) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) + + // maskSlot = 0 + codeBuilder.iconst_0() + codeBuilder.istore(maskSlot) + + // InstanceClass.[methodName]$default(instance, [params], mask, null) + if (!isStatic) { + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + } + loadDefaultParameters(thisSlot, parameterSlot, argsSlot, maskSlot, continuationSlot, codeBuilder) + codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt new file mode 100644 index 000000000..ee5ea22f5 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt @@ -0,0 +1,27 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import java.lang.classfile.CodeBuilder +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.jvmErasure + +internal abstract class AbstractDirectInvokerGenerator : AbstractInvokerGenerator() { + + protected fun AbstractClassFileMethodAccessorGenerator<*>.loadParameters( + thisSlot: Int, + parameterSlot: Int, + argsSlot: Int, + codeBuilder: CodeBuilder, + ) { + function.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed + + // var parameter = function.getParameters().get([index]) + loadParameter(codeBuilder, thisSlot, index, parameterSlot) + + // = args.get(parameter) + codeBuilder.loadArg(argsSlot, parameterSlot, parameter.type.jvmErasure.java) + } + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt new file mode 100644 index 000000000..ec04c5fd6 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt @@ -0,0 +1,38 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import java.lang.classfile.CodeBuilder +import java.lang.classfile.TypeKind +import java.lang.constant.ConstantDescs.CD_void +import java.lang.constant.ConstantDescs.INIT_NAME +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.Constructor + +internal object DirectConstructorInvokerGenerator : AbstractDirectInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + require(executable is Constructor<*>) + check(continuationSlot == null) { "Constructors cannot be suspending" } + + val methodTypeDesc = run { + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + MethodTypeDesc.of(CD_void, parameterDescs) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // new [className]([params]) + codeBuilder.new_(instanceDesc) + codeBuilder.dup() // So we can return it + + // .``([params]) + loadParameters(thisSlot, parameterSlot, argsSlot, codeBuilder) + codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectInvokerGenerator.kt new file mode 100644 index 000000000..65f337f3b --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectInvokerGenerator.kt @@ -0,0 +1,22 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import java.lang.classfile.CodeBuilder +import java.lang.reflect.Constructor +import java.lang.reflect.Method + +internal object DirectInvokerGenerator : AbstractInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + val generator = when (executable) { + is Method -> DirectMethodInvokerGenerator + is Constructor<*> -> DirectConstructorInvokerGenerator + } + + with(generator) { generate(continuationSlot, codeBuilder) } + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt new file mode 100644 index 000000000..453656caf --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt @@ -0,0 +1,43 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import java.lang.classfile.CodeBuilder +import java.lang.classfile.TypeKind +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.Method + +internal object DirectMethodInvokerGenerator : AbstractDirectInvokerGenerator() { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + require(executable is Method) + + val methodTypeDesc = run { + val returnTypeDesc = executable.returnType.describeConstable().get() + val parameterDescs = executable.parameters.map { it.type.describeConstable().get() } + MethodTypeDesc.of(returnTypeDesc, parameterDescs) + } + + val thisSlot = codeBuilder.receiverSlot() + val argsSlot = codeBuilder.parameterSlot(0) + + val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // this.instance.[methodName]([params]) + if (!isStatic) { + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + } + loadParameters(thisSlot, parameterSlot, argsSlot, codeBuilder) + if (continuationSlot != null) codeBuilder.aload(continuationSlot) + if (isStatic) { + codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) + } else if (isInterface) { + codeBuilder.invokeinterface(instanceDesc, executable.name, methodTypeDesc) + } else { + codeBuilder.invokevirtual(instanceDesc, executable.name, methodTypeDesc) + } + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/BlockingInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/BlockingInvokerGenerator.kt new file mode 100644 index 000000000..cbd9c701c --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/BlockingInvokerGenerator.kt @@ -0,0 +1,28 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.modality + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.InvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_Unit +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.boxIfPrimitive +import java.lang.classfile.CodeBuilder +import java.lang.reflect.Method + +internal object BlockingInvokerGenerator : ModalityAwareInvokerGenerator { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + invokerGenerator: InvokerGenerator, + codeBuilder: CodeBuilder, + ) { + with(invokerGenerator) { generate(continuationSlot = null, codeBuilder) } + + if (executable is Method) { + // Return value as Object, or return Unit as the implemented method must return something + if (executable.returnType != Void.TYPE) { + codeBuilder.boxIfPrimitive(type = executable.returnType) + } else { + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } + } + codeBuilder.areturn() + } +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/ModalityAwareInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/ModalityAwareInvokerGenerator.kt new file mode 100644 index 000000000..577b5d096 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/ModalityAwareInvokerGenerator.kt @@ -0,0 +1,13 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.modality + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.InvokerGenerator +import java.lang.classfile.CodeBuilder + +internal interface ModalityAwareInvokerGenerator { + + fun AbstractClassFileMethodAccessorGenerator<*>.generate( + invokerGenerator: InvokerGenerator, + codeBuilder: CodeBuilder, + ) +} diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/SuspendingInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/SuspendingInvokerGenerator.kt new file mode 100644 index 000000000..c14ce26d1 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/modality/SuspendingInvokerGenerator.kt @@ -0,0 +1,145 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.modality + +import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.InvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.* +import java.lang.classfile.CodeBuilder +import java.lang.classfile.TypeKind +import java.lang.classfile.instruction.SwitchCase +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.Method + +internal object SuspendingInvokerGenerator : ModalityAwareInvokerGenerator { + + override fun AbstractClassFileMethodAccessorGenerator<*>.generate( + invokerGenerator: InvokerGenerator, + codeBuilder: CodeBuilder, + ) { + require(executable is Method) + + val continuationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val callReturnValueSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + assignOrCreateContinuation(codeBuilder, continuationSlot) + + val firstRunLabel = codeBuilder.newLabel() + val firstResumeLabel = codeBuilder.newLabel() + val defaultResumeLabel = codeBuilder.newLabel() + /** Skips right to the return statement */ + val returnResultLabel = codeBuilder.newLabel() + + // var callReturnValue = continuation.result; + codeBuilder.aload(continuationSlot) + codeBuilder.getfield(CD_MethodAccessorContinuation, "result", CD_Object) + codeBuilder.astore(callReturnValueSlot) + + // switch (continuation.label) { ... } + codeBuilder.aload(continuationSlot) + codeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) + codeBuilder.tableswitch( + defaultResumeLabel, + listOf( + SwitchCase.of(0, firstRunLabel), + SwitchCase.of(1, firstResumeLabel) + ) + ) + + + codeBuilder.labelBinding(firstRunLabel) + // continuation.label = 1 + codeBuilder.aload(continuationSlot) + codeBuilder.iconst_1() + codeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) + + with(invokerGenerator) { generate(continuationSlot, codeBuilder) } + codeBuilder.astore(callReturnValueSlot) + + // if (callReturnValue == IntrinsicsKt.getCOROUTINE_SUSPENDED()) { ... } + codeBuilder.aload(callReturnValueSlot) + codeBuilder.invokestatic(CD_IntrinsicsKt, "getCOROUTINE_SUSPENDED", MethodTypeDesc.of(CD_Object)) + codeBuilder.if_acmpne(returnResultLabel) // If not COROUTINE_SUSPENDED, go return real value + // At this point the result is equal to COROUTINE_SUSPENDED + // DebugProbesKt.probeCoroutineSuspended(continuation) + codeBuilder.aload(continuationSlot) + codeBuilder.invokestatic(CD_DebugProbesKt, "probeCoroutineSuspended", MethodTypeDesc.of(CD_void, CD_Continuation)) + // return callReturnValue (always COROUTINE_SUSPENDED) + codeBuilder.aload(callReturnValueSlot) + codeBuilder.areturn() + + + codeBuilder.labelBinding(firstResumeLabel) + // After the first suspension point (i.e. the call to the user function), return result + // ResultKt.throwOnFailure(callReturnValue) + codeBuilder.aload(callReturnValueSlot) + codeBuilder.invokestatic(CD_ResultKt, "throwOnFailure", MethodTypeDesc.of(CD_void, CD_Object)) + // return result + codeBuilder.goto_(returnResultLabel) + + + codeBuilder.labelBinding(defaultResumeLabel) + // throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine") + codeBuilder.new_(CD_IllegalStateException) + codeBuilder.dup() + codeBuilder.ldc("call to 'resume' before 'invoke' with coroutine" as java.lang.String) + codeBuilder.invokespecial(CD_IllegalStateException, INIT_NAME, MethodTypeDesc.of(CD_void, CD_String)) + codeBuilder.athrow() + + + codeBuilder.labelBinding(returnResultLabel) + // As per KCallable#callSuspendBy, Unit functions may not return Unit in some cases + if (function.returnType.classifier == Unit::class && !function.returnType.isMarkedNullable) { + // In those cases, force return Unit + codeBuilder.getstatic(CD_Unit, "INSTANCE", CD_Unit) + } else { + codeBuilder.aload(callReturnValueSlot) + } + codeBuilder.areturn() + } + + private fun assignOrCreateContinuation(codeBuilder: CodeBuilder, continuationSlot: Int) { + val thisSlot = codeBuilder.receiverSlot() + val completionSlot = codeBuilder.parameterSlot(1) + + codeBuilder.block { blockCodeBuilder -> + // if (completion instanceof MethodAccessorContinuation) { ... } + blockCodeBuilder.aload(completionSlot) + blockCodeBuilder.instanceOf(CD_MethodAccessorContinuation) + blockCodeBuilder.ifThen { instanceOfCodeBuilder -> + // continuation = (MethodAccessorContinuation) completion; + instanceOfCodeBuilder.aload(completionSlot) + instanceOfCodeBuilder.checkcast(CD_MethodAccessorContinuation) + instanceOfCodeBuilder.astore(continuationSlot) + + // if (continuation.isResumeLabel()) { ... } + instanceOfCodeBuilder.aload(continuationSlot) + instanceOfCodeBuilder.invokevirtual( + CD_MethodAccessorContinuation, + "isResumeLabel", + MethodTypeDesc.of(CD_boolean) + ) + instanceOfCodeBuilder.ifThen { isResumeCodeBuilder -> + // continuation.label = continuation.label - Integer.MIN_VALUE + isResumeCodeBuilder.aload(continuationSlot) + isResumeCodeBuilder.dup() // So we can reassign it + isResumeCodeBuilder.getfield(CD_MethodAccessorContinuation, "label", CD_int) + isResumeCodeBuilder.loadConstant(Integer.MIN_VALUE) + isResumeCodeBuilder.isub() + isResumeCodeBuilder.putfield(CD_MethodAccessorContinuation, "label", CD_int) + + // break + isResumeCodeBuilder.goto_(blockCodeBuilder.breakLabel()) + } + } + + // If we're here, the continuation either isn't ours, or it is (what I assume) a resumed one + // continuation = new MethodAccessorContinuation(completion, this); + codeBuilder.new_(CD_MethodAccessorContinuation) + codeBuilder.dup() // To assign after + codeBuilder.aload(completionSlot) + codeBuilder.aload(thisSlot) + codeBuilder.invokespecial(CD_MethodAccessorContinuation, INIT_NAME, MethodTypeDesc.of(CD_void, CD_Continuation, CD_MethodAccessor)) + codeBuilder.astore(continuationSlot) + } + } +} From cf1380a0f0bff01b9d0ec99962ff859f5613815d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 31 Aug 2025 23:47:09 +0200 Subject: [PATCH 26/47] Add missing `@JvmStatic` --- .../io/github/freya022/botcommands/api/core/BotCommands.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt index e6128debe..4415ae72e 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/BotCommands.kt @@ -38,6 +38,7 @@ object BotCommands { * This feature requires *running* on Java 24+, if your bot doesn't, this method has no effect. */ @ExperimentalMethodAccessorsApi + @get:JvmStatic @get:JvmName("isPreferClassFileAccessors") var preferClassFileAccessors: Boolean = false private set @@ -50,6 +51,7 @@ object BotCommands { * * This feature requires *running* on Java 24+, if your bot doesn't, this method has no effect. */ + @JvmStatic @ExperimentalMethodAccessorsApi fun preferClassFileAccessors() { preferClassFileAccessors = true From f392197d7f1b9263e50afb12cbd5daa1603b43b7 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:21:28 +0200 Subject: [PATCH 27/47] Rename `MethodAccessor#call` to `callSuspend` --- .../application/context/message/MessageCommandInfoImpl.kt | 2 +- .../commands/application/context/user/UserCommandInfoImpl.kt | 2 +- .../commands/application/slash/SlashCommandInfoImpl.kt | 2 +- .../application/slash/autocomplete/AutocompleteHandler.kt | 2 +- .../internal/commands/text/TextCommandVariationImpl.kt | 2 +- .../internal/components/handler/ComponentHandlerExecutor.kt | 2 +- .../internal/components/handler/ComponentTimeoutExecutor.kt | 2 +- .../botcommands/internal/core/hooks/EventDispatcherImpl.kt | 4 ++-- .../internal/core/reflection/AggregatorFunction.kt | 2 +- .../freya022/botcommands/internal/modals/ModalHandlerInfo.kt | 2 +- .../method/accessors/internal/MethodAccessorContinuation.java | 2 +- .../codegen/AbstractClassFileMethodAccessorGenerator.kt | 2 +- .../method/accessors/ClassFileMethodAccessorGeneratorTest.kt | 2 +- .../botcommands/method/accessors/internal/MethodAccessor.kt | 2 +- .../method/accessors/internal/KotlinReflectMethodAccessor.kt | 2 +- .../accessors/internal/KotlinReflectStaticMethodAccessor.kt | 2 +- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt index c699b7090..8fe6be589 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt @@ -66,7 +66,7 @@ internal class MessageCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.call(finalParameters) + eventFunction.methodAccessor.callSuspend(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt index 16a919b70..eb85e0227 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt @@ -66,7 +66,7 @@ internal class UserCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.call(finalParameters) + eventFunction.methodAccessor.callSuspend(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt index 7f4099d14..3eeb43984 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt @@ -76,7 +76,7 @@ internal sealed class SlashCommandInfoImpl( internal suspend fun execute(event: GlobalSlashEvent): Boolean { val objects = getSlashOptions(event, parameters) ?: return false - eventFunction.methodAccessor.call(objects) + eventFunction.methodAccessor.callSuspend(objects) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt index f6e658e3d..5820cbf20 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt @@ -96,7 +96,7 @@ internal class AutocompleteHandler( ?: return emptyList() //Autocomplete was triggered without all the required parameters being present val actualChoices: MutableList = arrayOfSize(25) - val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.eventFunction.methodAccessor.call(objects)) + val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.eventFunction.methodAccessor.callSuspend(objects)) val autoCompleteQuery = event.focusedOption //If something is typed but there are no choices, don't display user input diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt index 32fd56ea3..d27aa05ce 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt @@ -89,7 +89,7 @@ internal class TextCommandVariationImpl internal constructor( internal suspend fun execute(event: BaseCommandEvent, optionValues: Map) { val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.call(finalParameters) + eventFunction.methodAccessor.callSuspend(finalParameters) } /** diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt index ebbfc3736..347597718 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt @@ -87,7 +87,7 @@ internal class ComponentHandlerExecutor internal constructor( return false } - eventFunction.methodAccessor.call(parameters.mapFinalParameters(event, optionValues)) + eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) } return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt index bf37c19b1..741b01c19 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt @@ -78,7 +78,7 @@ internal class ComponentTimeoutExecutor internal constructor( return false } - eventFunction.methodAccessor.call(parameters.mapFinalParameters(firstArgument, optionValues)) + eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(firstArgument, optionValues)) } return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt index d5e8e4789..fc83f36eb 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt @@ -111,13 +111,13 @@ internal class EventDispatcherImpl internal constructor( if (timeout != null) { // Timeout only works when the continuations implement a cancellation handler val result = withTimeoutOrNull(timeout) { - methodAccessor.call(args) + methodAccessor.callSuspend(args) } if (result == null) { logger.debug { "Event listener ${classPathFunction.function.shortSignatureNoSrc} timed out" } } } else { - methodAccessor.call(args) + methodAccessor.callSuspend(args) } } catch (e: InvocationTargetException) { if (event is InitializationEvent) { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index 174ddd0af..94ad90fb0 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt @@ -52,7 +52,7 @@ internal class AggregatorFunction private constructor( aggregatorArguments[eventParameter] = firstParam } - return methodAccessor.call(aggregatorArguments) + return methodAccessor.callSuspend(aggregatorArguments) } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt index cb831553f..d95694e4a 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt @@ -89,7 +89,7 @@ internal class ModalHandlerInfo internal constructor( throwInternal(::tryInsertOption, "Insertion function shouldn't have been aborted") } - eventFunction.methodAccessor.call(parameters.mapFinalParameters(event, optionValues)) + eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) } private suspend fun tryInsertOption( diff --git a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java index ea50ed80d..b49fec6a3 100644 --- a/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java +++ b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java @@ -27,6 +27,6 @@ public boolean isResumeLabel() { protected Object invokeSuspend(@NotNull Object result) { this.result = result; this.label |= Integer.MIN_VALUE; - return methodAccessor.call(null, this); + return methodAccessor.callSuspend(null, this); } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt index fae0eb172..b8a50d6cc 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -48,7 +48,7 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( addConstructor(classBuilder) - classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + classBuilder.withMethodBody("callSuspend", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> val modalityGenerator = when { function.isSuspend -> SuspendingInvokerGenerator else -> BlockingInvokerGenerator diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 2e5d9961e..5d197d83b 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -88,7 +88,7 @@ object ClassFileMethodAccessorGeneratorTest { fun `Generate method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { runBlocking { val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.call(buildMap { + methodAccessor.callSuspend(buildMap { args.forEachIndexed { index, arg -> this[function.valueParameters[index]] = arg } diff --git a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt index e29b21407..26b04b979 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt @@ -4,5 +4,5 @@ import kotlin.reflect.KParameter interface MethodAccessor { - suspend fun call(args: Map): R + suspend fun callSuspend(args: Map): R } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index ae0b30728..8979ba19a 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -12,7 +12,7 @@ internal class KotlinReflectMethodAccessor internal constructor( private val instanceParameter = function.instanceParameter!! - override suspend fun call(args: Map): R { + override suspend fun callSuspend(args: Map): R { val args = args.toMutableMap() args.putIfAbsent(instanceParameter, instance) diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt index 5ea632469..5ced1ec74 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt @@ -8,7 +8,7 @@ internal class KotlinReflectStaticMethodAccessor internal constructor( private val function: KFunction, ) : MethodAccessor { - override suspend fun call(args: Map): R { + override suspend fun callSuspend(args: Map): R { return function.callSuspendBy(args) } } From e672a7ef8edf44f6f0d6d70bf5cda9bcd464abff Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:23:00 +0200 Subject: [PATCH 28/47] Add `MethodAccessor#call` for blocking calls --- ...bstractClassFileMethodAccessorGenerator.kt | 22 +++++++++++++++++-- .../internal/codegen/utils/ClassDescs.kt | 2 ++ .../ClassFileMethodAccessorGeneratorTest.kt | 21 ++++++++++++++++++ .../accessors/internal/MethodAccessor.kt | 2 ++ .../exceptions/IllegalSuspendCallException.kt | 8 +++++++ .../internal/KotlinReflectMethodAccessor.kt | 10 +++++++++ .../KotlinReflectStaticMethodAccessor.kt | 6 +++++ 7 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/exceptions/IllegalSuspendCallException.kt diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt index b8a50d6cc..b02bf1094 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -6,6 +6,7 @@ import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct. import dev.freya02.botcommands.method.accessors.internal.codegen.modality.BlockingInvokerGenerator import dev.freya02.botcommands.method.accessors.internal.codegen.modality.SuspendingInvokerGenerator import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_Continuation +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_IllegalSuspendCallException import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodAccessor import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable import java.lang.classfile.ClassBuilder @@ -13,8 +14,7 @@ import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.ACC_FINAL import java.lang.classfile.ClassFile.ACC_PUBLIC import java.lang.constant.ClassDesc -import java.lang.constant.ConstantDescs.CD_Map -import java.lang.constant.ConstantDescs.CD_Object +import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles import java.lang.reflect.AccessFlag @@ -61,6 +61,24 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( generate(invokerGenerator, codeBuilder) } } + + classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + if (function.isSuspend) { + // throw new IllegalSuspendCallException() + codeBuilder.new_(CD_IllegalSuspendCallException) + codeBuilder.dup() // so we can throw it + codeBuilder.invokespecial(CD_IllegalSuspendCallException, INIT_NAME, MethodTypeDesc.of(CD_void)) + codeBuilder.athrow() + } else { + with(BlockingInvokerGenerator) { + val invokerGenerator = when { + function.parameters.any { it.isOptional } -> DefaultInvokerGenerator + else -> DirectInvokerGenerator + } + generate(invokerGenerator, codeBuilder) + } + } + } } val clazz = lookup diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 0ae2d8445..854156514 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -2,6 +2,7 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.utils import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import dev.freya02.botcommands.method.accessors.internal.MethodAccessorContinuation +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import java.lang.constant.ClassDesc import kotlin.coroutines.Continuation import kotlin.reflect.KCallable @@ -22,3 +23,4 @@ internal val CD_DebugProbesKt = ClassDesc.of("kotlin.coroutines.jvm.internal.Deb internal val CD_MethodAccessor = ClassDesc.of(MethodAccessor::class.java.name) internal val CD_MethodAccessorContinuation = ClassDesc.of(MethodAccessorContinuation::class.java.name) +internal val CD_IllegalSuspendCallException = ClassDesc.of(IllegalSuspendCallException::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 5d197d83b..23fe5b02e 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -1,14 +1,18 @@ package dev.freya02.botcommands.method.accessors import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments.argumentSet import org.junit.jupiter.params.provider.MethodSource import kotlin.reflect.KFunction import kotlin.reflect.full.valueParameters +import kotlin.test.Test import kotlin.time.Duration.Companion.milliseconds interface TestInterface { @@ -96,6 +100,23 @@ object ClassFileMethodAccessorGeneratorTest { } } + @Test + fun `'call' throws on non-suspend functions`() { + assertThrows { + val instance = TestClass() + val function = TestClass::coRun + val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) + methodAccessor.call(mapOf()) + } + + assertDoesNotThrow { + val instance = TestClass() + val function = TestClass::run + val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) + methodAccessor.call(mapOf()) + } + } + @JvmStatic fun testCallers(): List = listOf( argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), diff --git a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt index 26b04b979..20560250e 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt @@ -5,4 +5,6 @@ import kotlin.reflect.KParameter interface MethodAccessor { suspend fun callSuspend(args: Map): R + + fun call(args: Map): R } diff --git a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/exceptions/IllegalSuspendCallException.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/exceptions/IllegalSuspendCallException.kt new file mode 100644 index 000000000..3defbd5ce --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/exceptions/IllegalSuspendCallException.kt @@ -0,0 +1,8 @@ +package dev.freya02.botcommands.method.accessors.internal.exceptions + +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor + +/** + * Indicates a suspend function was called without calling [MethodAccessor.callSuspend]. + */ +class IllegalSuspendCallException : RuntimeException("Suspending functions must be called with 'callSuspend'") diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index 8979ba19a..955fb8492 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.method.accessors.internal +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy @@ -18,4 +19,13 @@ internal class KotlinReflectMethodAccessor internal constructor( return function.callSuspendBy(args) } + + override fun call(args: Map): R { + if (function.isSuspend) throw IllegalSuspendCallException() + + val args = args.toMutableMap() + args.putIfAbsent(instanceParameter, instance) + + return function.callBy(args) + } } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt index 5ced1ec74..1aee8d661 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.method.accessors.internal +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy @@ -11,4 +12,9 @@ internal class KotlinReflectStaticMethodAccessor internal constructor( override suspend fun callSuspend(args: Map): R { return function.callSuspendBy(args) } + + override fun call(args: Map): R { + if (function.isSuspend) throw IllegalSuspendCallException() + return function.callBy(args) + } } From 1ecdfd576ff6e2ba1aa463acca8bf68356d3588e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:30:07 +0200 Subject: [PATCH 29/47] Replace `KFunction#callBy` with `MethodAccessor#call` --- .../method/accessors/MethodAccessorFactoryProvider.kt | 9 +++++++++ .../botcommands/internal/core/service/Singletons.kt | 5 +++-- .../internal/core/service/provider/ServiceProvider.kt | 6 +++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt index 0d1ee69da..c600ce0d6 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt @@ -3,9 +3,11 @@ package io.github.freya022.botcommands.internal.core.method.accessors import dev.freya02.botcommands.method.accessors.api.annotations.ExperimentalMethodAccessorsApi import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory import dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import dev.freya02.botcommands.method.accessors.internal.MethodAccessorFactory import io.github.freya022.botcommands.api.core.BotCommands import io.github.oshai.kotlinlogging.KotlinLogging +import kotlin.reflect.KFunction internal object MethodAccessorFactoryProvider { @@ -20,6 +22,8 @@ internal object MethodAccessorFactoryProvider { } } + private val staticAccessors: MutableMap, MethodAccessor<*>> = hashMapOf() + @OptIn(ExperimentalMethodAccessorsApi::class) internal fun getAccessorFactory(): MethodAccessorFactory { fun logUsage(msg: String) { @@ -39,4 +43,9 @@ internal object MethodAccessorFactoryProvider { kotlinReflectAccessorFactory } } + + internal fun getStaticAccessor(function: KFunction): MethodAccessor = synchronized(this) { + @Suppress("UNCHECKED_CAST") + staticAccessors.getOrPut(function) { getAccessorFactory().create(null, function) } as MethodAccessor + } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt index ac0168bf5..dc7fe2d5b 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt @@ -1,6 +1,7 @@ package io.github.freya022.botcommands.internal.core.service import io.github.freya022.botcommands.api.core.utils.shortQualifiedName +import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.utils.throwArgument import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -30,6 +31,6 @@ internal object Singletons { "Constructor of ${clazz.shortQualifiedName} must be effectively public (internal is allowed)" } - return constructor.callBy(mapOf()) + return MethodAccessorFactoryProvider.getStaticAccessor(constructor).call(mapOf()) } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt index 9aa60fad0..25c0086f6 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt @@ -6,6 +6,7 @@ import io.github.freya022.botcommands.api.core.service.ServiceError.ErrorType import io.github.freya022.botcommands.api.core.service.annotations.* import io.github.freya022.botcommands.api.core.utils.* import io.github.freya022.botcommands.internal.core.exceptions.ServiceException +import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider import io.github.freya022.botcommands.internal.core.service.BCServiceContainerImpl import io.github.freya022.botcommands.internal.core.service.Singletons import io.github.freya022.botcommands.internal.core.service.canCreateWrappedService @@ -295,16 +296,15 @@ internal fun KFunction.callStatic(serviceContainer: BCServiceContainerImp } return when (val instanceParameter = this.instanceParameter) { - null -> this.callBy(args) + null -> MethodAccessorFactoryProvider.getStaticAccessor(this).call(args) else -> { val instanceErasure = instanceParameter.type.jvmErasure val instance = instanceErasure.objectInstance ?: serviceContainer.tryGetService(instanceErasure).getOrThrow { throwArgument(this, "Could not run function as it is not static, the declaring class isn't an object, and service creation failed:\n${it.toDetailedString()}") } - args[instanceParameter] = instance - this.callBy(args) + MethodAccessorFactoryProvider.getAccessorFactory().create(instance, this).call(args) } } } From 6894518d754af5e97e015463c71f25948d0273a7 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:26:15 +0200 Subject: [PATCH 30/47] Remove unnecessary instance parameter assignments They are handled automatically by the MethodAccessor --- .../botcommands/internal/core/reflection/MemberFunction.kt | 5 ----- .../freya022/botcommands/internal/utils/ExecutionUtils.kt | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt index 22630fc6e..0bbd9a154 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt @@ -6,8 +6,6 @@ import io.github.freya022.botcommands.internal.core.method.accessors.MethodAcces import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters import io.github.freya022.botcommands.internal.utils.throwInternal import kotlin.reflect.KFunction -import kotlin.reflect.full.instanceParameter -import kotlin.reflect.full.valueParameters internal open class MemberFunction internal constructor( boundFunction: KFunction, @@ -16,9 +14,6 @@ internal open class MemberFunction internal constructor( val instance by lazy(instanceSupplier) val methodAccessor: MethodAccessor by lazy { MethodAccessorFactoryProvider.getAccessorFactory().create(instance, kFunction) } - val resolvableParameters = kFunction.valueParameters.drop(1) //Drop the first parameter - val instanceParameter = kFunction.instanceParameter - ?: throwInternal(kFunction, "Function shouldn't be static or constructors") val firstParameter = kFunction.nonInstanceParameters.firstOrNull() ?: throwInternal(kFunction, "The function should have been checked to have at least one parameter") } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt index f619c36ea..ba54bb1a0 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt @@ -49,7 +49,6 @@ internal suspend fun Collection.mapFinalParameters( firstParam: Any, optionValues: Map ) = buildParameters(executable.eventFunction.kFunction) { - this[executable.eventFunction.instanceParameter] = executable.instance this[executable.eventFunction.firstParameter] = firstParam for (parameter in this@mapFinalParameters) { @@ -115,4 +114,4 @@ private operator fun MutableMap.set(option: OptionImpl, obj: A } else { this[option.executableParameter] = obj } -} \ No newline at end of file +} From a1f8f5969abe16cc6b734cbb4dda70d368859506 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:26:40 +0200 Subject: [PATCH 31/47] Add missing star projection --- .../botcommands/method/accessors/MethodAccessorBenchmark.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt index 0f52d9c80..8e277e080 100644 --- a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt +++ b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -20,9 +20,9 @@ open class MethodAccessorBenchmark { private lateinit var instance: MyClass - private lateinit var simpleMethodAccessor: MethodAccessor - private lateinit var methodWithDefaultsAccessor: MethodAccessor - private lateinit var suspendingMethodWithDefaultsAccessor: MethodAccessor + private lateinit var simpleMethodAccessor: MethodAccessor<*> + private lateinit var methodWithDefaultsAccessor: MethodAccessor<*> + private lateinit var suspendingMethodWithDefaultsAccessor: MethodAccessor<*> private lateinit var simpleMethodKotlin: KFunction<*> private lateinit var methodWithDefaultsKotlin: KFunction<*> From b41fc871b3d8d1a198716fb2770b006ff214fbbb Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:54:11 +0200 Subject: [PATCH 32/47] Simplify accessing method accessors --- .../github/freya022/botcommands/internal/ExecutableMixin.kt | 4 +++- .../application/context/message/MessageCommandInfoImpl.kt | 2 +- .../commands/application/context/user/UserCommandInfoImpl.kt | 2 +- .../commands/application/slash/SlashCommandInfoImpl.kt | 2 +- .../application/slash/autocomplete/AutocompleteInfoImpl.kt | 1 + .../internal/commands/text/TextCommandVariationImpl.kt | 2 +- .../internal/components/handler/ComponentHandlerExecutor.kt | 2 +- .../internal/components/handler/ComponentTimeoutExecutor.kt | 2 +- .../internal/core/reflection/AggregatorFunction.kt | 2 +- .../freya022/botcommands/internal/modals/ModalHandlerInfo.kt | 2 +- 10 files changed, 12 insertions(+), 9 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/ExecutableMixin.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/ExecutableMixin.kt index a22704245..736be5f92 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/ExecutableMixin.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/ExecutableMixin.kt @@ -14,6 +14,8 @@ internal interface ExecutableMixin : Executable { get() = eventFunction.kFunction val instance: Any get() = eventFunction.instance + val methodAccessor + get() = eventFunction.methodAccessor } @Suppress("NOTHING_TO_INLINE") //Don't want this to appear in stack trace @@ -26,4 +28,4 @@ internal inline fun ExecutableMixin.requireUser(value: Boolean, lazyMessage: () } requireAt(value, function, lazyMessage) -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt index 8fe6be589..bcecce56a 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/message/MessageCommandInfoImpl.kt @@ -66,7 +66,7 @@ internal class MessageCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.callSuspend(finalParameters) + methodAccessor.callSuspend(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt index eb85e0227..88fbba992 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/context/user/UserCommandInfoImpl.kt @@ -66,7 +66,7 @@ internal class UserCommandInfoImpl internal constructor( } val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.callSuspend(finalParameters) + methodAccessor.callSuspend(finalParameters) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt index 3eeb43984..e4ec2155a 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt @@ -76,7 +76,7 @@ internal sealed class SlashCommandInfoImpl( internal suspend fun execute(event: GlobalSlashEvent): Boolean { val objects = getSlashOptions(event, parameters) ?: return false - eventFunction.methodAccessor.callSuspend(objects) + methodAccessor.callSuspend(objects) return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteInfoImpl.kt index df9229aef..1aeae31aa 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteInfoImpl.kt @@ -18,6 +18,7 @@ internal class AutocompleteInfoImpl internal constructor( override val name: String? = builder.name internal val eventFunction = builder.function.toMemberParamFunction(context) override val function get() = eventFunction.kFunction + internal val methodAccessor get() = eventFunction.methodAccessor override val mode: AutocompleteMode = builder.mode override val showUserInput: Boolean = builder.showUserInput diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt index d27aa05ce..738c60ecc 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandVariationImpl.kt @@ -89,7 +89,7 @@ internal class TextCommandVariationImpl internal constructor( internal suspend fun execute(event: BaseCommandEvent, optionValues: Map) { val finalParameters = parameters.mapFinalParameters(event, optionValues) - eventFunction.methodAccessor.callSuspend(finalParameters) + methodAccessor.callSuspend(finalParameters) } /** diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt index 347597718..488e2fe6d 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt @@ -87,7 +87,7 @@ internal class ComponentHandlerExecutor internal constructor( return false } - eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) + methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) } return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt index 741b01c19..e08ed03b1 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentTimeoutExecutor.kt @@ -78,7 +78,7 @@ internal class ComponentTimeoutExecutor internal constructor( return false } - eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(firstArgument, optionValues)) + methodAccessor.callSuspend(parameters.mapFinalParameters(firstArgument, optionValues)) } return true } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index 94ad90fb0..109ce857e 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt @@ -20,7 +20,7 @@ internal class AggregatorFunction private constructor( /** * Nullable due to constructor aggregators */ - private val aggregatorInstance: Any?, + aggregatorInstance: Any?, firstParamType: KClass<*> ) : Function(boundAggregator) { init { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt index d95694e4a..8e89fdd4f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt @@ -89,7 +89,7 @@ internal class ModalHandlerInfo internal constructor( throwInternal(::tryInsertOption, "Insertion function shouldn't have been aborted") } - eventFunction.methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) + methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) } private suspend fun tryInsertOption( From 73162e5480a738cf60193bcccdc54fa7d6aa0d98 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:54:44 +0200 Subject: [PATCH 33/47] Do not insert annotations as services --- .../internal/core/service/provider/ServiceProviders.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProviders.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProviders.kt index 3dc8de721..845fe999a 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProviders.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProviders.kt @@ -39,6 +39,7 @@ internal class ServiceProviders : ClassGraphProcessor { isService: Boolean ) { if (!isService) return + if (classInfo.isAnnotation) return putServiceProvider(ClassServiceProvider(kClass)) } @@ -63,4 +64,4 @@ internal class ServiceProviders : ClassGraphProcessor { ?: throwInternal("Cannot get KFunction/KProperty.Getter from $method") putServiceProvider(FunctionServiceProvider(function)) } -} \ No newline at end of file +} From fd28a8a2a1d79d6378320a75b6468e78146592b1 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:55:17 +0200 Subject: [PATCH 34/47] Check for abstract modifier and interface flag on ClassServiceProvider --- .../internal/core/service/provider/ClassServiceProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ClassServiceProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ClassServiceProvider.kt index fb80409e9..99f5846b7 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ClassServiceProvider.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ClassServiceProvider.kt @@ -14,6 +14,7 @@ import io.github.freya022.botcommands.internal.utils.isObject import io.github.freya022.botcommands.internal.utils.shortSignature import io.github.freya022.botcommands.internal.utils.throwInternal import io.github.oshai.kotlinlogging.KotlinLogging +import java.lang.reflect.Modifier import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KVisibility @@ -25,7 +26,7 @@ internal class ClassServiceProvider internal constructor( private val clazz: KClass<*> ) : ServiceProvider { init { - require(!clazz.isAbstract) { + require(!Modifier.isAbstract(clazz.java.modifiers) && !clazz.java.isInterface) { "Abstract class '${clazz.simpleNestedName}' cannot be constructed" } } From e27d2309ec9e064beee7c7170b12490579f6f4e6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:51:24 +0200 Subject: [PATCH 35/47] Pass arguments using an array (`MethodArguments`) instead of a `Map` --- .../application/slash/SlashCommandInfoImpl.kt | 4 +- .../slash/autocomplete/AutocompleteHandler.kt | 2 +- .../core/hooks/EventDispatcherImpl.kt | 13 +-- .../core/hooks/EventHandlerFunction.kt | 14 ++- .../core/reflection/AggregatorFunction.kt | 6 +- .../internal/core/reflection/Function.kt | 3 +- .../internal/core/reflection/ParameterMap.kt | 82 ---------------- .../internal/core/service/Singletons.kt | 3 +- .../core/service/provider/ServiceProvider.kt | 43 +++++--- .../internal/utils/ExecutionUtils.kt | 44 +++++++-- BotCommands-method-accessors/README.md | 19 ++-- .../accessors/MethodAccessorBenchmark.kt | 30 +++--- ...bstractClassFileMethodAccessorGenerator.kt | 28 +++++- .../ClassFileMemberMethodAccessorGenerator.kt | 14 +-- .../ClassFileStaticMethodAccessorGenerator.kt | 20 +--- .../invoker/AbstractInvokerGenerator.kt | 97 ++++++++----------- .../AbstractDefaultInvokerGenerator.kt | 20 ++-- .../DefaultConstructorInvokerGenerator.kt | 4 +- .../default/DefaultMethodInvokerGenerator.kt | 3 +- .../direct/AbstractDirectInvokerGenerator.kt | 16 ++- .../DirectConstructorInvokerGenerator.kt | 6 +- .../direct/DirectMethodInvokerGenerator.kt | 5 +- .../internal/codegen/utils/ClassDescs.kt | 8 +- .../ClassFileMethodAccessorGeneratorTest.kt | 15 ++- .../accessors/internal/MethodArguments.java | 57 +++++++++++ .../accessors/internal/MethodAccessor.kt | 16 ++- .../AbstractKotlinReflectMethodAccessor.kt | 26 +++++ .../internal/KotlinReflectMethodAccessor.kt | 21 ++-- .../KotlinReflectStaticMethodAccessor.kt | 15 +-- 29 files changed, 335 insertions(+), 299 deletions(-) delete mode 100644 BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/ParameterMap.kt create mode 100644 BotCommands-method-accessors/core/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodArguments.java create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt index e4ec2155a..90cf86bc2 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.internal.commands.application.slash +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import io.github.freya022.botcommands.api.commands.INamedCommand import io.github.freya022.botcommands.api.commands.application.slash.GlobalSlashEvent import io.github.freya022.botcommands.api.commands.application.slash.GuildSlashEvent @@ -32,7 +33,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve import net.dv8tion.jda.api.interactions.Interaction import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload import net.dv8tion.jda.api.interactions.commands.OptionMapping -import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure private val logger = KotlinLogging.logger { } @@ -89,7 +89,7 @@ internal sealed class SlashCommandInfoImpl( internal suspend fun ExecutableMixin.getSlashOptions( event: T, parameters: List -): Map? where T : CommandInteractionPayload, T : Event { +): MethodArguments? where T : CommandInteractionPayload, T : Event { val optionValues = parameters.mapOptions { option -> if (tryInsertOption(event, this, option) == InsertOptionResult.ABORT) return null diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt index 5820cbf20..e1575ee9f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/autocomplete/AutocompleteHandler.kt @@ -96,7 +96,7 @@ internal class AutocompleteHandler( ?: return emptyList() //Autocomplete was triggered without all the required parameters being present val actualChoices: MutableList = arrayOfSize(25) - val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.eventFunction.methodAccessor.callSuspend(objects)) + val suppliedChoices = choiceSupplier.apply(event, autocompleteInfo.methodAccessor.callSuspend(objects)) val autoCompleteQuery = event.focusedOption //If something is typed but there are no choices, don't display user input diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt index fc83f36eb..d87d30296 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventDispatcherImpl.kt @@ -13,8 +13,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.* import net.dv8tion.jda.api.events.GenericEvent import java.lang.reflect.InvocationTargetException -import kotlin.reflect.KParameter -import kotlin.reflect.full.valueParameters private val logger = KotlinLogging.logger { } @@ -97,15 +95,8 @@ internal class EventDispatcherImpl internal constructor( try { val classPathFunction = eventHandlerFunction.classPathFunction val methodAccessor = classPathFunction.methodAccessor - val args: Map = buildMap { - classPathFunction.function.valueParameters.forEachIndexed { index, param -> - if (index == 0) { - this[param] = event - } else { - this[param] = eventHandlerFunction.parameters[index - 1] - } - } - } + val args = eventHandlerFunction.cloneBaseArgs() + args[0] = event val timeout = eventHandlerFunction.timeout if (timeout != null) { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventHandlerFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventHandlerFunction.kt index 5bde3ca4c..abe69f7ab 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventHandlerFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventHandlerFunction.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.internal.core.hooks +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.internal.core.ClassPathFunction import kotlin.time.Duration @@ -11,10 +12,19 @@ internal class EventHandlerFunction( val timeout: Duration?, private val parametersBlock: () -> Array ) { - val parameters: Array by lazy { - parametersBlock() + private val baseArgs: MethodArguments by lazy { + val args = classPathFunction.methodAccessor.createBlankArguments() + parametersBlock().forEachIndexed { index, arg -> + // +1 as the first parameter is the event + args[index + 1] = arg + } + args } + // Since the arguments are the same everytime except for the event, + // clone and only change the event on each invocation + internal fun cloneBaseArgs(): MethodArguments = baseArgs.clone() + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index 109ce857e..8ed375eda 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.internal.core.reflection +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.utils.isConstructor import io.github.freya022.botcommands.api.core.utils.isStatic @@ -12,7 +13,6 @@ import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstance import io.github.freya022.botcommands.internal.utils.checkAt import kotlin.reflect.KClass import kotlin.reflect.KFunction -import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure internal class AggregatorFunction private constructor( @@ -47,9 +47,9 @@ internal class AggregatorFunction private constructor( firstParamType: KClass<*> ) : this(aggregator, context.serviceContainer.getFunctionServiceOrNull(aggregator), firstParamType) - internal suspend fun aggregate(firstParam: Any, aggregatorArguments: MutableMap): Any? { + internal suspend fun aggregate(firstParam: Any, aggregatorArguments: MethodArguments): Any? { if (eventParameter != null) { - aggregatorArguments[eventParameter] = firstParam + aggregatorArguments[eventParameter.index] = firstParam } return methodAccessor.callSuspend(aggregatorArguments) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/Function.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/Function.kt index 67fa5c80c..6867bb3f7 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/Function.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/Function.kt @@ -5,7 +5,6 @@ import kotlin.reflect.KFunction internal sealed class Function(boundFunction: KFunction) { internal val kFunction = boundFunction.reflectReference() - internal val parametersSize = kFunction.parameters.size override fun equals(other: Any?): Boolean { if (this === other) return true @@ -17,4 +16,4 @@ internal sealed class Function(boundFunction: KFunction) { } override fun hashCode() = kFunction.hashCode() -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/ParameterMap.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/ParameterMap.kt deleted file mode 100644 index bc3118256..000000000 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/ParameterMap.kt +++ /dev/null @@ -1,82 +0,0 @@ -package io.github.freya022.botcommands.internal.core.reflection - -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter - -private typealias MutableParameterEntry = MutableMap.MutableEntry - -private val NO_VALUE = Any() - -internal class ParameterMap(function: KFunction<*>) : AbstractMutableMap(), - MutableMap { - - private val parameters = function.parameters - private val _values = Array(parameters.size) { NO_VALUE } - - override val entries: MutableSet - get() = Set() - - override fun containsKey(key: KParameter): Boolean { - return _values[key.index] !== NO_VALUE - } - - override fun get(key: KParameter): Any? { - return _values[key.index] - } - - override fun put(key: KParameter, value: Any?): Any? { - val oldVal = _values[key.index] - _values[key.index] = value - return oldVal - } - - private inner class Set : AbstractMutableSet() { - override fun add(element: MutableParameterEntry): Boolean { - return put(element.key, element.value) != element.value - } - - override val size: Int - get() = _values.count { it !== NO_VALUE } - - override fun iterator(): MutableIterator { - return Iterator() - } - } - - private inner class Iterator : MutableIterator { - private var index = 0 - - override fun hasNext(): Boolean { - for (i in index.., block: ParameterMap.() -> Unit): Map { - return ParameterMap(function).apply(block) -} \ No newline at end of file diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt index dc7fe2d5b..429fafe30 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/Singletons.kt @@ -31,6 +31,7 @@ internal object Singletons { "Constructor of ${clazz.shortQualifiedName} must be effectively public (internal is allowed)" } - return MethodAccessorFactoryProvider.getStaticAccessor(constructor).call(mapOf()) + val accessor = MethodAccessorFactoryProvider.getStaticAccessor(constructor) + return accessor.call(accessor.createBlankArguments()) } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt index 25c0086f6..ca931fc3c 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/provider/ServiceProvider.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.internal.core.service.provider +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import io.github.freya022.botcommands.api.core.service.CustomConditionChecker import io.github.freya022.botcommands.api.core.service.ServiceError import io.github.freya022.botcommands.api.core.service.ServiceError.ErrorType @@ -17,8 +18,8 @@ import io.github.freya022.botcommands.internal.utils.throwArgument import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.KFunction -import kotlin.reflect.KParameter import kotlin.reflect.full.instanceParameter +import kotlin.reflect.full.valueParameters import kotlin.reflect.jvm.jvmErasure import kotlin.time.Duration import kotlin.time.measureTimedValue @@ -266,23 +267,37 @@ internal fun KFunction<*>.checkConstructingFunction(serviceContainer: BCServiceC } internal fun KFunction<*>.callConstructingFunction(serviceContainer: BCServiceContainerImpl): TimedInstantiation<*> { - val params: MutableMap = hashMapOf() - this.nonInstanceParameters.forEach { + val instance: Any? = when (val instanceParameter = this.instanceParameter) { + null -> null + else -> { + val instanceErasure = instanceParameter.type.jvmErasure + instanceErasure.objectInstance + ?: serviceContainer.tryGetService(instanceErasure).getOrThrow { + throwArgument(this, "Could not run function as it is not static, the declaring class isn't an object, and service creation failed:\n${it.toDetailedString()}") + } + } + } + + val accessor = MethodAccessorFactoryProvider.getAccessorFactory().create(instance, this) + val args = accessor.createBlankArguments() + this.valueParameters.forEachIndexed { index, parameter -> //Try to get a dependency, if it doesn't work and parameter isn't nullable / cannot be omitted, then return the message - val dependencyResult = serviceContainer.tryGetWrappedService(it) - params[it] = dependencyResult.service ?: when { - it.type.isMarkedNullable -> null - it.isOptional -> return@forEach - else -> throw ServiceException(ErrorType.UNAVAILABLE_PARAMETER.toError( - "Cannot get service for parameter '${it.bestName}' (${it.type.jvmErasure.simpleNestedName})", - failedFunction = this, - nestedError = dependencyResult.serviceError - )) + val dependencyResult = serviceContainer.tryGetWrappedService(parameter) + args[index] = dependencyResult.service ?: when { + parameter.type.isMarkedNullable -> null + parameter.isOptional -> return@forEachIndexed + else -> throw ServiceException( + ErrorType.UNAVAILABLE_PARAMETER.toError( + "Cannot get service for parameter '${parameter.bestName}' (${parameter.type.jvmErasure.simpleNestedName})", + failedFunction = this, + nestedError = dependencyResult.serviceError + ) + ) } } return measureTimedInstantiation { - this.callStatic(serviceContainer, params) + this.callStatic(serviceContainer, args) ?: throw ServiceException(ErrorType.PROVIDER_RETURNED_NULL.toError( errorMessage = "Service factory returned null", failedFunction = this @@ -290,7 +305,7 @@ internal fun KFunction<*>.callConstructingFunction(serviceContainer: BCServiceCo } } -internal fun KFunction.callStatic(serviceContainer: BCServiceContainerImpl, args: MutableMap): R { +internal fun KFunction.callStatic(serviceContainer: BCServiceContainerImpl, args: MethodArguments): R { if (this.isSuspend) { throwArgument(this, "Suspending functions are not supported in this context") } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt index ba54bb1a0..d3290e9c7 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ExecutionUtils.kt @@ -1,8 +1,8 @@ package io.github.freya022.botcommands.internal.utils +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import io.github.freya022.botcommands.internal.ExecutableMixin import io.github.freya022.botcommands.internal.core.options.OptionImpl -import io.github.freya022.botcommands.internal.core.reflection.buildParameters import io.github.freya022.botcommands.internal.parameters.AggregatedParameterMixin import io.github.freya022.botcommands.internal.parameters.MethodParameterMixin import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function @@ -48,15 +48,26 @@ context(executable: ExecutableMixin) internal suspend fun Collection.mapFinalParameters( firstParam: Any, optionValues: Map -) = buildParameters(executable.eventFunction.kFunction) { - this[executable.eventFunction.firstParameter] = firstParam +): MethodArguments { + val args = executable.methodAccessor.createBlankArguments() + args[0] = firstParam for (parameter in this@mapFinalParameters) { - insertAggregate(firstParam, this, optionValues, parameter) + insertAggregate(firstParam, args, optionValues, parameter) } + + return args } -private suspend fun insertAggregate(firstParam: Any, aggregatedObjects: MutableMap, optionValues: Map, parameter: AggregatedParameterMixin) { +// TODO the whole thing with parameters, aggregates and options needs to be refactored +// This isn't even an utility, this is literally the whole logic +// Options and aggregates are set out-of-order, this wasn't a problem when we assigned a Map, +// but now that we use a glorified array to pass our arguments, +// we are keeping track of parameter indexes, which are offset if there is an instance parameter, very ugly. +// We should be using MethodArguments#push instead, so we don't worry about indexes, +// however this requires arguments to be set in the right order. +context(executable: ExecutableMixin) +private suspend fun insertAggregate(firstParam: Any, aggregatedObjects: MethodArguments, optionValues: Map, parameter: AggregatedParameterMixin) { val aggregator = parameter.aggregator if (aggregator.isSingleAggregator) { @@ -67,18 +78,26 @@ private suspend fun insertAggregate(firstParam: Any, aggregatedObjects: MutableM aggregatedObjects[parameter] = optionValues[option] } } else { - val aggregatorArguments: MutableMap = HashMap(aggregator.parametersSize) + val aggregatorArguments = aggregator.methodAccessor.createBlankArguments() var addedOption = false for (option in parameter.options) { //This is necessary to distinguish between null mappings and default mappings if (option in optionValues) { - aggregatorArguments[option] = optionValues[option] + if (aggregator.methodAccessor.hasInstance()) { + aggregatorArguments[option.index - 1] = optionValues[option] + } else { + aggregatorArguments[option.index] = optionValues[option] + } addedOption = true } } // If this is not a vararg, it should throw later when calling the aggregator if (!addedOption && parameter.isVararg) { - aggregatorArguments[parameter.aggregator.kFunction.valueParameters.last()] = emptyList() + if (aggregator.methodAccessor.hasInstance()) { + aggregatorArguments[aggregator.kFunction.valueParameters.last().index - 1] = emptyList() + } else { + aggregatorArguments[aggregator.kFunction.valueParameters.last().index] = emptyList() + } } for (nestedAggregatedParameter in parameter.nestedAggregatedParameters) { @@ -101,8 +120,13 @@ private suspend fun insertAggregate(firstParam: Any, aggregatedObjects: MutableM } } -private operator fun MutableMap.set(parameter: MethodParameterMixin, obj: Any?): Any? = obj.also { - this[parameter.executableParameter] = obj +context(executable: ExecutableMixin) +private operator fun MethodArguments.set(parameter: MethodParameterMixin, obj: Any?): Any? = obj.also { + if (executable.methodAccessor.hasInstance()) { + this[parameter.executableParameter.index - 1] = obj + } else { + this[parameter.executableParameter.index] = obj + } } @Suppress("UNCHECKED_CAST") diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md index 2c7ab0585..ef0d1de2a 100644 --- a/BotCommands-method-accessors/README.md +++ b/BotCommands-method-accessors/README.md @@ -28,10 +28,17 @@ note that this will only have an effect if your bot runs on Java 24+. ### Performance comparison -Performance numbers from [MethodAccessorBenchmark](./classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt) +Performance numbers from [MethodAccessorBenchmark](./classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt), baseline is a direct call: -| Function type | Baseline | ClassFile | kotlin-reflect | -|-----------------------------------------------------------------|-------------|-------------|----------------| -| () -> String | 0.110 µs/op | 0.115 µs/op | 0.227 µs/op | -| (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.177 µs/op | 0.287 µs/op | 0.514 µs/op | -| suspend (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.180 µs/op | 0.288 µs/op | 0.535 µs/op | +| Function type | Baseline | ClassFile | kotlin-reflect | +|-----------------------------------------------------------------|:------------------------:|:-------------------------:|:-------------------------:| +| () -> String | 0.114 µs/op
± 0,003 | 0.115 µs/op
± 0,003 | 0.244 µs/op
± 0,056 | +| (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.179 µs/op
± 0,007 | 0.184 µs/op
± 0,010 | 0.525 µs/op
± 0,039 | +| suspend (a: String, b: Int = 42, c: Double = 3.14159) -> String | 0.183 µs/op
± 0,006 | 0.179 µs/op
± 0,007 | 0.531 µs/op
± 0,030 | + +Note that each benchmark only use one accessor instance, in the real world there would be many more accessors, +meaning that each virtual call becomes non-trivial (see [megamorphic virtual calls](https://shipilev.net/jvm/anatomy-quarks/16-megamorphic-virtual-calls/)) and thus slower, +therefore this benchmark only shows: + +- The custom classes can be as fast as direct calls, but it depends how many implementations there are, among other profiling data +- The overhead of kotlin-reflect diff --git a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt index 8e277e080..2bcab563a 100644 --- a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt +++ b/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -10,6 +10,7 @@ import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters +// TODO move this to BotCommands-method-accessors @Suppress("FunctionName") @OutputTimeUnit(TimeUnit.MICROSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) @@ -20,9 +21,9 @@ open class MethodAccessorBenchmark { private lateinit var instance: MyClass - private lateinit var simpleMethodAccessor: MethodAccessor<*> - private lateinit var methodWithDefaultsAccessor: MethodAccessor<*> - private lateinit var suspendingMethodWithDefaultsAccessor: MethodAccessor<*> + private lateinit var simpleMethodAccessorClassFile: MethodAccessor<*> + private lateinit var methodWithDefaultsAccessorClassFile: MethodAccessor<*> + private lateinit var suspendingMethodWithDefaultsAccessorClassFile: MethodAccessor<*> private lateinit var simpleMethodKotlin: KFunction<*> private lateinit var methodWithDefaultsKotlin: KFunction<*> @@ -40,9 +41,9 @@ open class MethodAccessorBenchmark { methodWithDefaultsKotlin = MyClass::methodWithDefaults suspendingMethodWithDefaultsKotlin = MyClass::suspendingMethodWithDefaults - simpleMethodAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::simpleMethod) - methodWithDefaultsAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::methodWithDefaults) - suspendingMethodWithDefaultsAccessor = ClassFileMethodAccessorFactory().create(instance, MyClass::suspendingMethodWithDefaults) + simpleMethodAccessorClassFile = ClassFileMethodAccessorFactory().create(instance, MyClass::simpleMethod) + methodWithDefaultsAccessorClassFile = ClassFileMethodAccessorFactory().create(instance, MyClass::methodWithDefaults) + suspendingMethodWithDefaultsAccessorClassFile = ClassFileMethodAccessorFactory().create(instance, MyClass::suspendingMethodWithDefaults) } @Benchmark @@ -61,18 +62,23 @@ open class MethodAccessorBenchmark { } @Benchmark - fun simpleMethod_Accessor(): String = runBlocking { - simpleMethodAccessor.call(mapOf()) as String + fun simpleMethod_Accessor_ClassFile(): String = runBlocking { + val args = simpleMethodAccessorClassFile.createBlankArguments() + simpleMethodAccessorClassFile.callSuspend(args) as String } @Benchmark - fun methodWithDefaults_Accessor(): String = runBlocking { - methodWithDefaultsAccessor.call(mapOf(methodWithDefaultsKotlin.valueParameters[0] to sampleString)) as String + fun methodWithDefaults_Accessor_ClassFile(): String = runBlocking { + val args = methodWithDefaultsAccessorClassFile.createBlankArguments() + args[0] = sampleString + methodWithDefaultsAccessorClassFile.callSuspend(args) as String } @Benchmark - fun suspendingMethodWithDefaults_Accessor(): String = runBlocking { - suspendingMethodWithDefaultsAccessor.call(mapOf(suspendingMethodWithDefaultsKotlin.valueParameters[0] to sampleString)) as String + fun suspendingMethodWithDefaults_Accessor_ClassFile(): String = runBlocking { + val args = suspendingMethodWithDefaultsAccessorClassFile.createBlankArguments() + args[0] = sampleString + suspendingMethodWithDefaultsAccessorClassFile.callSuspend(args) as String } @Benchmark diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt index b02bf1094..7da42ecf0 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -8,6 +8,7 @@ import dev.freya02.botcommands.method.accessors.internal.codegen.modality.Suspen import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_Continuation import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_IllegalSuspendCallException import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodAccessor +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodArguments import dev.freya02.botcommands.method.accessors.internal.utils.javaExecutable import java.lang.classfile.ClassBuilder import java.lang.classfile.ClassFile @@ -18,9 +19,11 @@ import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles import java.lang.reflect.AccessFlag +import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Modifier import kotlin.reflect.KFunction +import kotlin.reflect.KParameter internal abstract class AbstractClassFileMethodAccessorGenerator( internal val instance: Any?, @@ -43,12 +46,24 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) classBuilder.withInterfaceSymbols(CD_MethodAccessor) + classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) + classBuilder.withInterfaceSymbols(CD_MethodAccessor) + // TODO replace with class data of hidden class addFields(classBuilder) addConstructor(classBuilder) - classBuilder.withMethodBody("callSuspend", MethodTypeDesc.of(CD_Object, CD_Map, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + classBuilder.withMethodBody("hasInstance", MethodTypeDesc.of(CD_boolean), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + if (isStatic || executable is Constructor<*>) { + codeBuilder.iconst_0() // false, does not have instance parameter + } else { + codeBuilder.iconst_1() // true, has instance parameter + } + codeBuilder.ireturn() + } + + classBuilder.withMethodBody("callSuspend", MethodTypeDesc.of(CD_Object, CD_MethodArguments, CD_Continuation), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> val modalityGenerator = when { function.isSuspend -> SuspendingInvokerGenerator else -> BlockingInvokerGenerator @@ -62,7 +77,7 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( } } - classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_Map), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + classBuilder.withMethodBody("call", MethodTypeDesc.of(CD_Object, CD_MethodArguments), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> if (function.isSuspend) { // throw new IllegalSuspendCallException() codeBuilder.new_(CD_IllegalSuspendCallException) @@ -79,6 +94,15 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( } } } + + classBuilder.withMethodBody("createBlankArguments", MethodTypeDesc.of(CD_MethodArguments), ACC_PUBLIC or ACC_FINAL) { codeBuilder -> + // return new MethodArguments([parameterCount]) + codeBuilder.new_(CD_MethodArguments) + codeBuilder.dup() + codeBuilder.loadConstant(function.parameters.count { it.kind != KParameter.Kind.INSTANCE }) + codeBuilder.invokespecial(CD_MethodArguments, INIT_NAME, MethodTypeDesc.of(CD_void, CD_int)) + codeBuilder.areturn() + } } val clazz = lookup diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt index 806e7d69d..ea503754e 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt @@ -1,7 +1,6 @@ package dev.freya02.botcommands.method.accessors.internal.codegen import dev.freya02.botcommands.method.accessors.internal.MethodAccessor -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction import java.lang.classfile.ClassBuilder import java.lang.classfile.ClassFile.* import java.lang.constant.ConstantDescs.* @@ -17,13 +16,11 @@ internal class ClassFileMemberMethodAccessorGenerator( override fun addFields(classBuilder: ClassBuilder) { classBuilder.withField("instance", instanceDesc, ACC_PRIVATE or ACC_FINAL) - classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) } override fun addConstructor(classBuilder: ClassBuilder) { - classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc), ACC_PUBLIC) { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() - val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) // this.super() codeBuilder.aload(thisSlot) @@ -35,18 +32,13 @@ internal class ClassFileMemberMethodAccessorGenerator( codeBuilder.aload(instanceSlot) codeBuilder.putfield(thisClass, "instance", instanceDesc) - // this.function = function; - codeBuilder.aload(thisSlot) - codeBuilder.aload(functionSlot) - codeBuilder.putfield(thisClass, "function", CD_KFunction) - codeBuilder.return_() } } override fun createInstance(clazz: Class<*>): MethodAccessor<*> { return clazz - .getDeclaredConstructor(instanceClass, KFunction::class.java) - .newInstance(instance, function) as MethodAccessor<*> + .getDeclaredConstructor(instanceClass) + .newInstance(instance) as MethodAccessor<*> } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt index 85352c10a..e2264cec2 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt @@ -1,14 +1,11 @@ package dev.freya02.botcommands.method.accessors.internal.codegen import dev.freya02.botcommands.method.accessors.internal.MethodAccessor -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodAccessor import java.lang.classfile.ClassBuilder -import java.lang.classfile.ClassFile.* +import java.lang.classfile.ClassFile.ACC_PUBLIC import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import java.lang.invoke.MethodHandles -import java.lang.reflect.AccessFlag import kotlin.reflect.KFunction internal class ClassFileStaticMethodAccessorGenerator( @@ -18,33 +15,24 @@ internal class ClassFileStaticMethodAccessorGenerator( ) : AbstractClassFileMethodAccessorGenerator(instance, function, lookup) { override fun addFields(classBuilder: ClassBuilder) { - classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) - classBuilder.withInterfaceSymbols(CD_MethodAccessor) - classBuilder.withField("function", CD_KFunction, ACC_PRIVATE or ACC_FINAL) } override fun addConstructor(classBuilder: ClassBuilder) { - classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, CD_KFunction), ACC_PUBLIC) { codeBuilder -> + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void), ACC_PUBLIC) { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() - val functionSlot = codeBuilder.parameterSlot(if (isStatic) 0 else 1) // this.super() codeBuilder.aload(thisSlot) codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) - // this.function = function; - codeBuilder.aload(thisSlot) - codeBuilder.aload(functionSlot) - codeBuilder.putfield(thisClass, "function", CD_KFunction) - codeBuilder.return_() } } override fun createInstance(clazz: Class<*>): MethodAccessor<*> { return clazz - .getDeclaredConstructor(KFunction::class.java) - .newInstance(function) as MethodAccessor<*> + .getDeclaredConstructor() + .newInstance() as MethodAccessor<*> } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt index e75257d56..67b4e38e5 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt @@ -1,77 +1,66 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.invoker -import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KCallable -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KFunction -import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_KParameter +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.CD_MethodArguments import dev.freya02.botcommands.method.accessors.internal.codegen.utils.unboxOrCastTo import java.lang.classfile.CodeBuilder -import java.lang.constant.ConstantDescs.* +import java.lang.constant.ConstantDescs.CD_Object +import java.lang.constant.ConstantDescs.CD_int import java.lang.constant.MethodTypeDesc internal abstract class AbstractInvokerGenerator : InvokerGenerator { - protected fun AbstractClassFileMethodAccessorGenerator<*>.loadParameter( - codeBuilder: CodeBuilder, - thisSlot: Int, - index: Int, - parameterSlot: Int - ) { - // var parameter = function.getParameters().get([index]) - codeBuilder.aload(thisSlot) - codeBuilder.getfield(thisClass, "function", CD_KFunction) - codeBuilder.invokeinterface(CD_KCallable, "getParameters", MethodTypeDesc.of(CD_List)) - codeBuilder.loadConstant(index) - codeBuilder.invokeinterface(CD_List, "get", MethodTypeDesc.of(CD_Object, CD_int)) - codeBuilder.checkcast(CD_KParameter) - codeBuilder.astore(parameterSlot) - } - protected fun CodeBuilder.loadUnboxedOptional( type: Class<*>, argsSlot: Int, - parameterSlot: Int, + index: Int, maskSlot: Int, valueParameterIndex: Int, ) { - aload(argsSlot) - aload(parameterSlot) - invokeinterface(CD_Map, "containsKey", MethodTypeDesc.of(CD_boolean, CD_Object)) + val realValueLabel = newLabel() + val endLabel = newLabel() + + // <- () args.get(parameter) + loadArg(argsSlot, index) + dup() // So we can cast and keep in stack - // NOTE: Remember to have the same amount of stack data in and out of the branch - ifThenElse( - { - // Key exists, unbox or cast - // <- () args.get(parameter) - loadArg(argsSlot, parameterSlot, type) - }, - { - // Key does not exist, load default - when (type) { - Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> - iconst_0() + // If the value is MethodArguments#NO_VALUE, jump to loading the default + getstatic(CD_MethodArguments, "NO_VALUE", CD_Object) + if_acmpeq(realValueLabel) // NOTE: Remember to have the same amount of stack data in and out of the branch + run { + // Key exists, unbox or cast + // The value may be null, but null can always be cast to any object type + unboxOrCastTo(type) + goto_(endLabel) + } - Long::class.javaPrimitiveType -> lconst_0() - Float::class.javaPrimitiveType -> fconst_0() - Double::class.javaPrimitiveType -> dconst_0() - else -> aconst_null() - } + labelBinding(realValueLabel) + run { + pop() // We don't need the NO_VALUE + // Key does not exist, load default + when (type) { + Boolean::class.javaPrimitiveType, Byte::class.javaPrimitiveType, Char::class.javaPrimitiveType, Short::class.javaPrimitiveType, Int::class.javaPrimitiveType -> + iconst_0() - // Also set our mask bit so the placeholder gets replaced by the default - // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] - iload(maskSlot) - loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) - ior() - istore(maskSlot) + Long::class.javaPrimitiveType -> lconst_0() + Float::class.javaPrimitiveType -> fconst_0() + Double::class.javaPrimitiveType -> dconst_0() + else -> aconst_null() } - ) + + // Also set our mask bit so the placeholder gets replaced by the default + // mask = mask | [1 << (valueParameterIndex % Integer.SIZE)] + iload(maskSlot) + loadConstant(1 shl (valueParameterIndex % Integer.SIZE)) + ior() + istore(maskSlot) + } + + labelBinding(endLabel) } - protected fun CodeBuilder.loadArg(argsSlot: Int, parameterSlot: Int, type: Class<*>) { + protected fun CodeBuilder.loadArg(argsSlot: Int, index: Int) { aload(argsSlot) - aload(parameterSlot) - invokeinterface(CD_Map, "get", MethodTypeDesc.of(CD_Object, CD_Object)) - // The value may be null, but null can always be cast to any object type - unboxOrCastTo(type) + loadConstant(index) + invokevirtual(CD_MethodArguments, "get", MethodTypeDesc.of(CD_Object, CD_int)) } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt index 33bd44512..6eb9668e3 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt @@ -2,6 +2,7 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.defaul import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.unboxOrCastTo import java.lang.classfile.CodeBuilder import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure @@ -9,27 +10,22 @@ import kotlin.reflect.jvm.jvmErasure internal abstract class AbstractDefaultInvokerGenerator : AbstractInvokerGenerator() { protected fun AbstractClassFileMethodAccessorGenerator<*>.loadDefaultParameters( - thisSlot: Int, - parameterSlot: Int, argsSlot: Int, maskSlot: Int, continuationSlot: Int?, codeBuilder: CodeBuilder, ) { - var valueParameterIndex = 0 - function.parameters.forEachIndexed { index, parameter -> - if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed - + var valueParameterIndex = 0 // Used for mask calculation + val nonInstanceParameters = function.parameters.filter { it.kind != KParameter.Kind.INSTANCE } + nonInstanceParameters.forEachIndexed { index, parameter -> val paramJavaType = parameter.type.jvmErasure.java - // var parameter = function.getParameters().get([index]) - loadParameter(codeBuilder, thisSlot, index, parameterSlot) - if (parameter.isOptional) { - codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, parameterSlot, maskSlot, valueParameterIndex) + codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, index, maskSlot, valueParameterIndex) } else { - // = args.get(parameter) - codeBuilder.loadArg(argsSlot, parameterSlot, paramJavaType) + // = args.get([index]) + codeBuilder.loadArg(argsSlot, index) + codeBuilder.unboxOrCastTo(paramJavaType) } valueParameterIndex++ diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt index 4232cc7fb..86a2d8df1 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt @@ -23,10 +23,8 @@ internal object DefaultConstructorInvokerGenerator : AbstractDefaultInvokerGener MethodTypeDesc.of(CD_void, effectiveParameters) } - val thisSlot = codeBuilder.receiverSlot() val argsSlot = codeBuilder.parameterSlot(0) - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) // maskSlot = 0 @@ -38,7 +36,7 @@ internal object DefaultConstructorInvokerGenerator : AbstractDefaultInvokerGener codeBuilder.dup() // So we can return it // .""([params], mask, null) - loadDefaultParameters(thisSlot, parameterSlot, argsSlot, maskSlot, continuationSlot, codeBuilder) + loadDefaultParameters(argsSlot, maskSlot, continuationSlot, codeBuilder) codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt index e0214ee4e..15f54378d 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt @@ -29,7 +29,6 @@ internal object DefaultMethodInvokerGenerator : AbstractDefaultInvokerGenerator( val thisSlot = codeBuilder.receiverSlot() val argsSlot = codeBuilder.parameterSlot(0) - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val maskSlot = codeBuilder.allocateLocal(TypeKind.INT) // maskSlot = 0 @@ -41,7 +40,7 @@ internal object DefaultMethodInvokerGenerator : AbstractDefaultInvokerGenerator( codeBuilder.aload(thisSlot) codeBuilder.getfield(thisClass, "instance", instanceDesc) } - loadDefaultParameters(thisSlot, parameterSlot, argsSlot, maskSlot, continuationSlot, codeBuilder) + loadDefaultParameters(argsSlot, maskSlot, continuationSlot, codeBuilder) codeBuilder.invokestatic(instanceDesc, $$"$${executable.name}$default", methodTypeDesc) } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt index ee5ea22f5..ce732726e 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt @@ -2,6 +2,7 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator import dev.freya02.botcommands.method.accessors.internal.codegen.invoker.AbstractInvokerGenerator +import dev.freya02.botcommands.method.accessors.internal.codegen.utils.unboxOrCastTo import java.lang.classfile.CodeBuilder import kotlin.reflect.KParameter import kotlin.reflect.jvm.jvmErasure @@ -9,19 +10,14 @@ import kotlin.reflect.jvm.jvmErasure internal abstract class AbstractDirectInvokerGenerator : AbstractInvokerGenerator() { protected fun AbstractClassFileMethodAccessorGenerator<*>.loadParameters( - thisSlot: Int, - parameterSlot: Int, argsSlot: Int, codeBuilder: CodeBuilder, ) { - function.parameters.forEachIndexed { index, parameter -> - if (parameter.kind != KParameter.Kind.VALUE) return@forEachIndexed - - // var parameter = function.getParameters().get([index]) - loadParameter(codeBuilder, thisSlot, index, parameterSlot) - - // = args.get(parameter) - codeBuilder.loadArg(argsSlot, parameterSlot, parameter.type.jvmErasure.java) + val nonInstanceParameters = function.parameters.filter { it.kind != KParameter.Kind.INSTANCE } + nonInstanceParameters.forEachIndexed { index, parameter -> + // = () args.get([index]) + codeBuilder.loadArg(argsSlot, index) + codeBuilder.unboxOrCastTo(parameter.type.jvmErasure.java) } } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt index ec04c5fd6..865462f2e 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt @@ -2,7 +2,6 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator import java.lang.classfile.CodeBuilder -import java.lang.classfile.TypeKind import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.ConstantDescs.INIT_NAME import java.lang.constant.MethodTypeDesc @@ -22,17 +21,14 @@ internal object DirectConstructorInvokerGenerator : AbstractDirectInvokerGenerat MethodTypeDesc.of(CD_void, parameterDescs) } - val thisSlot = codeBuilder.receiverSlot() val argsSlot = codeBuilder.parameterSlot(0) - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - // new [className]([params]) codeBuilder.new_(instanceDesc) codeBuilder.dup() // So we can return it // .``([params]) - loadParameters(thisSlot, parameterSlot, argsSlot, codeBuilder) + loadParameters(argsSlot, codeBuilder) codeBuilder.invokespecial(instanceDesc, INIT_NAME, methodTypeDesc) } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt index 453656caf..8ac30dd4d 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt @@ -2,7 +2,6 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.invoker.direct import dev.freya02.botcommands.method.accessors.internal.codegen.AbstractClassFileMethodAccessorGenerator import java.lang.classfile.CodeBuilder -import java.lang.classfile.TypeKind import java.lang.constant.MethodTypeDesc import java.lang.reflect.Method @@ -23,14 +22,12 @@ internal object DirectMethodInvokerGenerator : AbstractDirectInvokerGenerator() val thisSlot = codeBuilder.receiverSlot() val argsSlot = codeBuilder.parameterSlot(0) - val parameterSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - // this.instance.[methodName]([params]) if (!isStatic) { codeBuilder.aload(thisSlot) codeBuilder.getfield(thisClass, "instance", instanceDesc) } - loadParameters(thisSlot, parameterSlot, argsSlot, codeBuilder) + loadParameters(argsSlot, codeBuilder) if (continuationSlot != null) codeBuilder.aload(continuationSlot) if (isStatic) { codeBuilder.invokestatic(instanceDesc, executable.name, methodTypeDesc) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt index 854156514..7c795bd3d 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -2,25 +2,21 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.utils import dev.freya02.botcommands.method.accessors.internal.MethodAccessor import dev.freya02.botcommands.method.accessors.internal.MethodAccessorContinuation +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import java.lang.constant.ClassDesc import kotlin.coroutines.Continuation -import kotlin.reflect.KCallable -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter internal val CD_IllegalStateException = ClassDesc.of(IllegalStateException::class.java.name) internal val CD_Unit = ClassDesc.of(Unit::class.java.name) internal val CD_DefaultConstructorMarker = ClassDesc.of("kotlin.jvm.internal.DefaultConstructorMarker") internal val CD_Continuation = ClassDesc.of(Continuation::class.java.name) -internal val CD_KCallable = ClassDesc.of(KCallable::class.java.name) -internal val CD_KFunction = ClassDesc.of(KFunction::class.java.name) -internal val CD_KParameter = ClassDesc.of(KParameter::class.java.name) internal val CD_ResultKt = ClassDesc.of("kotlin.ResultKt") internal val CD_IntrinsicsKt = ClassDesc.of("kotlin.coroutines.intrinsics.IntrinsicsKt") internal val CD_DebugProbesKt = ClassDesc.of("kotlin.coroutines.jvm.internal.DebugProbesKt") internal val CD_MethodAccessor = ClassDesc.of(MethodAccessor::class.java.name) internal val CD_MethodAccessorContinuation = ClassDesc.of(MethodAccessorContinuation::class.java.name) +internal val CD_MethodArguments = ClassDesc.of(MethodArguments::class.java.name) internal val CD_IllegalSuspendCallException = ClassDesc.of(IllegalSuspendCallException::class.java.name) diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt index 23fe5b02e..1f07bee91 100644 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt @@ -11,7 +11,6 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments.argumentSet import org.junit.jupiter.params.provider.MethodSource import kotlin.reflect.KFunction -import kotlin.reflect.full.valueParameters import kotlin.test.Test import kotlin.time.Duration.Companion.milliseconds @@ -92,11 +91,11 @@ object ClassFileMethodAccessorGeneratorTest { fun `Generate method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { runBlocking { val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.callSuspend(buildMap { - args.forEachIndexed { index, arg -> - this[function.valueParameters[index]] = arg - } - }) + val args = methodAccessor.createBlankArguments().also { + args.forEach { arg -> it.push(arg) } + } + + methodAccessor.callSuspend(args) } } @@ -106,14 +105,14 @@ object ClassFileMethodAccessorGeneratorTest { val instance = TestClass() val function = TestClass::coRun val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.call(mapOf()) + methodAccessor.call(methodAccessor.createBlankArguments()) } assertDoesNotThrow { val instance = TestClass() val function = TestClass::run val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.call(mapOf()) + methodAccessor.call(methodAccessor.createBlankArguments()) } } diff --git a/BotCommands-method-accessors/core/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodArguments.java b/BotCommands-method-accessors/core/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodArguments.java new file mode 100644 index 000000000..c23bafdff --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodArguments.java @@ -0,0 +1,57 @@ +package dev.freya02.botcommands.method.accessors.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +public class MethodArguments implements Cloneable { + + public static final Object NO_VALUE = new Object(); + + private final Object[] args; + private int cursor = 0; + + // Built by the accessor + MethodArguments(int size) { + final var args = new Object[size]; + Arrays.fill(args, NO_VALUE); + this.args = args; + } + + @Override + public MethodArguments clone() { + try { + final MethodArguments clone = (MethodArguments) super.clone(); + clone.cursor = 0; + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + public void push(@Nullable Object value) { + args[cursor] = value; + cursor += 1; + } + + // TODO use `push` instead to avoid messing with offsets caused by instance parameters + // n.b. not all usages will be replaceable + public void set(int index, @Nullable Object value) { + args[index] = value; + } + + @Nullable + public Object get(int index) { + return args[index]; + } + + @NotNull + public Object[] getArgs() { + return args; + } + + public int size() { + return args.length; + } +} diff --git a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt index 20560250e..8a1d93267 100644 --- a/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt @@ -1,10 +1,18 @@ package dev.freya02.botcommands.method.accessors.internal -import kotlin.reflect.KParameter - interface MethodAccessor { - suspend fun callSuspend(args: Map): R + /** + * `true` if this method requires an instance parameter + * + * This is mostly used to offset the argument indexes in [MethodArguments], + * as the instance is already inserted by the accessor + */ + fun hasInstance(): Boolean + + suspend fun callSuspend(args: MethodArguments): R + + fun call(args: MethodArguments): R - fun call(args: Map): R + fun createBlankArguments(): MethodArguments } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt new file mode 100644 index 000000000..d5d71a8c3 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt @@ -0,0 +1,26 @@ +package dev.freya02.botcommands.method.accessors.internal + +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +// TODO create more subclasses which are optimized for certain cases (direct/with defaults) +internal abstract class AbstractKotlinReflectMethodAccessor( + protected val function: KFunction, +) : MethodAccessor { + + protected val parameters = function.parameters.filter { it.kind != KParameter.Kind.INSTANCE } + private val parameterCount = parameters.size + + override fun createBlankArguments(): MethodArguments { + return MethodArguments(parameterCount) + } + + protected inline fun argsToMap(args: MethodArguments, block: MutableMap.() -> Unit = {}): Map { + return buildMap(args.size()) { + block() + parameters.forEachIndexed { index, parameter -> + this[parameter] = args[index] + } + } + } +} diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt index 955fb8492..9aa53c154 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt @@ -2,29 +2,32 @@ package dev.freya02.botcommands.method.accessors.internal import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction -import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter internal class KotlinReflectMethodAccessor internal constructor( private val instance: Any, - private val function: KFunction, -) : MethodAccessor { + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { private val instanceParameter = function.instanceParameter!! - override suspend fun callSuspend(args: Map): R { - val args = args.toMutableMap() - args.putIfAbsent(instanceParameter, instance) + override fun hasInstance(): Boolean = true + + override suspend fun callSuspend(args: MethodArguments): R { + val args = argsToMap(args) { + put(instanceParameter, instance) + } return function.callSuspendBy(args) } - override fun call(args: Map): R { + override fun call(args: MethodArguments): R { if (function.isSuspend) throw IllegalSuspendCallException() - val args = args.toMutableMap() - args.putIfAbsent(instanceParameter, instance) + val args = argsToMap(args) { + put(instanceParameter, instance) + } return function.callBy(args) } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt index 1aee8d661..9677e88f6 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt @@ -2,19 +2,20 @@ package dev.freya02.botcommands.method.accessors.internal import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction -import kotlin.reflect.KParameter import kotlin.reflect.full.callSuspendBy internal class KotlinReflectStaticMethodAccessor internal constructor( - private val function: KFunction, -) : MethodAccessor { + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { - override suspend fun callSuspend(args: Map): R { - return function.callSuspendBy(args) + override fun hasInstance(): Boolean = false + + override suspend fun callSuspend(args: MethodArguments): R { + return function.callSuspendBy(argsToMap(args)) } - override fun call(args: Map): R { + override fun call(args: MethodArguments): R { if (function.isSuspend) throw IllegalSuspendCallException() - return function.callBy(args) + return function.callBy(argsToMap(args)) } } From c199bdd26558671bb664d05786ada97050dbb5bd Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:56:59 +0200 Subject: [PATCH 36/47] Move benchmarks to `:BotCommands-method-accessors` --- BotCommands-method-accessors/.gitignore | 1 + BotCommands-method-accessors/build.gradle.kts | 30 +++++++++++++++++++ .../classfile/.gitignore | 1 - .../classfile/build.gradle.kts | 11 +------ .../kotlin-reflect/build.gradle.kts | 2 +- .../accessors/MethodAccessorBenchmark.kt | 0 settings.gradle.kts | 1 + 7 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 BotCommands-method-accessors/.gitignore create mode 100644 BotCommands-method-accessors/build.gradle.kts rename BotCommands-method-accessors/{classfile => }/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt (100%) diff --git a/BotCommands-method-accessors/.gitignore b/BotCommands-method-accessors/.gitignore new file mode 100644 index 000000000..08ab34c52 --- /dev/null +++ b/BotCommands-method-accessors/.gitignore @@ -0,0 +1 @@ +/reports diff --git a/BotCommands-method-accessors/build.gradle.kts b/BotCommands-method-accessors/build.gradle.kts new file mode 100644 index 000000000..f9d752d1a --- /dev/null +++ b/BotCommands-method-accessors/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("BotCommands-conventions") + + alias(libs.plugins.jmh) +} + +dependencies { + implementation(projects.botCommandsMethodAccessors.classfile) + implementation(projects.botCommandsMethodAccessors.kotlinReflect) +} + +jmh { + // See https://github.com/melix/jmh-gradle-plugin?tab=readme-ov-file#configuration-options + failOnError = true // Should JMH fail immediately if any benchmark had experienced the unrecoverable error? + humanOutputFile = project.file("reports/jmh/human.txt") // human-readable output file + resultsFile = project.file("reports/jmh/results.txt") // results file +} + +java { + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_24 + } +} diff --git a/BotCommands-method-accessors/classfile/.gitignore b/BotCommands-method-accessors/classfile/.gitignore index f9d4ad1cc..6b468b62a 100644 --- a/BotCommands-method-accessors/classfile/.gitignore +++ b/BotCommands-method-accessors/classfile/.gitignore @@ -1,2 +1 @@ -/reports *.class diff --git a/BotCommands-method-accessors/classfile/build.gradle.kts b/BotCommands-method-accessors/classfile/build.gradle.kts index 690038c91..6abb36444 100644 --- a/BotCommands-method-accessors/classfile/build.gradle.kts +++ b/BotCommands-method-accessors/classfile/build.gradle.kts @@ -3,19 +3,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("BotCommands-conventions") id("BotCommands-publish-conventions") - - alias(libs.plugins.jmh) } dependencies { - implementation(projects.botCommandsMethodAccessors.core) -} - -jmh { - // See https://github.com/melix/jmh-gradle-plugin?tab=readme-ov-file#configuration-options - failOnError = true // Should JMH fail immediately if any benchmark had experienced the unrecoverable error? - humanOutputFile = project.file("reports/jmh/human.txt") // human-readable output file - resultsFile = project.file("reports/jmh/results.txt") // results file + api(projects.botCommandsMethodAccessors.core) } java { diff --git a/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts index c815467d0..be47b16b7 100644 --- a/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts +++ b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } dependencies { - implementation(projects.botCommandsMethodAccessors.core) + api(projects.botCommandsMethodAccessors.core) } configurePublishedArtifact(artifactId = "BotCommands-method-accessors-kotlin-reflect") diff --git a/BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt similarity index 100% rename from BotCommands-method-accessors/classfile/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt rename to BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 87cd43d43..65226a684 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":BotCommands-core") include( + ":BotCommands-method-accessors", ":BotCommands-method-accessors:core", ":BotCommands-method-accessors:classfile", ":BotCommands-method-accessors:kotlin-reflect", From f5c32a9c69a00699c1d4e094e7bc6e7ad2bb1205 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:35:42 +0200 Subject: [PATCH 37/47] Check for `MethodArguments.NO_VALUE` in kotlin-reflect accessor --- .../internal/AbstractKotlinReflectMethodAccessor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt index d5d71a8c3..502cbde3a 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt @@ -19,7 +19,10 @@ internal abstract class AbstractKotlinReflectMethodAccessor( return buildMap(args.size()) { block() parameters.forEachIndexed { index, parameter -> - this[parameter] = args[index] + val arg = args[index] + if (arg != MethodArguments.NO_VALUE) { + this[parameter] = arg + } } } } From d4194d987c11c852e8c195bad8dddb3b3d68fca7 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:36:47 +0200 Subject: [PATCH 38/47] Add specialized instances for direct/default calls of kotlin-reflect --- .../KotlinReflectMethodAccessorFactory.kt | 16 ++++++++++-- .../KotlinReflectDefaultMethodAccessor.kt} | 6 +++-- ...tlinReflectDefaultStaticMethodAccessor.kt} | 6 +++-- .../KotlinReflectDirectMethodAccessor.kt | 25 +++++++++++++++++++ ...KotlinReflectDirectStaticMethodAccessor.kt | 23 +++++++++++++++++ 5 files changed, 70 insertions(+), 6 deletions(-) rename BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/{KotlinReflectMethodAccessor.kt => invoker/default/KotlinReflectDefaultMethodAccessor.kt} (74%) rename BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/{KotlinReflectStaticMethodAccessor.kt => invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt} (65%) create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectMethodAccessor.kt create mode 100644 BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectStaticMethodAccessor.kt diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt index 10c32cde8..c707aef67 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -1,5 +1,9 @@ package dev.freya02.botcommands.method.accessors.internal +import dev.freya02.botcommands.method.accessors.internal.invoker.default.KotlinReflectDefaultMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.invoker.default.KotlinReflectDefaultStaticMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.invoker.direct.KotlinReflectDirectMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.invoker.direct.KotlinReflectDirectStaticMethodAccessor import kotlin.reflect.KFunction import kotlin.reflect.full.instanceParameter @@ -12,9 +16,17 @@ class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { return if (function.instanceParameter != null) { requireNotNull(instance) - KotlinReflectMethodAccessor(instance, function) + if (function.parameters.any { it.isOptional }) { + KotlinReflectDefaultMethodAccessor(instance, function) + } else { + KotlinReflectDirectMethodAccessor(instance, function) + } } else { - KotlinReflectStaticMethodAccessor(function) + if (function.parameters.any { it.isOptional }) { + KotlinReflectDefaultStaticMethodAccessor(function) + } else { + KotlinReflectDirectStaticMethodAccessor(function) + } } } } diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultMethodAccessor.kt similarity index 74% rename from BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt rename to BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultMethodAccessor.kt index 9aa53c154..00b6b6371 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultMethodAccessor.kt @@ -1,11 +1,13 @@ -package dev.freya02.botcommands.method.accessors.internal +package dev.freya02.botcommands.method.accessors.internal.invoker.default +import dev.freya02.botcommands.method.accessors.internal.AbstractKotlinReflectMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter -internal class KotlinReflectMethodAccessor internal constructor( +internal class KotlinReflectDefaultMethodAccessor internal constructor( private val instance: Any, function: KFunction, ) : AbstractKotlinReflectMethodAccessor(function) { diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt similarity index 65% rename from BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt rename to BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt index 9677e88f6..121f52a68 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectStaticMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt @@ -1,10 +1,12 @@ -package dev.freya02.botcommands.method.accessors.internal +package dev.freya02.botcommands.method.accessors.internal.invoker.default +import dev.freya02.botcommands.method.accessors.internal.AbstractKotlinReflectMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.MethodArguments import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException import kotlin.reflect.KFunction import kotlin.reflect.full.callSuspendBy -internal class KotlinReflectStaticMethodAccessor internal constructor( +internal class KotlinReflectDefaultStaticMethodAccessor internal constructor( function: KFunction, ) : AbstractKotlinReflectMethodAccessor(function) { diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectMethodAccessor.kt new file mode 100644 index 000000000..418d930bf --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectMethodAccessor.kt @@ -0,0 +1,25 @@ +package dev.freya02.botcommands.method.accessors.internal.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.AbstractKotlinReflectMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.MethodArguments +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException +import kotlin.reflect.KFunction +import kotlin.reflect.full.callSuspend + +internal class KotlinReflectDirectMethodAccessor internal constructor( + private val instance: Any, + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { + + override fun hasInstance(): Boolean = true + + override suspend fun callSuspend(args: MethodArguments): R { + return function.callSuspend(instance, *args.args) + } + + override fun call(args: MethodArguments): R { + if (function.isSuspend) throw IllegalSuspendCallException() + + return function.call(instance, *args.args) + } +} diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectStaticMethodAccessor.kt new file mode 100644 index 000000000..f7be14bbe --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/direct/KotlinReflectDirectStaticMethodAccessor.kt @@ -0,0 +1,23 @@ +package dev.freya02.botcommands.method.accessors.internal.invoker.direct + +import dev.freya02.botcommands.method.accessors.internal.AbstractKotlinReflectMethodAccessor +import dev.freya02.botcommands.method.accessors.internal.MethodArguments +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException +import kotlin.reflect.KFunction +import kotlin.reflect.full.callSuspend + +internal class KotlinReflectDirectStaticMethodAccessor internal constructor( + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { + + override fun hasInstance(): Boolean = false + + override suspend fun callSuspend(args: MethodArguments): R { + return function.callSuspend(*args.args) + } + + override fun call(args: MethodArguments): R { + if (function.isSuspend) throw IllegalSuspendCallException() + return function.call(*args.args) + } +} From 76bdbd47ffe1f89d0d8cc7580a9e2b96cb29ae8d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:55:46 +0200 Subject: [PATCH 39/47] Move tests to root module --- .../ClassFileMethodAccessorGeneratorTest.kt | 140 --------------- .../method/accessors/MethodAccessorTest.kt | 159 ++++++++++++++++++ 2 files changed, 159 insertions(+), 140 deletions(-) delete mode 100644 BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt create mode 100644 BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt diff --git a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt b/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt deleted file mode 100644 index 1f07bee91..000000000 --- a/BotCommands-method-accessors/classfile/src/test/kotlin/dev/freya02/botcommands/method/accessors/ClassFileMethodAccessorGeneratorTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -package dev.freya02.botcommands.method.accessors - -import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory -import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.Arguments.argumentSet -import org.junit.jupiter.params.provider.MethodSource -import kotlin.reflect.KFunction -import kotlin.test.Test -import kotlin.time.Duration.Companion.milliseconds - -interface TestInterface { - - fun run() { - - } -} - -object TestStatic { - - @JvmStatic - fun run() { - - } -} - -class TestConstructor(arg: Int) - -class TestConstructorWithDefaults(arg: Int = 2) - -class TestClass { - - fun run() { - - } - - fun runWithArgs(arg: String) { - - } - - fun runWithUnboxing(boolean: Boolean, byte: Byte, char: Char, short: Short, int: Int, long: Long, float: Float, double: Double) { - - } - - fun runWithDefaults(arg: Int = 2) { - - } - - fun runWithMoreDefaults(a: String, b: Int = 42, c: Double = 3.14159) { - - } - - fun runWithNullOptional(arg: Int? = 2) { - require(arg == null) { "The expected argument was null but the accessor replaced it with a value ($arg)" } - } - - fun runWithReturnType(): Int { - return 1 - } - - fun runWithReturnTypeWithDefaults(arg: Int = 2): Int { - return arg - } - - suspend fun coRun() { - - } - - suspend fun coRunWithDefaults(int: Int = 2) { - - } - - suspend fun coRunWithSuspensionPoints(a: Int, b: Int): Int { - delay(10.milliseconds) - val c = a + b - delay(10.milliseconds) - println("$a + $b = $c") - return c - } -} - -object ClassFileMethodAccessorGeneratorTest { - - @MethodSource("testCallers") - @ParameterizedTest - fun `Generate method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { - runBlocking { - val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - val args = methodAccessor.createBlankArguments().also { - args.forEach { arg -> it.push(arg) } - } - - methodAccessor.callSuspend(args) - } - } - - @Test - fun `'call' throws on non-suspend functions`() { - assertThrows { - val instance = TestClass() - val function = TestClass::coRun - val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.call(methodAccessor.createBlankArguments()) - } - - assertDoesNotThrow { - val instance = TestClass() - val function = TestClass::run - val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) - methodAccessor.call(methodAccessor.createBlankArguments()) - } - } - - @JvmStatic - fun testCallers(): List = listOf( - argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), - argumentSet("1-arg method", TestClass(), TestClass::runWithArgs, listOf("foobar")), - argumentSet("1-arg constructor", null, ::TestConstructor, listOf(1)), - argumentSet("Constructor with defaults", null, ::TestConstructorWithDefaults, listOf()), - argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), - argumentSet("From interface", object : TestInterface { }, TestInterface::run, listOf()), - argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), - argumentSet("With static modifier", null, TestStatic::run, listOf()), - argumentSet("With static modifier and instance", TestStatic, TestStatic::run, listOf()), - argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), - argumentSet("With more defaults", TestClass(), TestClass::runWithMoreDefaults, listOf("foobar")), - argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), - argumentSet("With optional parameter set to null", TestClass(), TestClass::runWithNullOptional, listOf(null)), - - argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), - argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), - argumentSet("Suspend with overridden defaults", TestClass(), TestClass::coRunWithDefaults, listOf(3)), - argumentSet("Suspend with suspension points", TestClass(), TestClass::coRunWithSuspensionPoints, listOf(1, 1)), - ) -} diff --git a/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt b/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt new file mode 100644 index 000000000..e2f795291 --- /dev/null +++ b/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt @@ -0,0 +1,159 @@ +package dev.freya02.botcommands.method.accessors + +import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.KotlinReflectMethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.MethodAccessorFactory +import dev.freya02.botcommands.method.accessors.internal.exceptions.IllegalSuspendCallException +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KFunction +import kotlin.time.Duration.Companion.milliseconds + +object MethodAccessorTest { + + @MethodSource("testCallers") + @ParameterizedTest + fun `Generate ClassFile method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { + runBlocking { + val methodAccessor = ClassFileMethodAccessorFactory().create(instance, function) + val args = methodAccessor.createBlankArguments().also { + args.forEach { arg -> it.push(arg) } + } + + methodAccessor.callSuspend(args) + } + } + + @MethodSource("testCallers") + @ParameterizedTest + fun `Generate kotlin-reflect method accessors and call them`(instance: Any?, function: KFunction<*>, args: List) { + runBlocking { + val methodAccessor = KotlinReflectMethodAccessorFactory().create(instance, function) + val args = methodAccessor.createBlankArguments().also { + args.forEach { arg -> it.push(arg) } + } + + methodAccessor.callSuspend(args) + } + } + + @JvmStatic + fun testCallers(): List = listOf( + Arguments.argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), + Arguments.argumentSet("1-arg method", TestClass(), TestClass::runWithArgs, listOf("foobar")), + Arguments.argumentSet("1-arg constructor", null, ::TestConstructor, listOf(1)), + Arguments.argumentSet("Constructor with defaults", null, ::TestConstructorWithDefaults, listOf()), + Arguments.argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), + Arguments.argumentSet("From interface", object : TestInterface {}, TestInterface::run, listOf()), + Arguments.argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), + Arguments.argumentSet("With static modifier", null, TestStatic::run, listOf()), + Arguments.argumentSet("With static modifier and instance", TestStatic, TestStatic::run, listOf()), + Arguments.argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), + Arguments.argumentSet("With more defaults", TestClass(), TestClass::runWithMoreDefaults, listOf("foobar")), + Arguments.argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), + Arguments.argumentSet("With optional parameter set to null", TestClass(), TestClass::runWithNullOptional, listOf(null)), + Arguments.argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), + Arguments.argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), + Arguments.argumentSet("Suspend with overridden defaults", TestClass(), TestClass::coRunWithDefaults, listOf(3)), + Arguments.argumentSet("Suspend with suspension points", TestClass(), TestClass::coRunWithSuspensionPoints, listOf(1, 1)), + ) + + @MethodSource("factories") + @ParameterizedTest + fun `'call' throws on non-suspend functions`(factory: MethodAccessorFactory) { + assertThrows { + val instance = TestClass() + val function = TestClass::coRun + val methodAccessor = factory.create(instance, function) + methodAccessor.call(methodAccessor.createBlankArguments()) + } + + assertDoesNotThrow { + val instance = TestClass() + val function = TestClass::run + val methodAccessor = factory.create(instance, function) + methodAccessor.call(methodAccessor.createBlankArguments()) + } + } + + @JvmStatic + fun factories() = listOf( + Arguments.argumentSet("ClassFile", ClassFileMethodAccessorFactory()), + Arguments.argumentSet("kotlin-reflect", KotlinReflectMethodAccessorFactory()), + ) +} + +interface TestInterface { + + fun run() { + + } +} + +object TestStatic { + + @JvmStatic + fun run() { + + } +} + +class TestConstructor(arg: Int) + +class TestConstructorWithDefaults(arg: Int = 2) + +class TestClass { + + fun run() { + + } + + fun runWithArgs(arg: String) { + + } + + fun runWithUnboxing(boolean: Boolean, byte: Byte, char: Char, short: Short, int: Int, long: Long, float: Float, double: Double) { + + } + + fun runWithDefaults(arg: Int = 2) { + + } + + fun runWithMoreDefaults(a: String, b: Int = 42, c: Double = 3.14159) { + + } + + fun runWithNullOptional(arg: Int? = 2) { + require(arg == null) { "The expected argument was null but the accessor replaced it with a value ($arg)" } + } + + fun runWithReturnType(): Int { + return 1 + } + + fun runWithReturnTypeWithDefaults(arg: Int = 2): Int { + return arg + } + + suspend fun coRun() { + + } + + suspend fun coRunWithDefaults(int: Int = 2) { + + } + + suspend fun coRunWithSuspensionPoints(a: Int, b: Int): Int { + delay(10.milliseconds) + val c = a + b + delay(10.milliseconds) + println("$a + $b = $c") + return c + } +} From 0a1676165a1e0c1f74e264e613e3d3b79fe2e76a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:02:00 +0200 Subject: [PATCH 40/47] Updated README.md --- BotCommands-method-accessors/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md index ef0d1de2a..55339e891 100644 --- a/BotCommands-method-accessors/README.md +++ b/BotCommands-method-accessors/README.md @@ -40,5 +40,7 @@ Note that each benchmark only use one accessor instance, in the real world there meaning that each virtual call becomes non-trivial (see [megamorphic virtual calls](https://shipilev.net/jvm/anatomy-quarks/16-megamorphic-virtual-calls/)) and thus slower, therefore this benchmark only shows: -- The custom classes can be as fast as direct calls, but it depends how many implementations there are, among other profiling data +- The custom classes can be as fast as direct calls* - The overhead of kotlin-reflect + +*Only if the JVM can accurately figure out which accessor implementation is called, as our use case call many different handlers, it cannot be optimized so well, this caveat applies equally to virtual calls and reflection calls From 9c051a8dbcd06c93cdea0fed2b200326fc2d5d56 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:07:50 +0200 Subject: [PATCH 41/47] Typo --- BotCommands-method-accessors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md index 55339e891..99550ef76 100644 --- a/BotCommands-method-accessors/README.md +++ b/BotCommands-method-accessors/README.md @@ -43,4 +43,4 @@ therefore this benchmark only shows: - The custom classes can be as fast as direct calls* - The overhead of kotlin-reflect -*Only if the JVM can accurately figure out which accessor implementation is called, as our use case call many different handlers, it cannot be optimized so well, this caveat applies equally to virtual calls and reflection calls +*Only if the JVM can accurately figure out which accessor implementation is called, as our use case can call many different handlers, it cannot be optimized so well, this caveat applies equally to virtual calls and reflection calls From 65291da883cc98b5cbb1cbe1544bccf8b35474da Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:47:30 +0200 Subject: [PATCH 42/47] Reword readme --- BotCommands-method-accessors/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md index 99550ef76..dc7211258 100644 --- a/BotCommands-method-accessors/README.md +++ b/BotCommands-method-accessors/README.md @@ -40,7 +40,15 @@ Note that each benchmark only use one accessor instance, in the real world there meaning that each virtual call becomes non-trivial (see [megamorphic virtual calls](https://shipilev.net/jvm/anatomy-quarks/16-megamorphic-virtual-calls/)) and thus slower, therefore this benchmark only shows: -- The custom classes can be as fast as direct calls* +- The custom classes can be as fast as direct calls - The overhead of kotlin-reflect -*Only if the JVM can accurately figure out which accessor implementation is called, as our use case can call many different handlers, it cannot be optimized so well, this caveat applies equally to virtual calls and reflection calls +### Performance in use-case + +Due to the nature of the use case, the accessors will not be as fast as the numbers above may make it look like, but it won't be slow either. + +For example, a slash command handler will need to call the appropriate user-defined method, and each method has its own accessor, +meaning that, at the call site (where the method is called), the JVM has no clue which accessor it has to call! + +Consequently, it will have to determine which implementation of the accessor it has to call (remember, 1 method = 1 implementation). +Also note that this issue is *due to the use case*, a direct interface call, a custom accessor or a reflection call, would all have the same slowdown. From 3578ecfab215305fc4932ecfdbe97eb99af93f91 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:30:13 +0200 Subject: [PATCH 43/47] Support value classes --- .../invoker/AbstractInvokerGenerator.kt | 4 +- .../AbstractDefaultInvokerGenerator.kt | 7 ++-- .../direct/AbstractDirectInvokerGenerator.kt | 5 ++- .../internal/codegen/utils/CodeBuilder.kt | 14 ++++++- .../method/accessors/MethodAccessorTest.kt | 42 +++++++++++++++++-- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt index 67b4e38e5..dabd41e56 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt @@ -6,11 +6,13 @@ import java.lang.classfile.CodeBuilder import java.lang.constant.ConstantDescs.CD_Object import java.lang.constant.ConstantDescs.CD_int import java.lang.constant.MethodTypeDesc +import kotlin.reflect.KClass internal abstract class AbstractInvokerGenerator : InvokerGenerator { protected fun CodeBuilder.loadUnboxedOptional( type: Class<*>, + kotlinErasure: KClass<*>, argsSlot: Int, index: Int, maskSlot: Int, @@ -29,7 +31,7 @@ internal abstract class AbstractInvokerGenerator : InvokerGenerator { run { // Key exists, unbox or cast // The value may be null, but null can always be cast to any object type - unboxOrCastTo(type) + unboxOrCastTo(type, kotlinErasure) goto_(endLabel) } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt index 6eb9668e3..fac1c6952 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt @@ -17,15 +17,16 @@ internal abstract class AbstractDefaultInvokerGenerator : AbstractInvokerGenerat ) { var valueParameterIndex = 0 // Used for mask calculation val nonInstanceParameters = function.parameters.filter { it.kind != KParameter.Kind.INSTANCE } + val javaParameterTypes = executable.parameterTypes nonInstanceParameters.forEachIndexed { index, parameter -> - val paramJavaType = parameter.type.jvmErasure.java + val javaParameterType = javaParameterTypes[index] if (parameter.isOptional) { - codeBuilder.loadUnboxedOptional(paramJavaType, argsSlot, index, maskSlot, valueParameterIndex) + codeBuilder.loadUnboxedOptional(javaParameterType, parameter.type.jvmErasure, argsSlot, index, maskSlot, valueParameterIndex) } else { // = args.get([index]) codeBuilder.loadArg(argsSlot, index) - codeBuilder.unboxOrCastTo(paramJavaType) + codeBuilder.unboxOrCastTo(javaParameterType, parameter.type.jvmErasure) } valueParameterIndex++ diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt index ce732726e..31b58e54c 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt @@ -14,10 +14,13 @@ internal abstract class AbstractDirectInvokerGenerator : AbstractInvokerGenerato codeBuilder: CodeBuilder, ) { val nonInstanceParameters = function.parameters.filter { it.kind != KParameter.Kind.INSTANCE } + val javaParameterTypes = executable.parameterTypes nonInstanceParameters.forEachIndexed { index, parameter -> + val javaParameterType = javaParameterTypes[index] + // = () args.get([index]) codeBuilder.loadArg(argsSlot, index) - codeBuilder.unboxOrCastTo(parameter.type.jvmErasure.java) + codeBuilder.unboxOrCastTo(javaParameterType, parameter.type.jvmErasure) } } } diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt index 7f46d543f..d80734995 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -3,6 +3,7 @@ package dev.freya02.botcommands.method.accessors.internal.codegen.utils import java.lang.classfile.CodeBuilder import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc +import kotlin.reflect.KClass /** * Consumes the top stack value @@ -24,7 +25,18 @@ internal fun CodeBuilder.ifNull(onNull: () -> Unit, onNonNull: () -> Unit) { labelBinding(resumeLabel) } -internal fun CodeBuilder.unboxOrCastTo(target: Class<*>) { +/** + * NOTE: [target] != [kotlinErasure].java due to value classes, do not pass KClass.java + */ +internal fun CodeBuilder.unboxOrCastTo(target: Class<*>, kotlinErasure: KClass<*>) { + if (kotlinErasure.isValue) { + val valuePropertyDesc = kotlinErasure.java.declaredFields[0].type.describeConstable().get() + val valueClassDesc = kotlinErasure.java.describeConstable().get() + checkcast(valueClassDesc) + invokevirtual(valueClassDesc, "unbox-impl", MethodTypeDesc.of(valuePropertyDesc)) + return + } + when (target) { Boolean::class.javaPrimitiveType -> { checkcast(CD_Boolean) diff --git a/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt b/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt index e2f795291..80db9bbfe 100644 --- a/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt +++ b/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt @@ -47,16 +47,23 @@ object MethodAccessorTest { Arguments.argumentSet("0-arg method", TestClass(), TestClass::run, listOf()), Arguments.argumentSet("1-arg method", TestClass(), TestClass::runWithArgs, listOf("foobar")), Arguments.argumentSet("1-arg constructor", null, ::TestConstructor, listOf(1)), + Arguments.argumentSet("Inline class arg", TestClass(), TestClass::runWithInlineClassArg, listOf(InlineDouble(3.14159))), + Arguments.argumentSet("Nested inline class arg", TestClass(), TestClass::runWithNestedInlineClassArg, listOf(NestedInlineDouble(InlineDouble(3.14159)))), Arguments.argumentSet("Constructor with defaults", null, ::TestConstructorWithDefaults, listOf()), Arguments.argumentSet("Unboxing", TestClass(), TestClass::runWithUnboxing, listOf(true, 1.toByte(), 1.toChar(), 1.toShort(), 1, 1.toLong(), 1.toFloat(), 1.toDouble())), Arguments.argumentSet("From interface", object : TestInterface {}, TestInterface::run, listOf()), Arguments.argumentSet("With return type", TestClass(), TestClass::runWithReturnType, listOf()), + Arguments.argumentSet("With inline class return type", TestClass(), TestClass::runWithInlineClassReturnType, listOf()), + Arguments.argumentSet("With nested inline class return type", TestClass(), TestClass::runWithNestedInlineClassReturnType, listOf()), Arguments.argumentSet("With static modifier", null, TestStatic::run, listOf()), Arguments.argumentSet("With static modifier and instance", TestStatic, TestStatic::run, listOf()), Arguments.argumentSet("With defaults", TestClass(), TestClass::runWithDefaults, listOf()), + Arguments.argumentSet("With inline class default", TestClass(), TestClass::runWithDefaultInlineClassArg, listOf()), + Arguments.argumentSet("With nested inline class default", TestClass(), TestClass::runWithDefaultNestedInlineClassArg, listOf()), Arguments.argumentSet("With more defaults", TestClass(), TestClass::runWithMoreDefaults, listOf("foobar")), Arguments.argumentSet("With overridden defaults", TestClass(), TestClass::runWithDefaults, listOf(3)), Arguments.argumentSet("With optional parameter set to null", TestClass(), TestClass::runWithNullOptional, listOf(null)), + Arguments.argumentSet("0-arg suspend method", TestClass(), TestClass::coRun, listOf()), Arguments.argumentSet("Suspend with defaults", TestClass(), TestClass::coRunWithDefaults, listOf()), Arguments.argumentSet("Suspend with overridden defaults", TestClass(), TestClass::coRunWithDefaults, listOf(3)), @@ -137,16 +144,37 @@ class TestClass { return 1 } - fun runWithReturnTypeWithDefaults(arg: Int = 2): Int { - return arg + fun runWithInlineClassArg(arg: InlineDouble) { + } - suspend fun coRun() { + fun runWithNestedInlineClassArg(arg: NestedInlineDouble) { + + } + + fun runWithDefaultInlineClassArg(arg: InlineDouble = InlineDouble(2.0)) { } - suspend fun coRunWithDefaults(int: Int = 2) { + fun runWithDefaultNestedInlineClassArg(arg: NestedInlineDouble = NestedInlineDouble(InlineDouble(2.0))) { + + } + + fun runWithInlineClassReturnType(): InlineDouble { + return InlineDouble(2.0) + } + fun runWithNestedInlineClassReturnType(): NestedInlineDouble { + return NestedInlineDouble(InlineDouble(2.0)) + } + + suspend fun coRun() { + delay(10.milliseconds) + } + + suspend fun coRunWithDefaults(int: Int = 2): Int { + delay(10.milliseconds) + return int } suspend fun coRunWithSuspensionPoints(a: Int, b: Int): Int { @@ -157,3 +185,9 @@ class TestClass { return c } } + +@JvmInline +value class NestedInlineDouble(val value: InlineDouble) + +@JvmInline +value class InlineDouble(val value: Double) From 90cded3008ba0afdc9f15ddd48409fd711d5f8ef Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:30:47 +0200 Subject: [PATCH 44/47] Update .gitignore --- BotCommands-method-accessors/.gitignore | 1 + BotCommands-method-accessors/classfile/.gitignore | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 BotCommands-method-accessors/classfile/.gitignore diff --git a/BotCommands-method-accessors/.gitignore b/BotCommands-method-accessors/.gitignore index 08ab34c52..f9d4ad1cc 100644 --- a/BotCommands-method-accessors/.gitignore +++ b/BotCommands-method-accessors/.gitignore @@ -1 +1,2 @@ /reports +*.class diff --git a/BotCommands-method-accessors/classfile/.gitignore b/BotCommands-method-accessors/classfile/.gitignore deleted file mode 100644 index 6b468b62a..000000000 --- a/BotCommands-method-accessors/classfile/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.class From 431a4122b1ec38b1c4090a525adf78003ecaa954 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:31:13 +0200 Subject: [PATCH 45/47] Remove unused CodeBuilder.ifNull --- .../internal/codegen/utils/CodeBuilder.kt | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt index d80734995..4220dd1d2 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -5,26 +5,6 @@ import java.lang.constant.ConstantDescs.* import java.lang.constant.MethodTypeDesc import kotlin.reflect.KClass -/** - * Consumes the top stack value - */ -internal fun CodeBuilder.ifNull(onNull: () -> Unit, onNonNull: () -> Unit) { - val ifNullLabel = newLabel() - val resumeLabel = newLabel() - - // If stack top value is null then jump - ifnull(ifNullLabel) - // At this point the value is non-null - onNonNull() - goto_(resumeLabel) // Skip null case - - labelBinding(ifNullLabel) - // At this point the value is null - onNull() - - labelBinding(resumeLabel) -} - /** * NOTE: [target] != [kotlinErasure].java due to value classes, do not pass KClass.java */ From 880bcc18ef45c852af5c0885b2a39c919e163dc2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:51:07 +0200 Subject: [PATCH 46/47] Explicitly prevent optional parameters in event listeners Current arguments are all loaded in order, can't have holes in them --- .../internal/core/hooks/EventListenerRegistry.kt | 1 + .../botcommands/internal/utils/FunctionFilter.kt | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventListenerRegistry.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventListenerRegistry.kt index 70511bbf1..8b40f3856 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventListenerRegistry.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/hooks/EventListenerRegistry.kt @@ -80,6 +80,7 @@ internal class EventListenerRegistry internal constructor( private fun Collection.addAsEventListeners() = this .requiredFilter(FunctionFilter.nonStatic()) .requiredFilter(FunctionFilter.firstArg(GenericEvent::class, BGenericEvent::class)) + .requiredFilter(FunctionFilter.noOptional()) .forEach { classPathFunc -> val function = classPathFunc.function val annotation = function.findAnnotationRecursive() diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/FunctionFilter.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/FunctionFilter.kt index 8903d2b20..d0039f3c7 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/FunctionFilter.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/FunctionFilter.kt @@ -99,8 +99,15 @@ internal abstract class FunctionFilter { override fun filter(function: Function): Boolean = function.hasAnnotationRecursive() } + + fun noOptional() = object : FunctionFilter() { + override val errorMessage: String + get() = "Function must have no optional parameter" + + override fun filter(function: Function): Boolean = function.parameters.none { it.isOptional } + } } } internal fun C.withFilter(filter: FunctionFilter) = this.filter { filter(it, false) } -internal fun C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it, true) } \ No newline at end of file +internal fun C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it, true) } From 15e6f6191c3997e239b54cb2ab9ba13794072046 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:57:49 +0200 Subject: [PATCH 47/47] Clean up --- .../internal/codegen/AbstractClassFileMethodAccessorGenerator.kt | 1 - .../accessors/internal/AbstractKotlinReflectMethodAccessor.kt | 1 - .../botcommands/method/accessors/MethodAccessorBenchmark.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt index 7da42ecf0..6307de987 100644 --- a/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -49,7 +49,6 @@ internal abstract class AbstractClassFileMethodAccessorGenerator( classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) classBuilder.withInterfaceSymbols(CD_MethodAccessor) - // TODO replace with class data of hidden class addFields(classBuilder) addConstructor(classBuilder) diff --git a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt index 502cbde3a..9f8515b0e 100644 --- a/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt @@ -3,7 +3,6 @@ package dev.freya02.botcommands.method.accessors.internal import kotlin.reflect.KFunction import kotlin.reflect.KParameter -// TODO create more subclasses which are optimized for certain cases (direct/with defaults) internal abstract class AbstractKotlinReflectMethodAccessor( protected val function: KFunction, ) : MethodAccessor { diff --git a/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt b/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt index 2bcab563a..2b5772aae 100644 --- a/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt +++ b/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -10,7 +10,6 @@ import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters -// TODO move this to BotCommands-method-accessors @Suppress("FunctionName") @OutputTimeUnit(TimeUnit.MICROSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)