kord icon indicating copy to clipboard operation
kord copied to clipboard

Status on GraalVM native image compatibility

Open DarkAtra opened this issue 1 year ago • 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.

DarkAtra avatar Mar 19 '23 00:03 DarkAtra

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

DRSchlaubi avatar Mar 19 '23 00:03 DRSchlaubi

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

DarkAtra avatar Mar 19 '23 01:03 DarkAtra

// 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

DRSchlaubi avatar Mar 19 '23 13:03 DRSchlaubi

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<Type>(), which should be easy to generate

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

DRSchlaubi avatar Mar 19 '23 14:03 DRSchlaubi

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

DRSchlaubi avatar Mar 19 '23 15:03 DRSchlaubi

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

DarkAtra avatar Mar 19 '23 17:03 DarkAtra

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 [email protected]/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.

DarkAtra avatar Mar 19 '23 18:03 DarkAtra

We will revisit this after #855

DRSchlaubi avatar Mar 10 '24 04:03 DRSchlaubi

Since #928 got merged, the Ktor reflection hints might no longer be needed, as those seem to be CIO specific

DRSchlaubi avatar Mar 21 '24 08:03 DRSchlaubi

Since #928 got merged, the Ktor reflection hints might no longer be needed, as those seem to be CIO specific

Seems like the hint for DefaultPool is still required. The one for InterestSuspensionsMap can be omitted though. E.g.:

reflect-config.json (see https://github.com/DarkAtra/v-rising-discord-bot/commit/fd1d786daf1e2a92dc1781e173b376cd7ac924e7):

[
  {
    "name": "kotlin.internal.jdk8.JDK8PlatformImplementations",
    "allPublicMethods": true,
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true
  },
  {
    "name": "io.ktor.utils.io.pool.DefaultPool",
    "allDeclaredFields": true
  }
]

DarkAtra avatar Mar 21 '24 22:03 DarkAtra

kotlin.internal.jdk8.JDK8PlatformImplementations

should no longer be needed as of 1.9.0

DRSchlaubi avatar Mar 22 '24 07:03 DRSchlaubi

kotlin.internal.jdk8.JDK8PlatformImplementations

should no longer be needed as of 1.9.0

In the project i linked i'm stuck with Kotlin 1.8.x for the time being due to https://github.com/oracle/graal/issues/7089. However, i validated that a different project (which already uses kotlin 1.9.23) runs fine without JDK8PlatformImplementations hints.

DarkAtra avatar Mar 22 '24 08:03 DarkAtra

Small update... It seems like Kord also needs to provide reflection hints for the following two classes when using newer GraalVM versions:

  • Optional.Null.Companion::class
  • Optional.Missing.Companion::class

Additionally, kotlinx.serialization also requires a few reflection hints for (in newer GraalVM versions):

  • JsonArray.Companion::class
  • JsonObject.Companion::class

(see https://github.com/oracle/graal/issues/7089)

DarkAtra avatar Jul 02 '24 07:07 DarkAtra