kordlib/kord

Status on GraalVM native image compatibility

DarkAtra opened this issue · 13 comments

Support GraalVM native images

In addition to supporting Kotlin/JS and Kotlin/Native (See #69) we should provide Support for GraalVM native image.

Tasks

Experimental builds

Currently, we provide an experimental Graal build (See #787), this build is not fully tested and not recommended for production use

You can obtain it using the feature-graal-SNAPSHOT version. See the Project README for more information

Original Issue

I saw that you're working on kotlin native/multiplatform support and was wondering if GraalVM Native Image is also something that you're interested in supporting in the future.

Some context:
I'm currently trying to build a native image for one of my side projects that uses kord and there are a lot of runtime hints required to get everything working. I was hoping that kord could provide such runtime hints so that i don't have to maintain it if something changes under the hood.

Never used Graal before but as the main guy of working on MPP here I am definitely interested in it. However I don't know what the problem here is as I haven't tried it yet.

What happened when you try to build a Graal image?

Another thing is about dependency compatibility. I only know that ktor server supports Graal not sure about the client

Never used Graal before but as the main guy of working on MPP here I am definitely interested in it.

Great to hear that you're interested ^^

What happened when you try to build a Graal image?

Some things wont work out of the box when an application is compiled using GraalVM Native Image. All limitations are described here: https://www.graalvm.org/22.1/reference-manual/native-image/Limitations/

As kord relies on kotlinx-serialization, i currently need to provide reflection hints for all @Serializable annotated classes. One example would be:

reflect-config.json:

[
  {
    "name": "dev.kord.common.entity.ApplicationCommandPermissionType",
    "allDeclaredFields": true,
    "allDeclaredConstructors": true,
    "queryAllDeclaredMethods": true,
    "methods": [
      {
        "name": "getValue",
        "parameterTypes": [ ]
      }
    ]
  },
  {
    "name": "dev.kord.common.entity.ApplicationCommandPermissionType$Companion",
    "methods": [
      {
        "name": "serializer",
        "parameterTypes": [ ]
      }
    ]
  }
]

The above configuration would tell GraalVM that it should expect reflection calls for fields, methods and constructors of ApplicationCommandPermissionType and also for the generated serializer method of its companion object. All of these calls are required during serialization. Without those hints, you'd see NoSuchFieldExceptions (or similar depending on the reflection call).


The same also applies to ktor-client to some extend. However, I'd expect the ktor team to provide compatibility for GraalVM in this case as there are no kord specific classes involved. Only for completeness, the following hints were required in my project:

// required by kotlin coroutines
.registerType(TypeReference.of("kotlin.internal.jdk8.JDK8PlatformImplementations"), MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)
// required by ktor
.registerType(InterestSuspensionsMap::class.java, MemberCategory.DECLARED_FIELDS)
.registerType(DefaultPool::class.java, MemberCategory.DECLARED_FIELDS)

(The above code is using Spring Frameworks RuntimeHintsRegistrar to generate json format that GraalVM expects during compilation.)


Please note, that i'm still very early in the testing phase and that there are a lot of hints still missing (not only for kord but also for other libraries). I'm currently still trying to get the application to boot, which also involves connecting to the discord gateway. That's the main reason why i've started with runtime hints for kord :D


And again, just for completeness, this is the branch i'm currently working on: https://github.com/DarkAtra/v-rising-discord-bot/tree/next

// required by kotlin coroutines
.registerType(TypeReference.of("kotlin.internal.jdk8.JDK8PlatformImplementations"), MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)

This should be fixed in Kotlin 1.9 and is tracked in KT-51579

I've created a test project and I can run using the following config

reflect-config.json
[
  {
    "name": "kotlin.internal.jdk8.JDK8PlatformImplementations",
    "allPublicMethods": true,
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true
  },
  {
    "name": "io.ktor.network.selector.InterestSuspensionsMap",
    "allDeclaredFields": true
  },
  {
    "name": "io.ktor.utils.io.pool.DefaultPool",
    "allDeclaredFields": true
  }
]
Main.kt
package dev.kord.core

import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.gateway.Intent
import dev.kord.gateway.PrivilegedIntent

suspend fun main(args: Array<String>) {
    val kord = Kord(args.firstOrNull() ?: error("token required"))

    kord.on<MessageCreateEvent> {
        if (message.author?.isBot == true) return@on
        if (message.content == "!ping") message.channel.createMessage("pong")
    }

    kord.login {
        presence { playing("!ping to pong") }

        @OptIn(PrivilegedIntent::class)
        intents += Intent.MessageContent
    }
}
build.gradle.kts
plugins {
    `kord-internal-module`
    application
    id("org.graalvm.buildtools.native") version "0.9.20"
}

dependencies {
    implementation(projects.core)
    implementation(libs.slf4j.simple)
}

application {
    mainClass.set("dev.kord.core.MainKt")
}

graalvmNative {
    binaries {
        named("main") {

            javaLauncher.set(javaToolchains.launcherFor {
                languageVersion.set(JavaLanguageVersion.of(19))
                vendor.set(JvmVendorSpec.matching("GraalVM Community"))
            })
        }
    }
}

Could you share an example of what causes issues with serialization?

Also, would you mind joining the Support Discord, so we can discuss this further?

After further investigation, the only method using reflection is serializer(), which should be easy to generate

{
   "name":"package.name.YourClassName",
   "fields":[{"name":"Companion"}]
},
{
   "name":"package.name.YourClassName$Companion",
   "methods":[{"name":"serializer","parameterTypes":[] }]
}

Could you please try the version feature-graal-SNAPSHOT and tell me what issues you encounter?

I've created a test project and I can run using the following config
reflect-config.json
Main.kt
build.gradle.kts

Could you share an example of what causes issues with serialization?

Also, would you mind joining the Support Discord, so we can discuss this further?

After further investigation, the only method using reflection is serializer(), which should be easy to generate

{
   "name":"package.name.YourClassName",
   "fields":[{"name":"Companion"}]
},
{
   "name":"package.name.YourClassName$Companion",
   "methods":[{"name":"serializer","parameterTypes":[] }]
}

joined discord in case you still want to discuss something.

Could you please try the version feature-graal-SNAPSHOT and tell me what issues you encounter?

i'll try the snapshot now

with the snapshot it does no longer require runtime hints for:

ApplicationCommandData::class.java,
AutoModerationRuleData::class.java,
ChannelData::class.java,
EmojiData::class.java,
GuildData::class.java,
MemberData::class.java,
MessageData::class.java,
PresenceData::class.java,
RoleData::class.java,
StickerData::class.java,
ThreadMemberData::class.java,
UserData::class.java,
VoiceStateData::class.java,
WebhookData::class.java

but the following hints are still required (probably because i am registering GlobalApplicationCommands on startup):

{
  "name": "dev.kord.core.cache.data.GuildApplicationCommandPermissionsData"
},
{
  "name": "dev.kord.core.cache.data.StickerPackData"
},

without i'm getting the followng exception:

kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Unresolved class: class dev.kord.core.cache.data.GuildApplicationCommandPermissionsData
        at kotlin.reflect.jvm.internal.KClassImpl.reportUnresolvedClass(KClassImpl.kt:328) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.jvm.internal.KClassImpl.access$reportUnresolvedClass(KClassImpl.kt:44) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.jvm.internal.KClassImpl$Data$descriptor$2.invoke(KClassImpl.kt:56) ~[na:na]
        at kotlin.reflect.jvm.internal.KClassImpl$Data$descriptor$2.invoke(KClassImpl.kt:48) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:93) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.jvm.internal.KClassImpl$Data.getDescriptor(KClassImpl.kt:48) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.jvm.internal.KClassImpl.getDescriptor(KClassImpl.kt:182) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.jvm.internal.KClassImpl.getDescriptor(KClassImpl.kt:44) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.reflect.full.KClassifiers.createType(KClassifiers.kt:47) ~[na:na]
        at kotlin.reflect.jvm.internal.CachesKt$CACHE_FOR_BASE_CLASSIFIERS$1.invoke(caches.kt:37) ~[na:na]
        at kotlin.reflect.jvm.internal.CachesKt$CACHE_FOR_BASE_CLASSIFIERS$1.invoke(caches.kt:36) ~[na:na]
        at kotlin.reflect.jvm.internal.ComputableClassValue.computeValue(CacheByClass.kt:48) ~[na:na]
        at kotlin.reflect.jvm.internal.ComputableClassValue.computeValue(CacheByClass.kt:46) ~[na:na]
        at java.base@17.0.5/java.lang.ClassValue.get(JavaLangSubstitutions.java:590) ~[v-rising-discord-bot:na]
        at kotlin.reflect.jvm.internal.ClassValueCache.get(CacheByClass.kt:61) ~[na:na]
        at kotlin.reflect.jvm.internal.CachesKt.getOrCreateKType(caches.kt:55) ~[na:na]
        at kotlin.reflect.jvm.internal.ReflectionFactoryImpl.typeOf(ReflectionFactoryImpl.java:123) ~[v-rising-discord-bot:1.8.10-release-430(1.8.10)]
        at kotlin.jvm.internal.Reflection.typeOf(Reflection.java:128) ~[na:na]
        at dev.kord.core.cache.data.GuildApplicationCommandPermissionsData.<clinit>(GuildApplicationCommandPermissionsData.kt:35) ~[na:na]
        at dev.kord.core.cache.DataCacheExtensionsKt.registerKordData(DataCacheExtensions.kt:23) ~[na:na]
        at dev.kord.core.builder.kord.KordBuilder.build(KordBuilder.kt:276) ~[na:na]
        at dev.kord.core.builder.kord.KordBuilder$build$1.invokeSuspend(KordBuilder.kt) ~[na:na]

Runtime hints for ktor and kotlin coroutines are also still required but that is totally fine as it's not something i'd expect kord to provide.