Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0b90bed
Minimum implementation of method accessors
freya022 Aug 29, 2025
799ca0d
Support static methods
freya022 Aug 29, 2025
25433b6
Support default arguments
freya022 Aug 29, 2025
f063fb2
Simplify unboxing or loading default value for optional parameters
freya022 Aug 29, 2025
edafc50
Always return `Unit.INSTANCE` in generated `MethodAccessor#call`
freya022 Aug 29, 2025
799c580
Refactor ifnull branching out
freya022 Aug 30, 2025
7e9c74f
Cleanup
freya022 Aug 30, 2025
24f01c6
Support return values
freya022 Aug 30, 2025
f3276c7
Start adding support for coroutines
freya022 Aug 30, 2025
c4362cb
Finish support for coroutines
freya022 Aug 30, 2025
230aaa8
Use Map#containsKey to determine if an optional parameter was set
freya022 Aug 31, 2025
3a4833a
Push `null` if optional parameter is an absent reference
freya022 Aug 31, 2025
8428c6d
More tests
freya022 Aug 31, 2025
66d0c57
Add simple benchmark
freya022 Aug 31, 2025
4dd97a2
Integrate method accessors
freya022 Aug 31, 2025
8d8018b
Update core module package
freya022 Aug 31, 2025
453f604
Add README.md
freya022 Aug 31, 2025
3329fee
Clean up
freya022 Aug 31, 2025
4e4cb65
Use `BotCommands.preferClassFileAccessors()`
freya022 Aug 31, 2025
de666d0
Support static methods with `null` instances
freya022 Aug 31, 2025
e22c4c4
Support constructors
freya022 Aug 31, 2025
a893c98
Use method accessors when calling aggregators
freya022 Aug 31, 2025
be9d8f3
Resize stack trace comparisons, update `alt`
freya022 Aug 31, 2025
c461c95
Refactor ClassFileMethodAccessorGenerator to use properties instead o…
freya022 Aug 31, 2025
24f84b6
Split `ClassFileMethodAccessorGenerator` to better represent the code…
freya022 Aug 31, 2025
cf1380a
Add missing `@JvmStatic`
freya022 Aug 31, 2025
f392197
Rename `MethodAccessor#call` to `callSuspend`
freya022 Sep 1, 2025
e672a7e
Add `MethodAccessor#call` for blocking calls
freya022 Sep 1, 2025
1ecdfd5
Replace `KFunction#callBy` with `MethodAccessor#call`
freya022 Sep 1, 2025
6894518
Remove unnecessary instance parameter assignments
freya022 Sep 1, 2025
a1f8f59
Add missing star projection
freya022 Sep 1, 2025
b41fc87
Simplify accessing method accessors
freya022 Sep 1, 2025
73162e5
Do not insert annotations as services
freya022 Sep 1, 2025
fd28a8a
Check for abstract modifier and interface flag on ClassServiceProvider
freya022 Sep 1, 2025
e27d230
Pass arguments using an array (`MethodArguments`) instead of a `Map<K…
freya022 Sep 2, 2025
c199bdd
Move benchmarks to `:BotCommands-method-accessors`
freya022 Sep 2, 2025
f5c32a9
Check for `MethodArguments.NO_VALUE` in kotlin-reflect accessor
freya022 Sep 2, 2025
d4194d9
Add specialized instances for direct/default calls of kotlin-reflect
freya022 Sep 2, 2025
76bdbd4
Move tests to root module
freya022 Sep 2, 2025
0a16761
Updated README.md
freya022 Sep 2, 2025
9c051a8
Typo
freya022 Sep 2, 2025
65291da
Reword readme
freya022 Sep 2, 2025
3578ecf
Support value classes
freya022 Sep 2, 2025
90cded3
Update .gitignore
freya022 Sep 2, 2025
431a412
Remove unused CodeBuilder.ifNull
freya022 Sep 2, 2025
880bcc1
Explicitly prevent optional parameters in event listeners
freya022 Sep 2, 2025
15e6f61
Clean up
freya022 Sep 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions BotCommands-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -146,6 +150,12 @@ dokka {
}
}

java {
// ClassFile-based method accessors require Java 24+
// but the class is conditionally loaded
disableAutoTargetJvm()
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,4 +28,4 @@ internal inline fun ExecutableMixin.requireUser(value: Boolean, lazyMessage: ()
}

requireAt(value, function, lazyMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,7 +66,7 @@ internal class MessageCommandInfoImpl internal constructor(
}

val finalParameters = parameters.mapFinalParameters(event, optionValues)
function.callSuspendBy(finalParameters)
methodAccessor.callSuspend(finalParameters)

return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,7 +66,7 @@ internal class UserCommandInfoImpl internal constructor(
}

val finalParameters = parameters.mapFinalParameters(event, optionValues)
function.callSuspendBy(finalParameters)
methodAccessor.callSuspend(finalParameters)

return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 { }
Expand Down Expand Up @@ -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
}
Expand All @@ -90,7 +89,7 @@ internal sealed class SlashCommandInfoImpl(
internal suspend fun <T> ExecutableMixin.getSlashOptions(
event: T,
parameters: List<AggregatedParameterMixin>
): Map<KParameter, Any?>? 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -97,7 +96,7 @@ internal class AutocompleteHandler(
?: return emptyList() //Autocomplete was triggered without all the required parameters being present

val actualChoices: MutableList<Command.Choice> = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class AutocompleteInfoImpl internal constructor(
override val name: String? = builder.name
internal val eventFunction = builder.function.toMemberParamFunction<CommandAutoCompleteInteractionEvent, _>(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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -90,7 +89,7 @@ internal class TextCommandVariationImpl internal constructor(
internal suspend fun execute(event: BaseCommandEvent, optionValues: Map<out OptionImpl, Any?>) {
val finalParameters = parameters.mapFinalParameters(event, optionValues)

function.callSuspendBy(finalParameters)
methodAccessor.callSuspend(finalParameters)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -135,4 +134,4 @@ internal class ComponentHandlerExecutor internal constructor(

return tryInsertNullableOption(value, option, optionMap)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }

Expand Down Expand Up @@ -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
}
Expand All @@ -102,4 +101,4 @@ internal class ComponentTimeoutExecutor internal constructor(

return tryInsertNullableOption(value, option, optionMap)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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<KFunction<*>>.toClassPathFunctions(instance: Any) = map { ClassPathFunction(instance, it) }
Expand All @@ -57,4 +62,4 @@ internal fun ClassPathFunction(instance: Any, function: KFunction<*>): ClassPath
}

internal fun <C : ClassPathFunctionIterable> C.withFilter(filter: FunctionFilter) = this.filter { filter(it.function, false) }
internal fun <C : ClassPathFunctionIterable> C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it.function, true) }
internal fun <C : ClassPathFunctionIterable> C.requiredFilter(filter: FunctionFilter) = this.onEach { filter(it.function, true) }
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,10 +12,19 @@ internal class EventHandlerFunction(
val timeout: Duration?,
private val parametersBlock: () -> Array<Any>
) {
val parameters: Array<Any> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ internal class EventListenerRegistry internal constructor(
private fun Collection<ClassPathFunction>.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<BEventListener>()
Expand Down
Loading