Skip to main content

The descriptor

The descriptor is the generated object VersatiaDescriptor, placed in the package of the plugin's main class so that the runtime can load it by name: for com.example.MyPlugin it is com.example.VersatiaDescriptor. It implements VersatiaPluginDescriptor from dev.versatia.engine.descriptor and builds its elements with the Direct and Reflective implementations of Instantiator, Invoker and Setter. Those types are marked @VersatiaInternalApi, so the file opts in with @file:OptIn(VersatiaInternalApi::class) and compiles without warnings in any plugin.

The descriptor is a technical detail of the enhancement mechanism, not a feature. It records facts about the annotated code (classes, supertypes, parameters, the annotations written on them) and never what a feature makes of those facts: there is no "bean name" field, the DI processor derives the name from the recorded @Qualifier attribute.

Shape​

The descriptor is deliberately flat: a format version and a list of EnhancedElements.

Field of EnhancedElementMeaning
processorClassthe processor that interprets it, for example dev.versatia.di.LocalBeanEnhancementProcessor
phasethe Phase at which it runs; see Phases and events
annotationthe enhancement annotation that produced it, with all its arguments
targetwhat was annotated: a ClassTarget, a FunctionTarget, a PropertyTarget or a PluginTarget
originthe class the annotation is physically written on: the target's own class, a base class of the main class for inherited enhancements, or the class carrying a subclass enhancement

No iteration order, grouping or phase structure is baked into the file; the runtime decides how to walk it. Changing how VersatiaCore iterates therefore never requires regenerating plugins.

AnnotationOccurrence, Parameter and TypeReference​

An AnnotationOccurrence is an annotation as written at one place: its class name and its arguments, defaults included. Values are strings, booleans, numbers, chars, enum entry names, class names and lists of those. A Parameter is a constructor or function parameter: its type, the attributes on it, inherited ones included, optional for a constructor parameter with a default value, and suppliedBy, the processor that supplies its argument instead of injection (the @Value processor for a constructor property). A TypeReference is a type as written on a property: a Kotlin qualified name, its type arguments and whether it is nullable, kotlin.collections.List<kotlin.Int>?.

ClassTarget​

A class whose instances the runtime may create.

FieldMeaning
classNameKotlin qualified name (com.example.Outer.Inner)
assignableTypesthe class and every supertype except Any
annotationsthe attributes written on the class
constructorParametersone Parameter per constructor parameter, in order
instantiatorhow to create an instance from resolved arguments

FunctionTarget​

A function the runtime may call.

FieldMeaning
classNameKotlin qualified name of the declaring class
binaryClassNameJVM name of the declaring class (com.example.Outer$Inner)
functionNamethe function
ownera MemberOwner: PLUGIN, OBJECT, INSTANCE or SUBCLASS_INSTANCE, who holds the instance to call it on; INSTANCE means the processor must obtain it (for @AutoInvoke, the bean of that class); SUBCLASS_INSTANCE, for @Value properties only, is the instance a subclass enhancement creates for a class that is no bean, such as a plain Listener
annotationsthe attributes written on the function
parametersone Parameter per parameter
invokerhow to call it with a receiver and resolved arguments
classAnnotationsthe attributes written on the declaring class, for what applies to a whole class such as @ConditionalOnPlugin

PropertyTarget​

A mutable property the runtime may set.

FieldMeaning
classNameKotlin qualified name of the declaring class
binaryClassNameJVM name of the declaring class
propertyNamethe property
ownera MemberOwner, as for a function
typethe property's type as a TypeReference; what a processor reads for it
annotationsthe attributes written on the property
setterhow to assign it on a receiver
classAnnotationsthe attributes written on the declaring class
constructorParameterfor a property declared in the primary constructor of a bean or of a SUBCLASS_INSTANCE class, the index of that parameter: its value is passed to the constructor

@Value produces these.

PluginTarget​

The plugin's main class. It carries className, assignableTypes and annotations like a ClassTarget, but no parameters and no instantiator: Bukkit creates the instance, the runtime only hands it out. One element is produced per class enhancement written on the main class or on one of its supertypes, all listed first; VersatiaPlugin contributes @PluginBean (the main class as a bean), @ExtractResources (the plugin's resources) and two elements of @RunBackgroundTasks (the plugin's coroutine scope, bound at start and stopped before the stop functions). The same annotation, a subclass enhancement, adds a ClassTarget per BackgroundTask class, or the PluginTarget when the main class is one.

Direct access versus reflection​

Encapsulation is preserved: beans, constructors, functions and their parameter types may be private. The generator picks per element:

  • Instantiator.Direct / Invoker.Direct when the class, the member and every parameter type are public or internal. The generated code contains the call itself:

    Instantiator.Direct { args -> ConsoleGreeter(args[0] as Clock) }
    Invoker.Direct { instance, args -> (instance as MyPlugin).start(args[0] as Clock) }
    Setter.Direct { instance, value -> (instance as Shop).tax = value as Percent }

    No reflection, no setAccessible.

  • Instantiator.Reflective / Invoker.Reflective / Setter.Reflective otherwise. They store JVM binary names and at runtime load the class without initialising it (Class.forName(name, false, loader)), find the matching declared constructor, method or property and make it accessible. A property with a private set is reflective too, however visible the property itself is. A Setter.Reflective with a third argument, "value", reads the property and stores into that member of what it holds, which is how a val holding a PreviousValueTracking is filled; the direct form is Setter.Direct { _, value -> Limits.max.value = value as Int }.

  • Instantiator.WithDefaults for a constructor with optional parameters, visible or not. It calls the constructor through Kotlin reflection (callBy), leaving out every argument given as DefaultArgument, so those parameters keep their defaults.

  • A Kotlin object reached through a subclass enhancement is named directly (Instantiator.Direct { _ -> Console }) when visible and read from its INSTANCE field (Instantiator.ObjectInstance) otherwise.

A generated file​

For

class AlphaPlugin : VersatiaPlugin()

interface Greeter

@ExposeSingleton
class Clock

@LocalBean
internal class ConsoleGreeter(val clock: Clock) : Greeter

@LocalBean
private class HiddenAudit(@Qualifier("consoleGreeter") val greeter: Greeter)

the processor emits (shortened):

@file:OptIn(VersatiaInternalApi::class)

object VersatiaDescriptor : VersatiaPluginDescriptor {
override val formatMajor: Int = 1
override val formatMinor: Int = 0

override val elements: List<EnhancedElement>
get() = listOf(
EnhancedElement(
processorClass = "dev.versatia.di.PluginBeanEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.di.PluginBean"),
target = PluginTarget(
className = "com.example.alpha.AlphaPlugin",
assignableTypes = listOf("com.example.alpha.AlphaPlugin", "dev.versatia.VersatiaPlugin", "org.bukkit.plugin.java.JavaPlugin", "org.bukkit.plugin.Plugin", /* ... */),
annotations = emptyList(),
),
),
EnhancedElement(
processorClass = "dev.versatia.resources.ExtractResourcesEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.resources.ExtractResources"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.task.PluginScopeEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.task.RunBackgroundTasks"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.task.PluginScopeEnhancementProcessor",
phase = Phase.PLUGIN_STOP,
annotation = AnnotationOccurrence("dev.versatia.task.RunBackgroundTasks"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.di.ExposeSingletonEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.ExposeSingleton"),
target = ClassTarget(
className = "com.example.alpha.Clock",
assignableTypes = listOf("com.example.alpha.Clock"),
annotations = emptyList(),
constructorParameters = emptyList(),
instantiator = Instantiator.Direct { _ -> Clock() },
),
),
EnhancedElement(
processorClass = "dev.versatia.di.LocalBeanEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.LocalBean"),
target = ClassTarget(
className = "com.example.alpha.ConsoleGreeter",
assignableTypes = listOf("com.example.alpha.ConsoleGreeter", "com.example.alpha.Greeter"),
annotations = emptyList(),
constructorParameters = listOf(Parameter("com.example.alpha.Clock")),
instantiator = Instantiator.Direct { args -> ConsoleGreeter(args[0] as Clock) },
),
),
EnhancedElement(
processorClass = "dev.versatia.di.LocalBeanEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.LocalBean"),
target = ClassTarget(
className = "com.example.alpha.HiddenAudit",
assignableTypes = listOf("com.example.alpha.HiddenAudit"),
annotations = emptyList(),
constructorParameters = listOf(Parameter("com.example.alpha.Greeter", listOf(AnnotationOccurrence("dev.versatia.di.Qualifier", mapOf("value" to "consoleGreeter"))))),
instantiator = Instantiator.Reflective("com.example.alpha.HiddenAudit", listOf("com.example.alpha.Greeter")),
),
),
)
}

Format version​

The format has its own major.minor version, VersatiaPluginDescriptor.FORMAT_VERSION, currently 1.0. It is independent of the library version. A runtime accepts exactly the version it was built with and rejects any other with a message naming the plugin, both versions and the fix; see The runtime.

The generator writes the two numbers as literal Int properties (formatMajor, formatMinor), so a runtime can read them from any descriptor, even one whose other members it does not understand, before deciding what to do with it. For the same reason elements is a getter rather than an initialised field: loading the descriptor class resolves nothing the elements refer to, so a descriptor of another format still yields a clear version message instead of a NoClassDefFoundError.