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..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 @@ -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 @@ -28,6 +29,34 @@ 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:JvmStatic + @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. + */ + @JvmStatic + @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/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 4af5d5019..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 @@ -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) + 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 0582ba068..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 @@ -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) + 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 d01ae00db..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,8 +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.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) + methodAccessor.callSuspend(objects) return true } @@ -90,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 be341db6d..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 @@ -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.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/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 5410516ed..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 @@ -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) + 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 682efb3ce..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 @@ -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)) + methodAccessor.callSuspend(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..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 @@ -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)) + methodAccessor.callSuspend(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..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,6 +1,8 @@ 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 java.lang.reflect.Method @@ -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..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,7 +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.full.callSuspend private val logger = KotlinLogging.logger { } @@ -94,19 +93,22 @@ 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 = eventHandlerFunction.cloneBaseArgs() + args[0] = event 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.callSuspend(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.callSuspend(args) } } catch (e: InvocationTargetException) { if (event is InitializationEvent) { 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/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/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..c600ce0d6 --- /dev/null +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt @@ -0,0 +1,51 @@ +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 { + + private var logged = false + + private val kotlinReflectAccessorFactory: MethodAccessorFactory = KotlinReflectMethodAccessorFactory() + private val classFileAccessorFactory: MethodAccessorFactory? by lazy { + if (Runtime.version().feature() >= 24) { + ClassFileMethodAccessorFactory() + } else { + null + } + } + + private val staticAccessors: MutableMap, MethodAccessor<*>> = hashMapOf() + + @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 + } + } + + 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/reflection/AggregatorFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/AggregatorFunction.kt index 913cca425..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,21 +1,18 @@ 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 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( @@ -23,7 +20,7 @@ internal class AggregatorFunction private constructor( /** * Nullable due to constructor aggregators */ - private val aggregatorInstance: Any?, + aggregatorInstance: Any?, firstParamType: KClass<*> ) : Function(boundAggregator) { init { @@ -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() @@ -50,19 +47,14 @@ internal class AggregatorFunction private constructor( firstParamType: KClass<*> ) : 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") - } - + internal suspend fun aggregate(firstParam: Any, aggregatorArguments: MethodArguments): Any? { if (eventParameter != null) { - aggregatorArguments[eventParameter] = firstParam + aggregatorArguments[eventParameter.index] = firstParam } - return aggregator.callSuspendBy(aggregatorArguments) + return methodAccessor.callSuspend(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/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/MemberFunction.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/reflection/MemberFunction.kt index 725869a6a..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 @@ -1,23 +1,21 @@ 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 kotlin.reflect.KFunction -import kotlin.reflect.full.instanceParameter -import kotlin.reflect.full.valueParameters internal open class MemberFunction internal constructor( boundFunction: KFunction, 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 - ?: 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") } -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/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 ac0168bf5..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 @@ -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,7 @@ internal object Singletons { "Constructor of ${clazz.shortQualifiedName} must be effectively public (internal is allowed)" } - return constructor.callBy(mapOf()) + val accessor = MethodAccessorFactoryProvider.getStaticAccessor(constructor) + return accessor.call(accessor.createBlankArguments()) } -} \ No newline at end of file +} 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" } } 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..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,11 +1,13 @@ 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 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 @@ -16,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 @@ -265,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 @@ -289,22 +305,21 @@ 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") } 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) } } } 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 +} 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..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 @@ -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)) + methodAccessor.callSuspend(parameters.mapFinalParameters(event, optionValues)) } private suspend fun tryInsertOption( 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..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,16 +48,26 @@ context(executable: ExecutableMixin) internal suspend fun Collection.mapFinalParameters( firstParam: Any, optionValues: Map -) = buildParameters(executable.eventFunction.kFunction) { - this[executable.eventFunction.instanceParameter] = executable.instance - 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) { @@ -68,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) { @@ -102,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") @@ -115,4 +138,4 @@ private operator fun MutableMap.set(option: OptionImpl, obj: A } else { this[option.executableParameter] = obj } -} \ No newline at end of file +} 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) } 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 diff --git a/BotCommands-method-accessors/.gitignore b/BotCommands-method-accessors/.gitignore new file mode 100644 index 000000000..f9d4ad1cc --- /dev/null +++ b/BotCommands-method-accessors/.gitignore @@ -0,0 +1,2 @@ +/reports +*.class diff --git a/BotCommands-method-accessors/README.md b/BotCommands-method-accessors/README.md new file mode 100644 index 000000000..dc7211258 --- /dev/null +++ b/BotCommands-method-accessors/README.md @@ -0,0 +1,54 @@ +# 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 + +Stack trace of kotlin-reflect call + +#### ClassFile + +Stack trace of custom caller + +### Performance comparison + +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.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 +- The overhead of kotlin-reflect + +### 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. 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 000000000..69437d28d Binary files /dev/null and b/BotCommands-method-accessors/assets/stack-trace-classfile.avif differ diff --git a/BotCommands-method-accessors/assets/stack-trace-kotlin-reflect.avif b/BotCommands-method-accessors/assets/stack-trace-kotlin-reflect.avif new file mode 100644 index 000000000..5dcf117e7 Binary files /dev/null and b/BotCommands-method-accessors/assets/stack-trace-kotlin-reflect.avif differ 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/build.gradle.kts b/BotCommands-method-accessors/classfile/build.gradle.kts new file mode 100644 index 000000000..6abb36444 --- /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 { + api(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/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..b49fec6a3 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/java/dev/freya02/botcommands/method/accessors/internal/MethodAccessorContinuation.java @@ -0,0 +1,32 @@ +package dev.freya02.botcommands.method.accessors.internal; + +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.callSuspend(null, this); + } +} 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..3f92f8bb3 --- /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 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?, + function: KFunction, + ): MethodAccessor { + val executable = function.javaExecutable + 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/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..6307de987 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/AbstractClassFileMethodAccessorGenerator.kt @@ -0,0 +1,120 @@ +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_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 +import java.lang.classfile.ClassFile.ACC_FINAL +import java.lang.classfile.ClassFile.ACC_PUBLIC +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.Executable +import java.lang.reflect.Modifier +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +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) + + classBuilder.withFlags(AccessFlag.PUBLIC, AccessFlag.FINAL) + classBuilder.withInterfaceSymbols(CD_MethodAccessor) + + addFields(classBuilder) + + addConstructor(classBuilder) + + 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 + } + with(modalityGenerator) { + val invokerGenerator = when { + function.parameters.any { it.isOptional } -> DefaultInvokerGenerator + else -> DirectInvokerGenerator + } + generate(invokerGenerator, 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) + 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) + } + } + } + + 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 + .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..ea503754e --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMemberMethodAccessorGenerator.kt @@ -0,0 +1,44 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.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 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) + } + + override fun addConstructor(classBuilder: ClassBuilder) { + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void, instanceDesc), ACC_PUBLIC) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + // 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) + + codeBuilder.return_() + } + } + + override fun createInstance(clazz: Class<*>): MethodAccessor<*> { + return clazz + .getDeclaredConstructor(instanceClass) + .newInstance(instance) 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 new file mode 100644 index 000000000..4b5a4b3ef --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileMethodAccessorGenerator.kt @@ -0,0 +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.utils.javaExecutable +import java.lang.invoke.MethodHandles +import java.lang.reflect.Modifier +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +internal object ClassFileMethodAccessorGenerator { + + 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 generator = when { + Modifier.isStatic(function.javaExecutable.modifiers) -> ClassFileStaticMethodAccessorGenerator(instance, function, lookup) + else -> ClassFileMemberMethodAccessorGenerator(instance, function, lookup) + } + return generator.generate() + } +} 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..e2264cec2 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/ClassFileStaticMethodAccessorGenerator.kt @@ -0,0 +1,38 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen + +import dev.freya02.botcommands.method.accessors.internal.MethodAccessor +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassFile.ACC_PUBLIC +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.invoke.MethodHandles +import kotlin.reflect.KFunction + +internal class ClassFileStaticMethodAccessorGenerator( + instance: Any?, + function: KFunction, + lookup: MethodHandles.Lookup, +) : AbstractClassFileMethodAccessorGenerator(instance, function, lookup) { + + override fun addFields(classBuilder: ClassBuilder) { + + } + + override fun addConstructor(classBuilder: ClassBuilder) { + classBuilder.withMethodBody(INIT_NAME, MethodTypeDesc.of(CD_void), ACC_PUBLIC) { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + // this.super() + codeBuilder.aload(thisSlot) + codeBuilder.invokespecial(CD_Object, INIT_NAME, MethodTypeDesc.of(CD_void)) + + codeBuilder.return_() + } + } + + override fun createInstance(clazz: Class<*>): MethodAccessor<*> { + return clazz + .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 new file mode 100644 index 000000000..dabd41e56 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/AbstractInvokerGenerator.kt @@ -0,0 +1,68 @@ +package dev.freya02.botcommands.method.accessors.internal.codegen.invoker + +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.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, + valueParameterIndex: Int, + ) { + val realValueLabel = newLabel() + val endLabel = newLabel() + + // <- () args.get(parameter) + loadArg(argsSlot, index) + dup() // So we can cast and keep in stack + + // 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, kotlinErasure) + goto_(endLabel) + } + + 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() + + 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, index: Int) { + aload(argsSlot) + 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/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..fac1c6952 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/AbstractDefaultInvokerGenerator.kt @@ -0,0 +1,38 @@ +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 dev.freya02.botcommands.method.accessors.internal.codegen.utils.unboxOrCastTo +import java.lang.classfile.CodeBuilder +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.jvmErasure + +internal abstract class AbstractDefaultInvokerGenerator : AbstractInvokerGenerator() { + + protected fun AbstractClassFileMethodAccessorGenerator<*>.loadDefaultParameters( + argsSlot: Int, + maskSlot: Int, + continuationSlot: Int?, + codeBuilder: CodeBuilder, + ) { + 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 javaParameterType = javaParameterTypes[index] + + if (parameter.isOptional) { + codeBuilder.loadUnboxedOptional(javaParameterType, parameter.type.jvmErasure, argsSlot, index, maskSlot, valueParameterIndex) + } else { + // = args.get([index]) + codeBuilder.loadArg(argsSlot, index) + codeBuilder.unboxOrCastTo(javaParameterType, parameter.type.jvmErasure) + } + + 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..86a2d8df1 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultConstructorInvokerGenerator.kt @@ -0,0 +1,42 @@ +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 argsSlot = codeBuilder.parameterSlot(0) + + 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(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..15f54378d --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/default/DefaultMethodInvokerGenerator.kt @@ -0,0 +1,46 @@ +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 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(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..31b58e54c --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/AbstractDirectInvokerGenerator.kt @@ -0,0 +1,26 @@ +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 + +internal abstract class AbstractDirectInvokerGenerator : AbstractInvokerGenerator() { + + protected fun AbstractClassFileMethodAccessorGenerator<*>.loadParameters( + argsSlot: Int, + 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(javaParameterType, parameter.type.jvmErasure) + } + } +} 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..865462f2e --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectConstructorInvokerGenerator.kt @@ -0,0 +1,34 @@ +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.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 argsSlot = codeBuilder.parameterSlot(0) + + // new [className]([params]) + codeBuilder.new_(instanceDesc) + codeBuilder.dup() // So we can return it + + // .``([params]) + 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/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..8ac30dd4d --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/invoker/direct/DirectMethodInvokerGenerator.kt @@ -0,0 +1,40 @@ +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.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) + + // this.instance.[methodName]([params]) + if (!isStatic) { + codeBuilder.aload(thisSlot) + codeBuilder.getfield(thisClass, "instance", instanceDesc) + } + loadParameters(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) + } + } +} 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..7c795bd3d --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/ClassDescs.kt @@ -0,0 +1,22 @@ +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 + +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_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/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..4220dd1d2 --- /dev/null +++ b/BotCommands-method-accessors/classfile/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/codegen/utils/CodeBuilder.kt @@ -0,0 +1,87 @@ +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 + +/** + * 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) + 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()) + } + } +} + +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/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/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/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/api/annotations/ExperimentalMethodAccessorsApi.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt new file mode 100644 index 000000000..9ea14373c --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/api/annotations/ExperimentalMethodAccessorsApi.kt @@ -0,0 +1,31 @@ +package dev.freya02.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=dev.freya02.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/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt new file mode 100644 index 000000000..8a1d93267 --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessor.kt @@ -0,0 +1,18 @@ +package dev.freya02.botcommands.method.accessors.internal + +interface MethodAccessor { + + /** + * `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 createBlankArguments(): MethodArguments +} 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 new file mode 100644 index 000000000..875000219 --- /dev/null +++ b/BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/MethodAccessorFactory.kt @@ -0,0 +1,8 @@ +package dev.freya02.botcommands.method.accessors.internal + +import kotlin.reflect.KFunction + +interface MethodAccessorFactory { + + fun create(instance: Any?, function: KFunction): MethodAccessor +} 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/build.gradle.kts b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts new file mode 100644 index 000000000..be47b16b7 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("BotCommands-conventions") + id("BotCommands-publish-conventions") +} + +dependencies { + api(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/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..9f8515b0e --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/AbstractKotlinReflectMethodAccessor.kt @@ -0,0 +1,28 @@ +package dev.freya02.botcommands.method.accessors.internal + +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +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 -> + val arg = args[index] + if (arg != MethodArguments.NO_VALUE) { + this[parameter] = arg + } + } + } + } +} 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..c707aef67 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/KotlinReflectMethodAccessorFactory.kt @@ -0,0 +1,32 @@ +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 + +class KotlinReflectMethodAccessorFactory : MethodAccessorFactory { + + override fun create( + instance: Any?, + function: KFunction, + ): MethodAccessor { + return if (function.instanceParameter != null) { + requireNotNull(instance) + + if (function.parameters.any { it.isOptional }) { + KotlinReflectDefaultMethodAccessor(instance, function) + } else { + KotlinReflectDirectMethodAccessor(instance, function) + } + } else { + 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/invoker/default/KotlinReflectDefaultMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultMethodAccessor.kt new file mode 100644 index 000000000..00b6b6371 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultMethodAccessor.kt @@ -0,0 +1,36 @@ +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 KotlinReflectDefaultMethodAccessor internal constructor( + private val instance: Any, + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { + + private val instanceParameter = function.instanceParameter!! + + 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: MethodArguments): R { + if (function.isSuspend) throw IllegalSuspendCallException() + + 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/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt new file mode 100644 index 000000000..121f52a68 --- /dev/null +++ b/BotCommands-method-accessors/kotlin-reflect/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/invoker/default/KotlinReflectDefaultStaticMethodAccessor.kt @@ -0,0 +1,23 @@ +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 KotlinReflectDefaultStaticMethodAccessor internal constructor( + function: KFunction, +) : AbstractKotlinReflectMethodAccessor(function) { + + override fun hasInstance(): Boolean = false + + override suspend fun callSuspend(args: MethodArguments): R { + return function.callSuspendBy(argsToMap(args)) + } + + override fun call(args: MethodArguments): R { + if (function.isSuspend) throw IllegalSuspendCallException() + return function.callBy(argsToMap(args)) + } +} 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) + } +} 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 new file mode 100644 index 000000000..2b5772aae --- /dev/null +++ b/BotCommands-method-accessors/src/jmh/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorBenchmark.kt @@ -0,0 +1,115 @@ +package dev.freya02.botcommands.method.accessors + +import dev.freya02.botcommands.method.accessors.internal.ClassFileMethodAccessorFactory +import dev.freya02.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 simpleMethodAccessorClassFile: MethodAccessor<*> + private lateinit var methodWithDefaultsAccessorClassFile: MethodAccessor<*> + private lateinit var suspendingMethodWithDefaultsAccessorClassFile: 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 + + simpleMethodAccessorClassFile = ClassFileMethodAccessorFactory().create(instance, MyClass::simpleMethod) + methodWithDefaultsAccessorClassFile = ClassFileMethodAccessorFactory().create(instance, MyClass::methodWithDefaults) + suspendingMethodWithDefaultsAccessorClassFile = 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_ClassFile(): String = runBlocking { + val args = simpleMethodAccessorClassFile.createBlankArguments() + simpleMethodAccessorClassFile.callSuspend(args) as String + } + + @Benchmark + fun methodWithDefaults_Accessor_ClassFile(): String = runBlocking { + val args = methodWithDefaultsAccessorClassFile.createBlankArguments() + args[0] = sampleString + methodWithDefaultsAccessorClassFile.callSuspend(args) as String + } + + @Benchmark + fun suspendingMethodWithDefaults_Accessor_ClassFile(): String = runBlocking { + val args = suspendingMethodWithDefaultsAccessorClassFile.createBlankArguments() + args[0] = sampleString + suspendingMethodWithDefaultsAccessorClassFile.callSuspend(args) 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/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..80db9bbfe --- /dev/null +++ b/BotCommands-method-accessors/src/test/kotlin/dev/freya02/botcommands/method/accessors/MethodAccessorTest.kt @@ -0,0 +1,193 @@ +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("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)), + 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 runWithInlineClassArg(arg: InlineDouble) { + + } + + fun runWithNestedInlineClassArg(arg: NestedInlineDouble) { + + } + + fun runWithDefaultInlineClassArg(arg: InlineDouble = InlineDouble(2.0)) { + + } + + 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 { + delay(10.milliseconds) + val c = a + b + delay(10.milliseconds) + println("$a + $b = $c") + return c + } +} + +@JvmInline +value class NestedInlineDouble(val value: InlineDouble) + +@JvmInline +value class InlineDouble(val value: Double) 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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index a81053e8b..65226a684 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,12 @@ rootProject.name = "BotCommands" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":BotCommands-core") +include( + ":BotCommands-method-accessors", + ":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")