├── .fleet ├── settings.json └── run.json ├── tests ├── .idea │ ├── .name │ ├── .gitignore │ ├── compiler.xml │ ├── kotlinc.xml │ ├── misc.xml │ ├── vcs.xml │ ├── artifacts │ │ ├── knee_runtime_frontend_0_2_0_SNAPSHOT.xml │ │ ├── knee_runtime_frontend_0_3_0_SNAPSHOT.xml │ │ ├── knee_annotations_frontend_0_2_0_SNAPSHOT.xml │ │ └── knee_annotations_frontend_0_3_0_SNAPSHOT.xml │ ├── runConfigurations │ │ ├── link_misc.xml │ │ ├── link_imports.xml │ │ ├── link_primitives.xml │ │ ├── test_misc.xml │ │ ├── test_classes.xml │ │ ├── test_imports.xml │ │ ├── test_coroutines.xml │ │ ├── test_primitives.xml │ │ └── test_interfaces.xml │ ├── deploymentTargetDropDown.xml │ └── gradle.xml ├── test-misc │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── androidInstrumentedTest │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── deepmedia │ │ │ │ └── tools │ │ │ │ └── knee │ │ │ │ └── tests │ │ │ │ ├── BufferTests.kt │ │ │ │ ├── DefaultValuesTests.kt │ │ │ │ └── EnumTests.kt │ │ └── backendMain │ │ │ └── kotlin │ │ │ ├── Init.kt │ │ │ ├── BufferDefinintions.kt │ │ │ ├── DefaultValuesDefinitions.kt │ │ │ └── EnumDefinintions.kt │ └── build.gradle.kts ├── test-classes │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── backendMain │ │ │ └── kotlin │ │ │ │ ├── Init.kt │ │ │ │ ├── ObjectDefinitions.kt │ │ │ │ └── ClassDefinintions.kt │ │ └── androidInstrumentedTest │ │ │ └── kotlin │ │ │ └── io │ │ │ └── deepmedia │ │ │ └── tools │ │ │ └── knee │ │ │ └── tests │ │ │ ├── ObjectTests.kt │ │ │ ├── InnerClassTests.kt │ │ │ └── ClassTests.kt │ └── build.gradle.kts ├── test-imports │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── backendMain │ │ │ └── kotlin │ │ │ │ ├── RangeDefinitions.kt │ │ │ │ ├── Init.kt │ │ │ │ ├── MiscDefinitions.kt │ │ │ │ ├── FlowDefinitions.kt │ │ │ │ └── LambdaDefinitions.kt │ │ └── androidInstrumentedTest │ │ │ └── kotlin │ │ │ └── io │ │ │ └── deepmedia │ │ │ └── tools │ │ │ └── knee │ │ │ └── tests │ │ │ └── ImportMiscTests.kt │ └── build.gradle.kts ├── test-coroutines │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── backendMain │ │ │ └── kotlin │ │ │ │ ├── Init.kt │ │ │ │ ├── SuspendDefinitions.kt │ │ │ │ └── ReverseSuspendDefinitions.kt │ │ └── androidInstrumentedTest │ │ │ └── kotlin │ │ │ └── io │ │ │ └── deepmedia │ │ │ └── tools │ │ │ └── knee │ │ │ └── tests │ │ │ └── SuspendTests.kt │ └── build.gradle.kts ├── test-interfaces │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── backendMain │ │ │ └── kotlin │ │ │ │ └── Init.kt │ │ └── androidInstrumentedTest │ │ │ └── kotlin │ │ │ └── io │ │ │ └── deepmedia │ │ │ └── tools │ │ │ └── knee │ │ │ └── tests │ │ │ └── InnerInterfaceTests.kt │ └── build.gradle.kts ├── test-primitives │ ├── .gitignore │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ ├── backendMain │ │ │ └── kotlin │ │ │ │ └── Init.kt │ │ └── androidInstrumentedTest │ │ │ └── kotlin │ │ │ └── io │ │ │ └── deepmedia │ │ │ └── tools │ │ │ └── knee │ │ │ └── tests │ │ │ ├── UnsignedPrimitiveTests.kt │ │ │ ├── PrimitiveTests.kt │ │ │ └── NullablePrimitiveTests.kt │ └── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties └── settings.gradle.kts ├── knee-runtime ├── .gitignore └── src │ ├── prebuiltHeadersMain │ ├── interop │ │ └── jni_prebuilt.def │ └── kotlin │ │ └── JniDefinitions.prebuilt.kt │ ├── frontendMain │ └── kotlin │ │ ├── buffer │ │ └── BufferTypeAliases.kt │ │ ├── compiler │ │ ├── Buffers.jvm.kt │ │ ├── Exceptions.jvm.kt │ │ └── Instances.jvm.kt │ │ └── module │ │ └── KneeModule.jvm.kt │ ├── backendMain │ └── kotlin │ │ ├── types │ │ ├── Booleans.kt │ │ ├── Enums.kt │ │ ├── Classes.kt │ │ ├── Strings.kt │ │ └── Interfaces.kt │ │ ├── JniDefinitions.kt │ │ ├── JvmHelpers.kt │ │ ├── compiler │ │ ├── Buffers.kn.kt │ │ ├── BoxMethods.kt │ │ └── Instances.kn.kt │ │ ├── buffer │ │ ├── IntBuffer.kt │ │ ├── LongBuffer.kt │ │ ├── FloatBuffer.kt │ │ └── DoubleBuffer.kt │ │ ├── Init.kt │ │ └── JvmApi.kt │ └── androidNativeMain │ └── kotlin │ └── JniDefinitions.android.kt ├── experiments ├── .idea │ ├── .name │ ├── .gitignore │ ├── compiler.xml │ ├── kotlinc.xml │ ├── vcs.xml │ ├── misc.xml │ ├── artifacts │ │ ├── knee_runtime_frontend_0_2_0_SNAPSHOT.xml │ │ └── knee_annotations_frontend_0_2_0_SNAPSHOT.xml │ ├── runConfigurations │ │ ├── compose_notes_c.xml │ │ ├── compose_notes_l.xml │ │ ├── expect_actual_c.xml │ │ └── expect_actual_l.xml │ ├── gradle.xml │ └── inspectionProfiles │ │ └── Project_Default.xml ├── compose-notes │ ├── .gitignore │ └── src │ │ ├── backendMain │ │ └── kotlin │ │ │ ├── Init.kt │ │ │ └── NoteManager.kt │ │ └── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── io │ │ └── deepmedia │ │ └── tools │ │ └── knee │ │ └── sample │ │ └── RootScreen.kt ├── expect-actual │ ├── .gitignore │ └── src │ │ ├── androidNativeX64Main │ │ └── kotlin │ │ │ └── Actual.kt │ │ ├── androidNativeX86Main │ │ └── kotlin │ │ │ └── Actual.kt │ │ ├── androidNativeArm64Main │ │ └── kotlin │ │ │ └── Actual.kt │ │ ├── backendMain │ │ └── kotlin │ │ │ ├── Init.kt │ │ │ └── Expect.kt │ │ ├── androidNativeArm32Main │ │ └── kotlin │ │ │ └── Actual.kt │ │ └── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── io │ │ └── deepmedia │ │ └── tools │ │ └── knee │ │ └── sample │ │ └── RootScreen.kt ├── multimodule-consumer │ ├── .gitignore │ └── src │ │ ├── androidMain │ │ └── AndroidManifest.xml │ │ └── backendMain │ │ └── kotlin │ │ └── Init.kt ├── multimodule-producer │ ├── .gitignore │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── ProducerFrontend.kt │ │ └── backendMain │ │ └── kotlin │ │ └── Init.kt ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle.kts ├── .gitignore ├── gradle.properties └── settings.gradle.kts ├── knee-annotations ├── .gitignore ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── Knee.common.kt │ └── backendMain │ │ └── kotlin │ │ └── Knee.kt └── build.gradle.kts ├── knee-compiler-plugin ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ ├── org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor │ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar │ │ └── kotlin │ │ ├── instances │ │ └── InterfaceNames.kt │ │ ├── symbols │ │ ├── PackageNames.kt │ │ └── KneeSymbols.kt │ │ ├── features │ │ ├── KneeModule.kt │ │ ├── KneeUpwardProperty.kt │ │ ├── KneeEnum.kt │ │ ├── KneeFeature.kt │ │ ├── KneeDownwardProperty.kt │ │ ├── KneeUpwardFunction.kt │ │ ├── KneeInitializer.kt │ │ ├── KneeObject.kt │ │ └── KneeImport.kt │ │ ├── export │ │ ├── v2 │ │ │ └── ExportedTypeInfo.kt │ │ └── v1 │ │ │ └── ExportFlags.kt │ │ ├── services │ │ └── KneeComponentRegistrar.kt │ │ ├── context │ │ └── KneeContext.kt │ │ ├── codec │ │ ├── IdentityCodec.kt │ │ ├── StringCodecs.kt │ │ └── PrimitiveCodecs.kt │ │ ├── import │ │ └── ImportInfo.kt │ │ ├── jni │ │ └── JniSignature.kt │ │ ├── UpwardProperties.kt │ │ └── serialization │ │ ├── Classes.kt │ │ └── Types.kt └── build.gradle.kts ├── knee-gradle-plugin ├── .gitignore └── src │ └── main │ └── kotlin │ ├── tasks │ └── UnpackageCodegenSources.kt │ ├── utils │ └── AndroidUtils.kt │ └── KneeExtension.kt ├── .idea ├── .gitignore ├── compiler.xml ├── kotlinc.xml ├── vcs.xml ├── misc.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── runConfigurations │ ├── runtime_deployLocal.xml │ ├── deployLocal.xml │ ├── gradle_plugin_deployLocal.xml │ ├── compiler_plugin_deployLocal.xml │ ├── deployGithub.xml │ ├── deploySonatype.xml │ └── annotations_deployLocal.xml └── jarRepositories.xml ├── assets └── logo_256.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitmodules ├── .gitignore ├── gradle.properties ├── settings.gradle.kts ├── .github └── workflows │ ├── snapshot.yml │ ├── deploy.yml │ └── build.yml ├── README.md └── docs ├── install.mdx ├── features ├── callables.mdx ├── enums.mdx ├── objects.mdx └── suspend-functions.mdx ├── index.mdx └── concepts.mdx /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.idea/.name: -------------------------------------------------------------------------------- 1 | KneeTests -------------------------------------------------------------------------------- /knee-runtime/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-misc/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /experiments/.idea/.name: -------------------------------------------------------------------------------- 1 | KneeSamples -------------------------------------------------------------------------------- /knee-annotations/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /knee-compiler-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /knee-gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-classes/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-imports/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /experiments/compose-notes/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /experiments/expect-actual/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-interfaces/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /tests/test-primitives/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /experiments/multimodule-consumer/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /experiments/multimodule-producer/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ -------------------------------------------------------------------------------- /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | 4 | 5 | ] 6 | } -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /tests/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /assets/logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmedia/Knee/HEAD/assets/logo_256.png -------------------------------------------------------------------------------- /experiments/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /knee-annotations/src/commonMain/kotlin/Knee.common.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.annotations 2 | 3 | -------------------------------------------------------------------------------- /knee-runtime/src/prebuiltHeadersMain/interop/jni_prebuilt.def: -------------------------------------------------------------------------------- 1 | headers = jni.h 2 | headerFilter = jni.h jni_md.h -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmedia/Knee/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tests/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmedia/Knee/HEAD/tests/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /experiments/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmedia/Knee/HEAD/experiments/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependencies/jdk17"] 2 | path = dependencies/jdk17 3 | url = https://android.googlesource.com/platform/prebuilts/jdk/jdk17 4 | -------------------------------------------------------------------------------- /experiments/multimodule-producer/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /experiments/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") apply false 3 | kotlin("jvm") apply false 4 | id("com.android.application") apply false 5 | } -------------------------------------------------------------------------------- /tests/test-misc/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test-classes/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test-coroutines/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test-imports/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test-interfaces/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test-primitives/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor: -------------------------------------------------------------------------------- 1 | io.deepmedia.tools.knee.plugin.compiler.services.KneeCommandLineProcessor 2 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar: -------------------------------------------------------------------------------- 1 | io.deepmedia.tools.knee.plugin.compiler.services.KneeComponentRegistrar 2 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /experiments/multimodule-producer/src/androidMain/kotlin/ProducerFrontend.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample.mm.producer 2 | 3 | enum class ProducerFrontendEnum { 4 | FrontendFoo, FrontendBar 5 | } -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /tests/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /experiments/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /experiments/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /experiments/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /tests/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /experiments/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /experiments/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /experiments/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /tests/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /experiments/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.stability.nowarn=true 2 | android.useAndroidX=true 3 | org.gradle.caching=true 4 | kotlin.incremental.useClasspathSnapshot=true 5 | kotlin.mpp.import.enableKgpDependencyResolution=true 6 | org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m -XX:MaxMetaspaceSize=512m -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/artifacts 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | .kotlin 17 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /knee-runtime/src/frontendMain/kotlin/buffer/BufferTypeAliases.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.buffer 2 | 3 | typealias ByteBuffer = java.nio.ByteBuffer 4 | typealias DoubleBuffer = java.nio.DoubleBuffer 5 | typealias FloatBuffer = java.nio.FloatBuffer 6 | typealias IntBuffer = java.nio.IntBuffer 7 | typealias LongBuffer = java.nio.LongBuffer -------------------------------------------------------------------------------- /experiments/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-runtime/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /experiments/compose-notes/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @OptIn(ExperimentalStdlibApi::class) 6 | @CName(externName = "JNI_OnLoad") 7 | @KneeInit 8 | fun initKnee() { 9 | require(isExperimentalMM()) { 10 | "Not experimental MM" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /experiments/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-annotations/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/BufferTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | 5 | class BufferTests { 6 | 7 | companion object { 8 | init { 9 | System.loadLibrary("test_misc") 10 | } 11 | } 12 | 13 | @Test 14 | fun testBuffers() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/types/Booleans.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.types 2 | 3 | import platform.android.* 4 | 5 | @PublishedApi 6 | internal fun decodeBoolean(data: jboolean): Boolean = data == JNI_TRUE.toUByte() 7 | 8 | @PublishedApi 9 | internal fun encodeBoolean(data: Boolean): jboolean { 10 | return if (data) JNI_TRUE.toUByte() else JNI_FALSE.toUByte() 11 | } 12 | -------------------------------------------------------------------------------- /knee-runtime/src/androidNativeMain/kotlin/JniDefinitions.android.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime 2 | 3 | // actual val JNI_OK: Int get() = platform.android.JNI_OK 4 | // actual val JNI_VERSION_1_6: Int get() = platform.android.JNI_VERSION_1_6 5 | // actual typealias JNIInvokeInterface = platform.android.JNIInvokeInterface 6 | // actual typealias JNINativeInterface = platform.android.JNINativeInterface 7 | 8 | -------------------------------------------------------------------------------- /tests/test-imports/src/backendMain/kotlin/RangeDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.flow.* 6 | import kotlinx.coroutines.* 7 | 8 | @KneeInterface typealias ClosedFloatRange = ClosedRange 9 | 10 | @Knee var currentFloatRange: ClosedRange = 0F .. 10F 11 | 12 | -------------------------------------------------------------------------------- /tests/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-runtime/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-runtime/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /knee-runtime/src/frontendMain/kotlin/compiler/Buffers.jvm.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("BuffersKt") 2 | package io.deepmedia.tools.knee.runtime.compiler 3 | 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | 7 | @Suppress("unused") 8 | internal object KneeBuffers { 9 | @JvmStatic 10 | private fun setNativeOrder(buffer: ByteBuffer) { 11 | buffer.order(ByteOrder.nativeOrder()) 12 | } 13 | } 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-annotations/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/../knee-annotations/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.caching.debug=false 3 | org.gradle.parallel=true 4 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 5 | 6 | kotlin.mpp.stability.nowarn=true 7 | kotlin.incremental.useClasspathSnapshot=true 8 | kotlin.mpp.enableCInteropCommonization=true 9 | kotlin.mpp.import.enableKgpDependencyResolution=true 10 | 11 | android.useAndroidX=true 12 | 13 | knee.version=1.2.0 14 | knee.group=io.deepmedia.tools.knee 15 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/types/Enums.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.runtime.types 4 | 5 | import kotlin.enums.enumEntries 6 | 7 | @OptIn(ExperimentalStdlibApi::class) 8 | @PublishedApi 9 | internal inline fun > decodeEnum(index: Int): T { 10 | return enumEntries()[index] 11 | } 12 | 13 | @PublishedApi 14 | internal fun > encodeEnum(instance: T): Int { 15 | return instance.ordinal 16 | } 17 | -------------------------------------------------------------------------------- /tests/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.caching.debug=false 3 | org.gradle.parallel=true 4 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 5 | 6 | kotlin.mpp.stability.nowarn=true 7 | kotlin.incremental.useClasspathSnapshot=true 8 | kotlin.mpp.enableCInteropCommonization=true 9 | kotlin.mpp.import.enableKgpDependencyResolution=true 10 | 11 | android.useAndroidX=true 12 | 13 | io.deepmedia.knee.verboseLogs=true 14 | io.deepmedia.knee.verboseSources=true 15 | -------------------------------------------------------------------------------- /knee-runtime/src/prebuiltHeadersMain/kotlin/JniDefinitions.prebuilt.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime 2 | 3 | // actual val JNI_OK: Int get() = io.deepmedia.tools.knee.runtime.internal.JNI_OK 4 | // actual val JNI_VERSION_1_6: Int get() = io.deepmedia.tools.knee.runtime.internal.JNI_VERSION_1_6 5 | // actual typealias JNIInvokeInterface = io.deepmedia.tools.knee.runtime.internal.JNIInvokeInterface_ 6 | // actual typealias JNINativeInterface = io.deepmedia.tools.knee.runtime.internal.JNINativeInterface_ 7 | -------------------------------------------------------------------------------- /tests/test-misc/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 4 | import io.deepmedia.tools.knee.runtime.useEnv 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlin.experimental.ExperimentalNativeApi 7 | 8 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 9 | @CName(externName = "JNI_OnLoad") 10 | fun onLoad(vm: JavaVirtualMachine): Int { 11 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 12 | return 0x00010006 13 | } 14 | -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidNativeX64Main/kotlin/Actual.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @Knee 6 | actual fun targetName() = "androidNativeX64" 7 | 8 | actual typealias NativePlatformInfoA = X64PlatformInfo 9 | 10 | class X64PlatformInfo @Knee constructor() : PlatformInfo() { 11 | override val targetName: String = "androidNativeX64" 12 | } 13 | 14 | actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { 15 | override val targetName: String = "androidNativeX64" 16 | } -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidNativeX86Main/kotlin/Actual.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @Knee 6 | actual fun targetName() = "androidNativeX86" 7 | 8 | actual typealias NativePlatformInfoA = X86PlatformInfo 9 | 10 | class X86PlatformInfo @Knee constructor() : PlatformInfo() { 11 | override val targetName: String = "androidNativeX86" 12 | } 13 | 14 | actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { 15 | override val targetName: String = "androidNativeX86" 16 | } -------------------------------------------------------------------------------- /tests/test-imports/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 4 | import io.deepmedia.tools.knee.runtime.useEnv 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlin.experimental.ExperimentalNativeApi 7 | 8 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 9 | @CName(externName = "JNI_OnLoad") 10 | fun onLoad(vm: JavaVirtualMachine): Int { 11 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 12 | return 0x00010006 13 | } 14 | -------------------------------------------------------------------------------- /tests/test-coroutines/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 4 | import io.deepmedia.tools.knee.runtime.useEnv 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlin.experimental.ExperimentalNativeApi 7 | 8 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 9 | @CName(externName = "JNI_OnLoad") 10 | fun onLoad(vm: JavaVirtualMachine): Int { 11 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 12 | return 0x00010006 13 | } 14 | -------------------------------------------------------------------------------- /tests/test-interfaces/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 4 | import io.deepmedia.tools.knee.runtime.useEnv 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlin.experimental.ExperimentalNativeApi 7 | 8 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 9 | @CName(externName = "JNI_OnLoad") 10 | fun onLoad(vm: JavaVirtualMachine): Int { 11 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 12 | return 0x00010006 13 | } 14 | -------------------------------------------------------------------------------- /tests/test-primitives/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 4 | import io.deepmedia.tools.knee.runtime.useEnv 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlin.experimental.ExperimentalNativeApi 7 | 8 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 9 | @CName(externName = "JNI_OnLoad") 10 | fun onLoad(vm: JavaVirtualMachine): Int { 11 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 12 | return 0x00010006 13 | } 14 | 15 | -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidNativeArm64Main/kotlin/Actual.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @Knee 6 | actual fun targetName() = "androidNativeArm64" 7 | 8 | actual typealias NativePlatformInfoA = Arm64PlatformInfo 9 | 10 | class Arm64PlatformInfo @Knee constructor() : PlatformInfo() { 11 | override val targetName: String = "androidNativeArm64" 12 | } 13 | 14 | actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { 15 | override val targetName: String = "androidNativeArm64" 16 | } -------------------------------------------------------------------------------- /tests/test-misc/src/backendMain/kotlin/BufferDefinintions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import io.deepmedia.tools.knee.runtime.buffer.* 6 | 7 | 8 | /* @Knee 9 | fun simpleBuffer(buffer: ByteBuffer): Unit { 10 | } */ 11 | 12 | @Knee 13 | fun simpleBufferCallback(callback: () -> ByteBuffer): Unit { 14 | } 15 | 16 | /* @Knee 17 | suspend fun suspendingBufferCallback(callback: () -> ByteBuffer): Unit { 18 | } 19 | */ 20 | 21 | @KneeInterface typealias BufferCallback = () -> ByteBuffer -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/types/Classes.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.runtime.types 4 | 5 | import kotlinx.cinterop.* 6 | 7 | @PublishedApi 8 | internal fun encodeClass(instance: Any): Long { 9 | return StableRef.create(instance).asCPointer().toLong() 10 | } 11 | 12 | @PublishedApi 13 | internal inline fun decodeClass(instance: Long): T { 14 | val pointer = checkNotNull(instance.toCPointer()) { 15 | "Class reference $instance is invalid!" 16 | } 17 | return pointer.asStableRef().get() 18 | } 19 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | google() 6 | mavenLocal() 7 | } 8 | } 9 | 10 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 11 | 12 | dependencyResolutionManagement { 13 | repositories { 14 | mavenCentral() 15 | mavenLocal() 16 | google() 17 | } 18 | } 19 | 20 | includeBuild("..") 21 | 22 | include("test-coroutines") 23 | include("test-interfaces") 24 | include("test-primitives") 25 | include("test-imports") 26 | include("test-classes") 27 | include("test-misc") 28 | 29 | rootProject.name = "KneeTests" -------------------------------------------------------------------------------- /experiments/multimodule-producer/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample.mm.producer 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | 6 | @KneeInit 7 | fun initProducerKnee(env: JniEnvironment) { 8 | 9 | } 10 | 11 | @KneeEnum(exported = true) 12 | enum class ProducerEnum { 13 | Foo, Bar 14 | } 15 | 16 | @KneeClass(exported = true) 17 | class ProducerClass { 18 | fun asd() = 2 19 | } 20 | 21 | @KneeInterface(exported = true) 22 | interface ProducerInterface { 23 | val foo: Int 24 | } 25 | 26 | @Knee 27 | fun getProducerEnum(): ProducerEnum { 28 | return ProducerEnum.Foo 29 | } -------------------------------------------------------------------------------- /experiments/expect-actual/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalForeignApi::class) 2 | 3 | package io.deepmedia.tools.knee.sample 4 | 5 | import io.deepmedia.tools.knee.annotations.* 6 | import io.deepmedia.tools.knee.runtime.currentJavaVirtualMachine 7 | import kotlinx.cinterop.ExperimentalForeignApi 8 | import platform.android.ANDROID_LOG_WARN 9 | import platform.android.__android_log_print 10 | 11 | @OptIn(ExperimentalStdlibApi::class) 12 | @CName(externName = "JNI_OnLoad") 13 | @KneeInit 14 | fun initKnee() { 15 | __android_log_print(ANDROID_LOG_WARN.toInt(), "Sample", "Hello") 16 | __android_log_print(ANDROID_LOG_WARN.toInt(), "Sample", "Hello $currentJavaVirtualMachine") 17 | } 18 | -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidNativeArm32Main/kotlin/Actual.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @Knee 6 | actual fun targetName() = "androidNativeArm32" 7 | 8 | actual typealias NativePlatformInfoA = Arm32PlatformInfo 9 | 10 | class Arm32PlatformInfo @Knee constructor() : PlatformInfo() { 11 | override val targetName: String = "androidNativeArm32" 12 | } 13 | 14 | actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { 15 | override val targetName: String = "androidNativeArm32" 16 | } 17 | 18 | @KneeClass 19 | actual class FunctionWithDefaultParameter { 20 | @Knee 21 | actual fun doSomethingWithDefaultParameter(parameter: Int) { } 22 | } -------------------------------------------------------------------------------- /tests/test-classes/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.JavaVirtualMachine 5 | import io.deepmedia.tools.knee.runtime.attachCurrentThread 6 | import io.deepmedia.tools.knee.runtime.module.KneeModule 7 | import io.deepmedia.tools.knee.runtime.useEnv 8 | import kotlinx.cinterop.ExperimentalForeignApi 9 | import kotlin.experimental.ExperimentalNativeApi 10 | 11 | 12 | @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) 13 | @CName(externName = "JNI_OnLoad") 14 | fun onLoad(vm: JavaVirtualMachine): Int { 15 | vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } 16 | return 0x00010006 17 | } 18 | -------------------------------------------------------------------------------- /experiments/compose-notes/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /knee-runtime/src/frontendMain/kotlin/compiler/Exceptions.jvm.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("ExceptionsKt") 2 | package io.deepmedia.tools.knee.runtime.compiler 3 | 4 | import java.nio.ByteBuffer 5 | 6 | @Suppress("unused") 7 | public class KneeKnExceptionToken(val reference: Long) : RuntimeException() { 8 | protected fun finalize() { 9 | clear(reference) 10 | } 11 | private external fun clear(reference: Long) 12 | 13 | private companion object { 14 | @JvmStatic 15 | @Suppress("unused") 16 | private fun get(throwable: Throwable): Long { 17 | val cause = throwable.cause as? KneeKnExceptionToken ?: return 0 18 | return cause.reference 19 | } 20 | } 21 | } 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/test-imports/src/backendMain/kotlin/MiscDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.flow.* 6 | import kotlinx.coroutines.* 7 | 8 | @KneeEnum typealias DeprecationLevel = kotlin.DeprecationLevel 9 | 10 | @Knee var currentDeprecationLevel: DeprecationLevel? = null 11 | 12 | @Knee 13 | fun getStrongerDeprecationLevel(level: DeprecationLevel): DeprecationLevel { 14 | return when (level) { 15 | DeprecationLevel.WARNING -> DeprecationLevel.ERROR 16 | DeprecationLevel.ERROR -> DeprecationLevel.HIDDEN 17 | DeprecationLevel.HIDDEN -> error("Nothing stronger than DeprecationLevel.HIDDEN") 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/test-classes/src/backendMain/kotlin/ObjectDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlin.random.Random 6 | import kotlin.random.nextUInt 7 | 8 | @KneeObject 9 | object TopLevelObject { 10 | @Knee var value: Int = 0 11 | @Knee fun reset() { value = 0 } 12 | @Knee fun increment() { value += 1 } 13 | @Knee fun decrement() { value -= 1 } 14 | @Knee override fun toString(): String = "TopLevelObject($value)" 15 | } 16 | 17 | class ObjectParent { 18 | @KneeObject object InnerObject { 19 | @Knee var value: Int = 0 20 | } 21 | 22 | @KneeObject companion object { 23 | @Knee var value: Int = 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/UnsignedPrimitiveTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | import java.lang.System.identityHashCode 5 | import kotlin.math.abs 6 | 7 | class UnsignedPrimitiveTests { 8 | 9 | companion object { 10 | init { 11 | System.loadLibrary("test_primitives") 12 | } 13 | } 14 | 15 | @Test 16 | fun testUInts() { 17 | check(10u == sumUInts(3u, 7u)) 18 | } 19 | 20 | @Test 21 | fun testULongs() { 22 | check(10000000000UL == sumULongs(3000000000UL, 7000000000UL)) 23 | } 24 | 25 | @Test 26 | fun testUBytes() { 27 | check(10.toUByte() == sumUBytes(3u, 7u)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /experiments/multimodule-consumer/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/JniDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime 2 | 3 | import kotlinx.cinterop.COpaquePointer 4 | import kotlinx.cinterop.CPointed 5 | import kotlinx.cinterop.CStructVar 6 | 7 | // typealias jobject = COpaquePointer 8 | // typealias jclass = jobject 9 | // typealias jstring = jobject 10 | // expect val JNI_OK: Int 11 | // expect val JNI_VERSION_1_6: Int 12 | // expect class JNIInvokeInterface : CStructVar 13 | // expect class JNINativeInterface : CStructVar 14 | // typealias JavaVMVar = kotlinx.cinterop.CPointerVarOf 15 | // typealias JavaVM = kotlinx.cinterop.CPointer 16 | // typealias JNIEnvVar = kotlinx.cinterop.CPointerVarOf 17 | // typealias JNIEnv = kotlinx.cinterop.CPointer 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /experiments/expect-actual/src/backendMain/kotlin/Expect.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | 7 | expect fun targetName(): String 8 | 9 | @OptIn(ExperimentalForeignApi::class) 10 | @Knee 11 | fun jvmToString(): String = currentJavaVirtualMachine.toString() 12 | 13 | @KneeClass(name = "PlatformInfoA") 14 | expect class NativePlatformInfoA : PlatformInfo 15 | 16 | @KneeClass(name = "PlatformInfoB") 17 | expect class NativePlatformInfoB : PlatformInfo 18 | 19 | abstract class PlatformInfo { 20 | @Knee abstract val targetName: String 21 | } 22 | 23 | expect class FunctionWithDefaultParameter { 24 | fun doSomethingWithDefaultParameter(parameter: Int = 0) 25 | } -------------------------------------------------------------------------------- /experiments/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | google() 6 | mavenLocal() 7 | } 8 | plugins { 9 | kotlin("multiplatform") version "1.9.23" apply false 10 | kotlin("jvm") version "1.9.23" apply false 11 | id("com.android.application") version "8.1.1" apply false 12 | } 13 | } 14 | 15 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 16 | 17 | dependencyResolutionManagement { 18 | repositories { 19 | mavenCentral() 20 | mavenLocal() 21 | google() 22 | } 23 | } 24 | 25 | includeBuild("..") 26 | 27 | include("compose-notes") 28 | include("expect-actual") 29 | include("multimodule-producer") 30 | include("multimodule-consumer") 31 | 32 | rootProject.name = "KneeSamples" -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/types/Strings.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.types 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.* 6 | 7 | @PublishedApi 8 | internal fun decodeString(env: JniEnvironment, data: jstring): String { 9 | // The UTF8 version is null terminated so we can pass it to KN without reading length 10 | // This is not the most efficient though 11 | // https://developer.android.com/training/articles/perf-jni#utf-8-and-utf-16-strings 12 | val chars = env.getStringUTFChars(data) 13 | val str = chars.toKStringFromUtf8() 14 | env.releaseStringUTFChars(data, chars) 15 | return str 16 | } 17 | 18 | @PublishedApi 19 | internal fun encodeString(env: JniEnvironment, data: String): jstring { 20 | return env.newStringUTF(data) 21 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | google() 8 | mavenLocal() 9 | } 10 | 11 | plugins { 12 | kotlin("multiplatform") version "2.0.20" apply false 13 | kotlin("plugin.serialization") version "2.0.20" apply false 14 | kotlin("jvm") version "2.0.20" apply false 15 | id("io.deepmedia.tools.deployer") version "0.14.0" apply false 16 | } 17 | } 18 | 19 | dependencyResolutionManagement { 20 | repositories { 21 | mavenCentral() 22 | mavenLocal() 23 | google() 24 | } 25 | } 26 | 27 | include(":knee-annotations") 28 | include(":knee-runtime") 29 | include(":knee-compiler-plugin") 30 | include(":knee-gradle-plugin") 31 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 2 | name: Snapshot 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | SNAPSHOT: 9 | name: Publish Snapshot 10 | runs-on: ubuntu-latest 11 | env: 12 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 13 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 14 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 15 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | java-version: 17 21 | distribution: temurin 22 | cache: gradle 23 | - name: Publish Nexus Snapshot 24 | run: ./gradlew deployNexusSnapshot --stacktrace -------------------------------------------------------------------------------- /knee-runtime/src/frontendMain/kotlin/module/KneeModule.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.module 2 | 3 | 4 | @Suppress("unused") 5 | abstract class KneeModule { 6 | 7 | protected abstract val exportAdapters: Map> 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | fun getExportAdapter(typeId: Int): Adapter { 11 | val adapter = checkNotNull(exportAdapters[typeId]) { "No adapter for type: $typeId" } 12 | return adapter as Adapter 13 | } 14 | 15 | class Adapter( 16 | private val encoder: (decoded: Decoded) -> Encoded, 17 | private val decoder: (encoded: Encoded) -> Decoded 18 | ) { 19 | fun encode(decoded: Decoded): Encoded = encoder(decoded) 20 | fun decode(encoded: Encoded): Decoded = decoder(encoded) 21 | } 22 | } -------------------------------------------------------------------------------- /knee-annotations/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 2 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | `maven-publish` 7 | id("io.deepmedia.tools.deployer") 8 | } 9 | 10 | kotlin { 11 | applyDefaultHierarchyTemplate { 12 | common { 13 | group("backend") { 14 | withNative() 15 | } 16 | } 17 | } 18 | 19 | // native targets 20 | androidNativeArm32() 21 | androidNativeArm64() 22 | androidNativeX64() 23 | androidNativeX86() 24 | // linuxX64() 25 | // mingwX64() 26 | // macosArm64() 27 | // macosX64() 28 | 29 | // for other consumers 30 | jvmToolchain(11) 31 | jvm(name = "frontend") 32 | } 33 | 34 | deployer { 35 | content.kotlinComponents { 36 | emptyDocs() 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/JvmHelpers.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.runtime 4 | 5 | 6 | val currentJavaVirtualMachine: JavaVirtualMachine 7 | get() = checkNotNull(initializationData?.jvm) { "JVM is null. Did you forget to call initKnee?" } 8 | 9 | 10 | inline fun JavaVirtualMachine.useEnv(block: (JniEnvironment) -> T): T { 11 | var env = env 12 | if (env != null) { 13 | return block(env) 14 | } 15 | env = attachCurrentThread() 16 | return try { 17 | block(env) 18 | } finally { 19 | detachCurrentThread() 20 | } 21 | } 22 | 23 | // Just for compiler plugin, too lazy to use the property 24 | // @PublishedApi internal fun JavaVirtualMachine.requireEnv(): JniEnvironment = env!! 25 | 26 | // Just for compiler plugin, too lazy to use the property 27 | // @PublishedApi internal fun JniEnvironment.requireJavaVM(): JavaVirtualMachine = javaVM 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | DEPLOY: 7 | name: GitHub and Maven Central publication 8 | runs-on: ubuntu-latest 9 | env: 10 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 11 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 12 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 13 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 14 | GHUB_USER: ${{ secrets.GHUB_USER }} 15 | GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | java-version: 17 21 | distribution: temurin 22 | cache: gradle 23 | - name: Publish to Maven Central 24 | run: ./gradlew deployNexus --stacktrace 25 | - name: Publish to GitHub Packages 26 | run: ./gradlew deployGithub --stacktrace 27 | -------------------------------------------------------------------------------- /knee-gradle-plugin/src/main/kotlin/tasks/UnpackageCodegenSources.kt: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import org.gradle.api.file.ConfigurableFileCollection 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.file.ProjectLayout 6 | import org.gradle.api.model.ObjectFactory 7 | import org.gradle.api.tasks.Copy 8 | import org.gradle.api.tasks.InputFiles 9 | import org.gradle.api.tasks.OutputDirectory 10 | import javax.inject.Inject 11 | 12 | open class UnpackageCodegenSources @Inject constructor(objects: ObjectFactory, layout: ProjectLayout) : Copy() { 13 | @get:InputFiles 14 | val codegenFiles: ConfigurableFileCollection = objects.fileCollection() 15 | 16 | @get:OutputDirectory 17 | val outputDir: DirectoryProperty = objects.directoryProperty() 18 | .convention(layout.buildDirectory.dir("codegen")) 19 | 20 | init { 21 | from(codegenFiles) 22 | into(outputDir) 23 | exclude { it.name == "META-INF" } 24 | } 25 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/instances/InterfaceNames.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.instances 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 4 | import io.deepmedia.tools.knee.plugin.compiler.utils.map 5 | import org.jetbrains.kotlin.name.Name 6 | 7 | /** 8 | * Interface called 'Foo' becomes: 9 | * - KneeFoo (if not imported) 10 | * - KneeFoo$propertyName (if imported through property) 11 | */ 12 | object InterfaceNames { 13 | // val interfacePrefixMapper: (String) -> String = { "Knee$it" } 14 | 15 | private fun interfaceNameMapper(importInfo: ImportInfo?): (String) -> String { 16 | return { "Knee$it${importInfo?.let { info -> "$${info.id}" } ?: ""}" } 17 | } 18 | 19 | fun Name.asInterfaceName(importInfo: ImportInfo?): Name = map(block = interfaceNameMapper(importInfo)) 20 | 21 | fun String.asInterfaceName(importInfo: ImportInfo?): String = interfaceNameMapper(importInfo).invoke(this) 22 | } -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/compiler/Buffers.kn.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.compiler 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import platform.android.* 5 | 6 | 7 | private lateinit var bufferUtils: jclass 8 | private lateinit var setByteBufferOrderMethod: jmethodID 9 | 10 | internal fun initBuffers(environment: JniEnvironment) { 11 | bufferUtils = ClassIds.get(environment, "io.deepmedia.tools.knee.runtime.compiler.KneeBuffers") 12 | setByteBufferOrderMethod = MethodIds.get( 13 | env = environment, 14 | classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeBuffers", 15 | methodName = "setNativeOrder", 16 | methodSignature = "(Ljava/nio/ByteBuffer;)V", 17 | static = true, 18 | classObject = bufferUtils 19 | ) 20 | } 21 | 22 | internal fun JniEnvironment.setDirectBufferNativeOrder(buffer: jobject) { 23 | callStaticVoidMethod(bufferUtils, setByteBufferOrderMethod, buffer) 24 | } 25 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/buffer/IntBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.buffer 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.jobject 6 | 7 | class IntBuffer private constructor(private val bytes: ByteBuffer) { 8 | 9 | @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( 10 | environment = environment, 11 | jobject = jobject, 12 | storage = null, 13 | freeStorage = { }, 14 | size = environment.getDirectBufferCapacity(jobject).toInt() * 4 15 | )) 16 | 17 | constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( 18 | environment = environment, 19 | size = size * 4 20 | )) 21 | 22 | @PublishedApi internal val obj get() = bytes.obj 23 | val size: Int get() = bytes.size / 4 24 | val ptr: CArrayPointer = bytes.ptr.reinterpret() 25 | fun free() = bytes.free() 26 | } -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/buffer/LongBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.buffer 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.jobject 6 | 7 | class LongBuffer private constructor(private val bytes: ByteBuffer) { 8 | 9 | @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( 10 | environment = environment, 11 | jobject = jobject, 12 | storage = null, 13 | freeStorage = { }, 14 | size = environment.getDirectBufferCapacity(jobject).toInt() * 8 15 | )) 16 | 17 | constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( 18 | environment = environment, 19 | size = size * 8 20 | )) 21 | 22 | @PublishedApi internal val obj get() = bytes.obj 23 | val size: Int get() = bytes.size / 8 24 | val ptr: CArrayPointer = bytes.ptr.reinterpret() 25 | fun free() = bytes.free() 26 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/symbols/PackageNames.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.symbols 2 | 3 | import org.jetbrains.kotlin.name.FqName 4 | 5 | object PackageNames { 6 | val kotlin = FqName("kotlin") 7 | val kotlinCollections = FqName("kotlin.collections") 8 | val kotlinCoroutines = FqName("kotlin.coroutines") 9 | val cinterop = FqName("kotlinx.cinterop") 10 | val platformAndroid = FqName("platform.android") 11 | val annotations = FqName("io.deepmedia.tools.knee.annotations") 12 | val runtime = FqName("io.deepmedia.tools.knee.runtime") 13 | val runtimeCompiler = FqName("io.deepmedia.tools.knee.runtime.compiler") 14 | val runtimeTypes = FqName("io.deepmedia.tools.knee.runtime.types") 15 | val runtimeCollections = FqName("io.deepmedia.tools.knee.runtime.collections") 16 | val runtimeBuffer = FqName("io.deepmedia.tools.knee.runtime.buffer") 17 | val runtimeModule = FqName("io.deepmedia.tools.knee.runtime.module") 18 | } 19 | 20 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/buffer/FloatBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.buffer 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.jobject 6 | 7 | class FloatBuffer private constructor(private val bytes: ByteBuffer) { 8 | 9 | @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( 10 | environment = environment, 11 | jobject = jobject, 12 | storage = null, 13 | freeStorage = { }, 14 | size = environment.getDirectBufferCapacity(jobject).toInt() * 4 15 | )) 16 | 17 | constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( 18 | environment = environment, 19 | size = size * 4 20 | )) 21 | 22 | @PublishedApi internal val obj get() = bytes.obj 23 | val size: Int get() = bytes.size / 4 24 | val ptr: CArrayPointer = bytes.ptr.reinterpret() 25 | fun free() = bytes.free() 26 | } -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/buffer/DoubleBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.buffer 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.jobject 6 | 7 | class DoubleBuffer private constructor(private val bytes: ByteBuffer) { 8 | 9 | @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( 10 | environment = environment, 11 | jobject = jobject, 12 | storage = null, 13 | freeStorage = { }, 14 | size = environment.getDirectBufferCapacity(jobject).toInt() * 8 15 | )) 16 | 17 | constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( 18 | environment = environment, 19 | size = size * 8 20 | )) 21 | 22 | @PublishedApi internal val obj get() = bytes.obj 23 | val size: Int get() = bytes.size / 8 24 | val ptr: CArrayPointer = bytes.ptr.reinterpret() 25 | fun free() = bytes.free() 26 | } -------------------------------------------------------------------------------- /tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/DefaultValuesTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | 5 | 6 | class DefaultValuesTests { 7 | 8 | companion object { 9 | init { 10 | System.loadLibrary("test_misc") 11 | } 12 | } 13 | 14 | @Test 15 | fun testDefaultValue_function() { 16 | nullableWithNullDefaultValue() 17 | } 18 | 19 | @Test 20 | fun testDefaultValue_class() { 21 | ConcreteClassWithDefaultValues().withNull() 22 | } 23 | 24 | @Test 25 | fun testDefaultValue_emptyString() { 26 | emptyStringDefaultValue() 27 | } 28 | 29 | @Test 30 | fun testDefaultValue_enum() { 31 | enumDefaultValue() 32 | } 33 | 34 | @Test 35 | fun testDefaultValue_float() { 36 | floatDefaultValue() 37 | } 38 | 39 | @Test 40 | fun testDefaultValue_long() { 41 | floatDefaultValue() 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/test-misc/src/backendMain/kotlin/DefaultValuesDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import io.deepmedia.tools.knee.runtime.buffer.* 6 | 7 | 8 | @Knee 9 | fun nullableWithNullDefaultValue(foo: Int? = null) { 10 | } 11 | 12 | interface BaseInterfaceWithDefaultValues { 13 | fun withNull(foo: Int? = null) 14 | } 15 | 16 | @KneeClass class ConcreteClassWithDefaultValues @Knee constructor() : BaseInterfaceWithDefaultValues { 17 | @Knee 18 | override fun withNull(foo: Int?) { 19 | 20 | } 21 | } 22 | 23 | @Knee 24 | fun emptyStringDefaultValue(foo: String = "") { 25 | } 26 | 27 | @Knee 28 | fun floatDefaultValue(foo: Float = 1F) { 29 | } 30 | 31 | @Knee 32 | fun longDefaultValue(foo: Long = 1000L) { 33 | } 34 | 35 | @Knee 36 | fun enumDefaultValue(foo: DefaultValuesEnum = DefaultValuesEnum.First) { 37 | } 38 | 39 | @KneeEnum 40 | enum class DefaultValuesEnum { 41 | First, Second 42 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeModule.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds 4 | import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex 5 | import org.jetbrains.kotlin.descriptors.ClassKind 6 | import org.jetbrains.kotlin.ir.declarations.IrClass 7 | import org.jetbrains.kotlin.ir.util.classId 8 | import org.jetbrains.kotlin.ir.util.isTopLevel 9 | import org.jetbrains.kotlin.ir.util.superClass 10 | 11 | class KneeModule(source: IrClass) : KneeFeature(source, "KneeModule") { 12 | init { 13 | source.requireNotComplex(this, ClassKind.OBJECT) 14 | requireNotNull(source.superClass?.takeIf { it.classId == RuntimeIds.KneeModule }) { 15 | "$this must extend KneeModule." 16 | } 17 | require(source.visibility.isPublicAPI) { "$this must be a public, top-level object." } 18 | require(source.isTopLevel) { "$this must be a public, top-level object." } 19 | } 20 | } -------------------------------------------------------------------------------- /experiments/.idea/runConfigurations/compose_notes_c.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /experiments/.idea/runConfigurations/compose_notes_l.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /experiments/.idea/runConfigurations/expect_actual_c.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /experiments/.idea/runConfigurations/expect_actual_l.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/runtime_deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/runConfigurations/gradle_plugin_deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportMiscTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | import kotlinx.coroutines.flow.toList 7 | import kotlinx.coroutines.runBlocking 8 | import org.junit.Test 9 | 10 | class ImportMiscTests { 11 | 12 | companion object { 13 | init { 14 | System.loadLibrary("test_imports") 15 | } 16 | } 17 | 18 | @Test 19 | fun testImportedEnumNullableProperty() { 20 | check(currentDeprecationLevel == null) 21 | currentDeprecationLevel = DeprecationLevel.HIDDEN 22 | check(currentDeprecationLevel == DeprecationLevel.HIDDEN) 23 | } 24 | 25 | @Test 26 | fun testImportedEnumArgumentsAndReturnType() { 27 | check(getStrongerDeprecationLevel(DeprecationLevel.WARNING) == DeprecationLevel.ERROR) 28 | check(getStrongerDeprecationLevel(DeprecationLevel.ERROR) == DeprecationLevel.HIDDEN) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.idea/runConfigurations/compiler_plugin_deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/deployGithub.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/runConfigurations/deploySonatype.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /knee-gradle-plugin/src/main/kotlin/utils/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.gradle.utils 2 | 3 | import com.android.build.api.dsl.CommonExtension 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.getByName 6 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 7 | import org.jetbrains.kotlin.konan.target.KonanTarget 8 | 9 | 10 | internal val KotlinNativeTarget.androidAbi get() = when (konanTarget) { 11 | is KonanTarget.ANDROID_ARM32 -> "armeabi-v7a" 12 | is KonanTarget.ANDROID_ARM64 -> "arm64-v8a" 13 | is KonanTarget.ANDROID_X64 -> "x86_64" 14 | is KonanTarget.ANDROID_X86 -> "x86" 15 | else -> error("Unknown KonanTarget $konanTarget") 16 | } 17 | 18 | internal fun Project.configureAndroidExtension(block: (CommonExtension<*, *, *, *, *>) -> Unit) { 19 | fun runBlock() { 20 | val android = extensions.getByName>("android") 21 | block(android) 22 | } 23 | plugins.withId("com.android.application") { runBlock() } 24 | plugins.withId("com.android.library") { runBlock() } 25 | } -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/link_misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/runConfigurations/annotations_deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/link_imports.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/link_primitives.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeUpwardProperty.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 4 | import org.jetbrains.kotlin.ir.declarations.IrProperty 5 | import org.jetbrains.kotlin.ir.util.copyAnnotationsFrom 6 | 7 | class KneeUpwardProperty( 8 | source: IrProperty, 9 | parentInterface: KneeInterface? 10 | ) : KneeFeature(source, "Knee⬆") { 11 | 12 | sealed class Kind { 13 | class InterfaceMember(val parent: KneeInterface) : Kind() 14 | 15 | val importInfo: ImportInfo? get() = when (this) { 16 | is InterfaceMember -> parent.importInfo 17 | } 18 | } 19 | 20 | val kind = Kind.InterfaceMember(parentInterface!!) 21 | 22 | val setter: KneeUpwardFunction? = source.setter?.let { 23 | it.copyAnnotationsFrom(source) 24 | KneeUpwardFunction(it, parentInterface) 25 | } 26 | 27 | val getter: KneeUpwardFunction = requireNotNull(source.getter) { "$this must have a getter." }.let { 28 | it.copyAnnotationsFrom(source) 29 | KneeUpwardFunction(it, parentInterface) 30 | } 31 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/deepmedia/Knee/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/deepmedia/Knee/actions) 2 | [![Release](https://img.shields.io/github/release/deepmedia/Knee.svg)](https://github.com/deepmedia/Knee/releases) 3 | [![Issues](https://img.shields.io/github/issues-raw/deepmedia/Knee.svg)](https://github.com/deepmedia/Knee/issues) 4 | 5 | ![Project logo](assets/logo_256.png) 6 | 7 | # 🦵 Knee 🦵 8 | 9 | A Kotlin compiler plugin and companion runtime tools that provides seamless communication between Kotlin/Native 10 | binaries and Kotlin/JVM, using a thin and efficient layer around the JNI interface. 11 | 12 | With Knee, you can write idiomatic Kotlin/Native code, annotate it and then invoke it transparently from JVM 13 | as if they were running on the same environment. 14 | 15 | ```kotlin 16 | // settings.gradle.kts 17 | pluginManagement { 18 | repositories { 19 | gradlePluginPortal() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | // build.gradle.kts 25 | plugins { 26 | id("io.deepmedia.tools.knee") version "1.2.0" 27 | } 28 | ``` 29 | 30 | Please check out [the documentation](https://opensource.deepmedia.io/knee). 31 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/export/v2/ExportedTypeInfo.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.export.v2 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.codec.Codec 4 | import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType 5 | import io.deepmedia.tools.knee.plugin.compiler.jni.JniType 6 | import kotlinx.serialization.Contextual 7 | import kotlinx.serialization.Serializable 8 | import org.jetbrains.kotlin.ir.types.IrSimpleType 9 | import org.jetbrains.kotlin.ir.types.IrType 10 | 11 | 12 | @Serializable 13 | data class ExportedTypeInfo( 14 | val id: Int, // unique within the same module 15 | @Contextual val localIrType: IrSimpleType, 16 | val localCodegenType: CodegenType, 17 | val encodedType: JniType.Real 18 | ) { 19 | constructor(id: Int, codec: Codec) : this( 20 | id = id, 21 | localIrType = codec.localIrType, 22 | localCodegenType = codec.localCodegenType, 23 | encodedType = checkNotNull(codec.encodedType as? JniType.Real) { 24 | "Can't export ${codec.localIrType}, its jni representation is not JniType.Real" 25 | } 26 | ) 27 | // val uniqueId: Int get() = localIrType.disambiguationHash() 28 | } -------------------------------------------------------------------------------- /tests/test-coroutines/src/backendMain/kotlin/SuspendDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.delay 6 | 7 | 8 | @Knee 9 | suspend fun sumInts(first: Int, second: Int, delay: Long): Int { 10 | if (delay > 0) kotlinx.coroutines.delay(delay) 11 | return first + second 12 | } 13 | 14 | @Knee 15 | suspend fun sumStrings(first: String, second: String, delay: Long): String { 16 | if (delay > 0) kotlinx.coroutines.delay(delay) 17 | return first + second 18 | } 19 | 20 | @Knee 21 | suspend fun sumLists(first: List, second: List, delay: Long): List { 22 | if (delay > 0) kotlinx.coroutines.delay(delay) 23 | return first + second 24 | } 25 | 26 | @Knee 27 | suspend fun sumNullableStrings(first: String?, second: String?, delay: Long): String? { 28 | if (delay > 0) kotlinx.coroutines.delay(delay) 29 | if (first == null && second == null) return null 30 | return "$first$second" 31 | } 32 | 33 | @Knee 34 | suspend fun crash(message: String, delay: Long): Unit { 35 | if (delay > 0) kotlinx.coroutines.delay(delay) 36 | error(message) 37 | } -------------------------------------------------------------------------------- /experiments/compose-notes/src/backendMain/kotlin/NoteManager.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | 6 | @KneeClass class NoteManager @Knee constructor() { 7 | 8 | private val notes = mutableListOf(*FakeNotes.sortedBy { it.date }.toTypedArray()) 9 | private val callbacks = mutableListOf() 10 | 11 | @Knee fun addNote(note: Note) { 12 | if (notes.add(note)) { 13 | callbacks.forEach { it.onNoteAdded(note) } 14 | } 15 | } 16 | 17 | @Knee fun removeNote(id: String) { 18 | notes.filter { it.id == id }.forEach { note -> 19 | notes.remove(note) 20 | callbacks.forEach { it.onNoteRemoved(note) } 21 | } 22 | } 23 | 24 | @Knee val current: List get() = notes 25 | 26 | @Knee val size get() = notes.size 27 | 28 | @Knee fun registerCallback(callback: Callback) { 29 | callbacks.add(callback) 30 | } 31 | 32 | @Knee fun unregisterCallback(callback: Callback) { 33 | callbacks.remove(callback) 34 | } 35 | 36 | @KneeInterface 37 | interface Callback { 38 | fun onNoteAdded(note: Note) 39 | fun onNoteRemoved(note: Note) 40 | } 41 | } -------------------------------------------------------------------------------- /knee-runtime/src/frontendMain/kotlin/compiler/Instances.jvm.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("InstancesKt") 2 | @file:Suppress("RedundantVisibilityModifier") 3 | 4 | package io.deepmedia.tools.knee.runtime.compiler 5 | 6 | @Suppress("Unused") 7 | public external fun kneeDisposeInstance(ref: Long) 8 | 9 | @Suppress("Unused") 10 | public external fun kneeHashInstance(ref: Long): Int 11 | 12 | @Suppress("Unused") 13 | public external fun kneeCompareInstance(ref0: Long, ref1: Long): Boolean 14 | 15 | @Suppress("Unused") 16 | public external fun kneeDescribeInstance(ref: Long): String 17 | 18 | // Turns out we don't need these. We just act from JNI which has no access control 19 | 20 | /* @Suppress("Unused") 21 | public fun kneeUnwrapInstance(instance: Any): Long { 22 | return try { 23 | val field = instance.javaClass.getDeclaredField("\$knee") 24 | field.isAccessible = true 25 | field.getLong(instance) 26 | } catch (e: Throwable) { 27 | 0L 28 | } 29 | } 30 | 31 | @Suppress("Unused") 32 | public fun kneeWrapInstance(ref: Long, className: String): Any? { 33 | return try { 34 | Class.forName(className).getDeclaredConstructor(Long::class.java).newInstance(ref) 35 | } catch (e: Throwable) { 36 | null 37 | } 38 | } */ -------------------------------------------------------------------------------- /experiments/multimodule-consumer/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.mm.consumer 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import io.deepmedia.tools.knee.sample.mm.producer.ProducerClass 6 | import io.deepmedia.tools.knee.sample.mm.producer.ProducerEnum 7 | import io.deepmedia.tools.knee.sample.mm.producer.ProducerInterface 8 | import io.deepmedia.tools.knee.sample.mm.producer.initProducerKnee 9 | import kotlin.random.Random 10 | 11 | @CName(externName = "JNI_OnLoad") 12 | @KneeInit 13 | fun initKnee(jvm: JavaVirtualMachine) { 14 | jvm.useEnv { env -> 15 | initProducerKnee(env) 16 | } 17 | } 18 | 19 | @KneeEnum 20 | enum class ConsumerEnum { 21 | Foo, Bar 22 | } 23 | 24 | @Knee 25 | fun getConsumerEnum(): ConsumerEnum { 26 | return ConsumerEnum.Foo 27 | } 28 | 29 | @Knee 30 | fun getProducerEnumExportedByConsumer(): ProducerEnum { 31 | return ProducerEnum.Bar 32 | } 33 | 34 | @Knee 35 | fun getProducerClassExportedByConsumer(): ProducerClass { 36 | return ProducerClass() 37 | } 38 | 39 | @Knee 40 | fun getProducerInterfaceExportedByConsumer(): ProducerInterface { 41 | return object : ProducerInterface { 42 | override val foo: Int 43 | get() = 20 44 | } 45 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeEnum.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 4 | import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex 5 | import org.jetbrains.kotlin.descriptors.ClassKind 6 | import org.jetbrains.kotlin.ir.IrElement 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrEnumEntry 9 | import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid 10 | import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid 11 | 12 | class KneeEnum( 13 | source: IrClass, 14 | val importInfo: ImportInfo? = null 15 | ) : KneeFeature(source, "KneeEnum") { 16 | 17 | val entries: List 18 | 19 | init { 20 | source.requireNotComplex(this, ClassKind.ENUM_CLASS) 21 | val entries = mutableListOf() 22 | source.acceptChildrenVoid(object : IrElementVisitorVoid { 23 | override fun visitElement(element: IrElement) = Unit 24 | override fun visitEnumEntry(declaration: IrEnumEntry) { 25 | entries.add(declaration) 26 | super.visitEnumEntry(declaration) 27 | } 28 | }) 29 | this.entries = entries 30 | } 31 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | import java.lang.System.identityHashCode 5 | import kotlin.math.abs 6 | 7 | class PrimitiveTests { 8 | 9 | companion object { 10 | init { 11 | System.loadLibrary("test_primitives") 12 | } 13 | } 14 | 15 | @Test 16 | fun testInts() { 17 | check(10 == sumInts(3, 7)) 18 | } 19 | 20 | @Test 21 | fun testLongs() { 22 | check(10000000000L == sumLongs(3000000000L, 7000000000L)) 23 | } 24 | 25 | @Test 26 | fun testBytes() { 27 | check(10.toByte() == sumBytes(3, 7)) 28 | } 29 | 30 | @Test 31 | fun testFloats() { 32 | check(abs(1F - sumFloats(0.3F, 0.7F)) < 0.001F) 33 | } 34 | 35 | @Test 36 | fun testDoubles() { 37 | check(abs(1.0 - sumDoubles(0.3, 0.7)) < 0.001) 38 | } 39 | 40 | @Test 41 | fun testStrings() { 42 | check("Hello, world!" == sumStrings("Hello, ", "world!")) 43 | } 44 | 45 | @Test 46 | fun testBooleans() { 47 | check(andBooleans(true, true)) 48 | check(!andBooleans(false, true)) 49 | check(!andBooleans(false, false)) 50 | check(orBooleans(true, false)) 51 | check(orBooleans(true, true)) 52 | check(!orBooleans(false, false)) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/export/v1/ExportFlags.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.export.v1 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds 4 | import org.jetbrains.kotlin.descriptors.ClassDescriptor 5 | import org.jetbrains.kotlin.ir.declarations.IrClass 6 | import org.jetbrains.kotlin.ir.expressions.IrConst 7 | import org.jetbrains.kotlin.ir.util.getAnnotation 8 | import org.jetbrains.kotlin.ir.util.getValueArgument 9 | import org.jetbrains.kotlin.name.Name 10 | import org.jetbrains.kotlin.resolve.constants.BooleanValue 11 | 12 | val IrClass.hasExport1Flag: Boolean get() { 13 | val e = getAnnotation(AnnotationIds.KneeClass) 14 | ?: getAnnotation(AnnotationIds.KneeEnum) 15 | ?: getAnnotation(AnnotationIds.KneeInterface) 16 | ?: return false 17 | 18 | val a = e.getValueArgument(Name.identifier("exported")) ?: return false 19 | @Suppress("UNCHECKED_CAST") 20 | return (a as? IrConst)?.value ?: false 21 | } 22 | 23 | val ClassDescriptor.hasExport1Flag: Boolean get() { 24 | val e = annotations.findAnnotation(AnnotationIds.KneeClass) 25 | ?: annotations.findAnnotation(AnnotationIds.KneeEnum) 26 | ?: annotations.findAnnotation(AnnotationIds.KneeInterface) 27 | ?: return false 28 | 29 | val arg = e.allValueArguments[Name.identifier("exported")] ?: return false 30 | return (arg as? BooleanValue)?.value ?: false 31 | } -------------------------------------------------------------------------------- /experiments/expect-actual/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import androidx.activity.compose.setContent 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import kotlinx.coroutines.flow.flowOf 14 | 15 | 16 | class ExpectActualActivity : androidx.activity.ComponentActivity() { 17 | companion object { 18 | init { 19 | System.loadLibrary("expect_actual") 20 | } 21 | } 22 | 23 | override fun onCreate(savedInstanceState: android.os.Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContent { 26 | MaterialTheme(colors = lightColors()) { 27 | Surface { 28 | RootScreen(Modifier.fillMaxSize()) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | 36 | @Composable 37 | fun RootScreen(modifier: Modifier = Modifier) { 38 | Column(modifier) { 39 | Text(jvmToString()) 40 | Text(targetName()) 41 | Text(PlatformInfoA().targetName) 42 | Text(PlatformInfoB().targetName) 43 | } 44 | } -------------------------------------------------------------------------------- /tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerInterfaceTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | import java.lang.System.identityHashCode 5 | 6 | class InnerInterfaceTests { 7 | 8 | companion object { 9 | init { 10 | System.loadLibrary("test_interfaces") 11 | } 12 | } 13 | 14 | val jvm = object : Outer.Inner { 15 | override var value: Int = 24 16 | } 17 | 18 | @Test 19 | fun testProperty_retrieveNatively() { 20 | val kn = makeInner(0) 21 | // update on JVM, retreive natively 22 | jvm.value = 100 23 | kn.value = 100 24 | require(jvm.value == getInnerValue(kn)) 25 | // update on KN, retreive natively 26 | setInnerValue(jvm, 200) 27 | setInnerValue(kn, 200) 28 | require(jvm.value == getInnerValue(kn)) 29 | } 30 | 31 | @Test 32 | fun testProperty_updateNatively() { 33 | val kn = makeInner(0) 34 | // update natively, retreive on JVM 35 | jvm.value = 300 36 | setInnerValue(kn, 300) 37 | require(jvm.value == kn.value) 38 | require(jvm.value == 300) 39 | 40 | // update natively, retreive on KN 41 | jvm.value = 400 42 | setInnerValue(kn, 400) 43 | require(getInnerValue(jvm) == getInnerValue(kn)) 44 | require(getInnerValue(kn) == 400) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/install.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Install 3 | description: Follow the instructions to download Knee Gradle Plugin and runtime and discover how to install release snapshots. 4 | --- 5 | 6 | # Installation 7 | 8 | Knee can be installed into your project using a Gradle Plugin. 9 | The plugin will take care of adding runtime dependencies, applying the Kotlin Compiler plugin and more. 10 | 11 | ## Configuration 12 | 13 | To use `Knee` in your project, add the following lines: 14 | 15 | ```kotlin 16 | // settings.gradle.kts 17 | pluginManagement { 18 | repositories { 19 | gradlePluginPortal() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | // build.gradle.kts 25 | plugins { 26 | id("io.deepmedia.tools.knee") version "LATEST_VERSION" 27 | } 28 | ``` 29 | 30 | Replace `LATEST_VERSION` with the desired version number, {version}. 31 | 32 | ## Snapshots 33 | 34 | We regularly push development snapshots of the library at `https://s01.oss.sonatype.org/content/repositories/snapshots/` 35 | on each push to main. To use snapshots, add the url as a maven repository and depend on `latest-SNAPSHOT`: 36 | 37 | ```kotlin 38 | // settings.gradle.kts 39 | pluginManagement { 40 | repositories { 41 | gradlePluginPortal() 42 | maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") 43 | } 44 | } 45 | 46 | // build.gradle.kts 47 | plugins { 48 | id("io.deepmedia.tools.knee") version "latest-SNAPSHOT" 49 | } 50 | ``` -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 2 | name: Build 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | jobs: 9 | CHECK_DEPLOY: 10 | name: Deploy locally 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | java-version: 17 17 | distribution: temurin 18 | cache: gradle 19 | - uses: gradle/actions/wrapper-validation@v4 20 | - name: Check local deployment 21 | run: ./gradlew build deployLocal 22 | CHECK_TESTS: 23 | name: Run emulator tests 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-java@v4 28 | with: 29 | java-version: 17 30 | distribution: temurin 31 | cache: gradle 32 | - name: Enable KVM group perms 33 | run: | 34 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 35 | sudo udevadm control --reload-rules 36 | sudo udevadm trigger --name-match=kvm 37 | - name: Run tests 38 | uses: reactivecircus/android-emulator-runner@v2 39 | with: 40 | api-level: 29 41 | script: cd tests && ./gradlew connectedCheck --stacktrace -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/Init.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime 2 | 3 | import io.deepmedia.tools.knee.runtime.compiler.* 4 | import io.deepmedia.tools.knee.runtime.compiler.initBoxMethods 5 | import io.deepmedia.tools.knee.runtime.compiler.initBuffers 6 | import io.deepmedia.tools.knee.runtime.compiler.initExceptions 7 | import io.deepmedia.tools.knee.runtime.compiler.initInstances 8 | import io.deepmedia.tools.knee.runtime.compiler.initSuspend 9 | import io.deepmedia.tools.knee.runtime.module.KneeModule 10 | import kotlinx.atomicfu.atomic 11 | 12 | private val kneeInitialized = atomic(0L) 13 | 14 | internal var initializationData: InitializationData? = null 15 | 16 | internal class InitializationData( 17 | val jvm: JavaVirtualMachine, 18 | val exceptions: MutableSet 19 | ) 20 | 21 | fun initKnee(environment: JniEnvironment, vararg modules: KneeModule) { 22 | val vm = environment.javaVM 23 | val id = vm.rawValue.toLong() 24 | val oldId = kneeInitialized.getAndSet(id) 25 | if (id != oldId) { 26 | initializationData = InitializationData(vm, mutableSetOf()) 27 | initSuspend(environment) 28 | initInstances(environment) 29 | initBoxMethods(environment) 30 | initExceptions(environment) 31 | initBuffers(environment) 32 | } 33 | val data = initializationData!! 34 | modules.forEach { 35 | it.collectExceptions(data.exceptions) 36 | it.initializeIfNeeded(environment) 37 | } 38 | } -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/types/Interfaces.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.types 2 | 3 | import io.deepmedia.tools.knee.runtime.JniEnvironment 4 | import io.deepmedia.tools.knee.runtime.compiler.ClassIds 5 | import io.deepmedia.tools.knee.runtime.compiler.JvmInterfaceWrapper 6 | import io.deepmedia.tools.knee.runtime.getObjectClass 7 | import io.deepmedia.tools.knee.runtime.isSameObject 8 | import kotlinx.cinterop.* 9 | import platform.android.jlong 10 | import platform.android.jobject 11 | 12 | 13 | @Suppress("unused") 14 | @PublishedApi 15 | internal fun encodeInterface(environment: JniEnvironment, interface_: T): jobject { 16 | return if (interface_ is JvmInterfaceWrapper<*>) { 17 | interface_.jvmInterfaceObject 18 | } else { 19 | val address: jlong = StableRef.create(interface_).asCPointer().toLong() 20 | encodeBoxedLong(environment, address) 21 | } 22 | } 23 | 24 | @Suppress("unused") 25 | internal inline fun decodeInterface( 26 | environment: JniEnvironment, 27 | interface_: jobject, 28 | wrapper: () -> JvmInterfaceWrapper 29 | ): T { 30 | val interfaceClass = environment.getObjectClass(interface_) 31 | val longClass = ClassIds.get(environment, "java.lang.Long") 32 | return if (environment.isSameObject(interfaceClass, longClass)) { 33 | val longValue = decodeBoxedLong(environment, interface_) 34 | longValue.toCPointer()!!.asStableRef().get() 35 | } else { 36 | wrapper() as T 37 | } 38 | } -------------------------------------------------------------------------------- /tests/test-coroutines/src/backendMain/kotlin/ReverseSuspendDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.delay 6 | 7 | @KneeInterface 8 | interface ReverseUtil { 9 | suspend fun sumInts(first: Int, second: Int, delay: Long): Int 10 | suspend fun sumStrings(first: String, second: String, delay: Long): String 11 | suspend fun sumNullableStrings(first: String?, second: String?, delay: Long): String? 12 | suspend fun sumLists(first: List, second: List, delay: Long): List 13 | suspend fun crash(message: String, delay: Long): Unit 14 | } 15 | 16 | @Knee 17 | suspend fun invokeSumInts(receiver: ReverseUtil, first: Int, second: Int, delay: Long): Int { 18 | return receiver.sumInts(first, second, delay) 19 | } 20 | 21 | @Knee 22 | suspend fun invokeSumStrings(receiver: ReverseUtil, first: String, second: String, delay: Long): String { 23 | return receiver.sumStrings(first, second, delay) 24 | } 25 | 26 | @Knee 27 | suspend fun invokeSumNullableStrings(receiver: ReverseUtil, first: String?, second: String?, delay: Long): String? { 28 | return receiver.sumNullableStrings(first, second, delay) 29 | } 30 | 31 | @Knee 32 | suspend fun invokeSumLists(receiver: ReverseUtil, first: List, second: List, delay: Long): List { 33 | return receiver.sumLists(first, second, delay) 34 | } 35 | 36 | @Knee 37 | suspend fun invokeCrash(receiver: ReverseUtil, message: String, delay: Long) { 38 | return receiver.crash(message, delay) 39 | } 40 | 41 | -------------------------------------------------------------------------------- /tests/test-misc/src/backendMain/kotlin/EnumDefinintions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | 5 | @KneeEnum 6 | enum class Day { 7 | Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday 8 | } 9 | 10 | class OuterClass { 11 | @KneeEnum 12 | enum class InnerDay { 13 | Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday 14 | } 15 | } 16 | 17 | interface OuterInterface { 18 | @KneeEnum 19 | enum class InnerDay { 20 | Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday 21 | } 22 | } 23 | 24 | @Knee var currentDay: Day? = null 25 | @Knee var currentInnerDay1: OuterClass.InnerDay? = null 26 | @Knee var currentInnerDay2: OuterInterface.InnerDay? = null 27 | 28 | @Knee 29 | fun getDayAfter(day: Day): Day { 30 | if (day == Day.Sunday) return Day.Monday 31 | else return Day.entries[Day.entries.indexOf(day) + 1] 32 | } 33 | 34 | @Knee 35 | fun getAllDays(): List { 36 | return Day.entries.toList() 37 | } 38 | 39 | @Knee 40 | fun getInnerDay1After(day: OuterClass.InnerDay): OuterClass.InnerDay { 41 | if (day == OuterClass.InnerDay.Sunday) return OuterClass.InnerDay.Monday 42 | else return OuterClass.InnerDay.entries[OuterClass.InnerDay.entries.indexOf(day) + 1] 43 | } 44 | 45 | @Knee 46 | fun getInnerDay2After(day: OuterInterface.InnerDay): OuterInterface.InnerDay { 47 | if (day == OuterInterface.InnerDay.Saturday) return OuterInterface.InnerDay.Sunday 48 | else return OuterInterface.InnerDay.entries[OuterInterface.InnerDay.entries.indexOf(day) + 1] 49 | } 50 | 51 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/symbols/KneeSymbols.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.symbols 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 4 | import org.jetbrains.kotlin.ir.IrBuiltIns 5 | import org.jetbrains.kotlin.ir.symbols.IrClassSymbol 6 | import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol 7 | import org.jetbrains.kotlin.ir.symbols.IrTypeAliasSymbol 8 | import org.jetbrains.kotlin.ir.types.IrType 9 | import org.jetbrains.kotlin.name.CallableId 10 | import org.jetbrains.kotlin.name.ClassId 11 | 12 | 13 | class KneeSymbols(private val plugin: IrPluginContext) { 14 | 15 | val builtIns: IrBuiltIns get() = plugin.irBuiltIns 16 | 17 | private val classes2 = mutableMapOf() 18 | private val typeAliases2 = mutableMapOf() 19 | private val functions2 = mutableMapOf>() 20 | 21 | fun klass(classId: ClassId): IrClassSymbol = classes2.getOrPut(classId) { 22 | requireNotNull(plugin.referenceClass(classId)) { "Could not find classId $classId" } 23 | } 24 | 25 | fun functions(name: CallableId) = functions2.getOrPut(name) { 26 | plugin.referenceFunctions(name).also { 27 | require(it.isNotEmpty()) { "Could not find callableId $name" } 28 | } 29 | } 30 | 31 | fun typeAlias(name: ClassId) = typeAliases2.getOrPut(name) { 32 | requireNotNull(plugin.referenceTypeAlias(name)) { "Could not find type alias $name" } 33 | } 34 | 35 | 36 | fun typeAliasUnwrapped(name: ClassId): IrType = typeAlias(name).owner.expandedType 37 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/services/KneeComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.services 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.KneeIrGeneration 4 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 5 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 6 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 9 | import org.jetbrains.kotlin.config.CompilerConfiguration 10 | import java.io.File 11 | 12 | @OptIn(ExperimentalCompilerApi::class) 13 | class KneeComponentRegistrar : CompilerPluginRegistrar() { 14 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 15 | if (configuration[KneeCommandLineProcessor.KneeEnabled] == false) return 16 | val logs = configuration[CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY]!! 17 | val verboseLogs = configuration[KneeCommandLineProcessor.KneeVerboseLogs] ?: false 18 | val verboseRuntime = configuration[KneeCommandLineProcessor.KneeVerboseRuntime] ?: false 19 | val verboseCodegen = configuration[KneeCommandLineProcessor.KneeVerboseSources] ?: false 20 | val outputDir = File(configuration[KneeCommandLineProcessor.KneeOutputDir]!!) 21 | IrGenerationExtension.registerExtension(KneeIrGeneration(logs, verboseLogs, verboseRuntime, verboseCodegen, outputDir, true)) 22 | // if (legacyIo) { 23 | // SyntheticResolveExtension.registerExtension(KneeSyntheticResolve()) 24 | // } 25 | } 26 | 27 | override val supportsK2: Boolean 28 | get() = true 29 | } -------------------------------------------------------------------------------- /docs/features/callables.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Callables 3 | description: > 4 | Learn about the concept of callables in Knee - a set of function and properties that are explicitly marked 5 | to be serializable and represent the links between the Native and JVM part of your Kotlin project. 6 | --- 7 | 8 | # Callables 9 | 10 | We refer to functions and properties as *callables*. When appropriately annotated, callables can be invoked from 11 | either side of the JNI interface (frontend or backend), execute your code on the other side and return some value. 12 | 13 | ## Functions 14 | 15 | For a function to be available on the JVM side, it must be annotated with the `@Knee` annotation. 16 | We support top-level functions and functions nested in `@KneeClass` declarations, as you can learn in [classes](classes). 17 | Upward functions (called from K/N, implemented on the JVM) are also available through [interfaces](interfaces). 18 | 19 | ```kotlin 20 | // Kotlin/Native 21 | @Knee fun topLevelFunction(): Int { 22 | return 42 23 | } 24 | 25 | // Kotlin/JVM 26 | check(topLevelFunction() == 42) 27 | ``` 28 | 29 | ## Properties 30 | 31 | For a property to be available on the JVM side, it must be annotated with the `@Knee` annotation. 32 | We support top-level properties and properties nested in `@KneeClass` declarations, as you can learn in [classes](classes). 33 | Upward properties (called from K/N, implemented on the JVM) are also available through [interfaces](interfaces). 34 | 35 | Both `var` and `val` properties are supported. 36 | 37 | ```kotlin 38 | // Kotlin/Native 39 | @Knee val immutableProp: Int = 42 40 | @Knee var mutableProp: Int = 0 41 | 42 | // Kotlin/JVM 43 | mutableProp = immutableProp 44 | check(mutableProp == immutableProp) 45 | ``` 46 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/JvmApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.runtime 4 | 5 | import kotlinx.cinterop.* 6 | import platform.android.* 7 | 8 | // JNI APIs, with same namings and so on, just wrapped in a more convenient 9 | // fashion, as extensions to CPointer<*>. 10 | 11 | typealias JavaVirtualMachine = CPointer 12 | 13 | internal val JavaVirtualMachine.api get() = pointed.pointed!! 14 | 15 | /** 16 | * Returns the [JniEnvironment] attached to the current thread. The environment is 17 | * guaranteed to exist when calling this function from a JNI method, or more generally 18 | * from JVM-managed threads that have an associated java.lang.Thread. 19 | * In other cases, this function will return null - [attachCurrentThread] should be called instead. 20 | */ 21 | val JavaVirtualMachine.env: JniEnvironment? get() = memScoped { 22 | val envPointer: CPointerVar = allocPointerTo() 23 | val res = api.GetEnv!!(this@env, envPointer.ptr.reinterpret(), JNI_VERSION_1_6) 24 | // NOTE: GetEnv returns JNI_EDETACHED if the current thread is not attached to the VM. 25 | return if (res == JNI_OK) envPointer.pointed!!.ptr else null 26 | } 27 | 28 | fun JavaVirtualMachine.attachCurrentThread(): JniEnvironment = memScoped { 29 | val envPointer: CPointerVar = allocPointerTo() 30 | val res = api.AttachCurrentThread!!(this@attachCurrentThread, envPointer.ptr.reinterpret(), null) 31 | check(res == JNI_OK) { "AttachCurrentThread failed: $res" } 32 | envPointer.pointed!!.ptr 33 | } 34 | 35 | fun JavaVirtualMachine.detachCurrentThread() { 36 | val res = api.DetachCurrentThread!!(this) 37 | check(res == JNI_OK) { "DetachCurrentThread failed: $res" } 38 | } 39 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/context/KneeContext.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.context 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.serialization.IrClassListSerializer 4 | import io.deepmedia.tools.knee.plugin.compiler.serialization.IrClassSerializer 5 | import io.deepmedia.tools.knee.plugin.compiler.serialization.IrSimpleTypeSerializer 6 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 7 | import kotlinx.serialization.json.Json 8 | import kotlinx.serialization.modules.SerializersModule 9 | import kotlinx.serialization.modules.contextual 10 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 11 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 12 | import org.jetbrains.kotlin.ir.declarations.* 13 | import org.jetbrains.kotlin.ir.util.file 14 | 15 | 16 | object KneeOrigin { 17 | val KNEE by IrDeclarationOriginImpl.Synthetic 18 | val KNEE_IMPORT_PARENT by IrDeclarationOriginImpl.Synthetic 19 | } 20 | 21 | class KneeContext( 22 | val plugin: IrPluginContext, 23 | log: MessageCollector, 24 | verboseLogs: Boolean, 25 | verboseRuntime: Boolean, 26 | val module: IrModuleFragment, 27 | val useExport2: Boolean 28 | ) { 29 | 30 | val factory get() = plugin.irFactory 31 | 32 | val symbols = KneeSymbols(plugin) 33 | 34 | val json = Json { 35 | serializersModule = SerializersModule { 36 | contextual(IrClassSerializer(symbols)) 37 | contextual(IrClassListSerializer(symbols)) 38 | contextual(IrSimpleTypeSerializer(symbols)) 39 | } 40 | } 41 | 42 | val mapper by lazy { KneeMapper(this, json) } 43 | 44 | val log = KneeLogger(log, verboseLogs, verboseRuntime) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ObjectTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.SharedFlow 5 | import org.junit.Test 6 | 7 | class ObjectTests { 8 | 9 | companion object { 10 | init { 11 | System.loadLibrary("test_classes") 12 | } 13 | } 14 | 15 | @Test 16 | fun testTopLevel_exists() { 17 | TopLevelObject::class.toString() 18 | } 19 | 20 | @Test 21 | fun testTopLevel_toString() { 22 | TopLevelObject.reset() 23 | val tl = TopLevelObject.toString() 24 | check(tl == "TopLevelObject(0)") { tl } 25 | } 26 | 27 | @Test 28 | fun testTopLevel_functions() { 29 | TopLevelObject.reset() 30 | TopLevelObject.increment() 31 | TopLevelObject.increment() 32 | check(TopLevelObject.toString() == "TopLevelObject(2)") { TopLevelObject.toString() } 33 | TopLevelObject.decrement() 34 | check(TopLevelObject.toString() == "TopLevelObject(1)") { TopLevelObject.toString() } 35 | } 36 | 37 | @Test 38 | fun testTopLevel_property() { 39 | TopLevelObject.reset() 40 | TopLevelObject.value = 15 41 | check(TopLevelObject.value == 15) { TopLevelObject.value } 42 | } 43 | 44 | @Test 45 | fun testInner_property() { 46 | ObjectParent.InnerObject.value = 15 47 | check(ObjectParent.InnerObject.value == 15) { ObjectParent.InnerObject.value } 48 | } 49 | 50 | @Test 51 | fun testCompanion_property() { 52 | ObjectParent.value = 15 53 | check(ObjectParent.value == 15) { ObjectParent.value } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/NullablePrimitiveTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | import java.lang.System.identityHashCode 5 | import kotlin.math.abs 6 | 7 | class NullablePrimitiveTests { 8 | 9 | companion object { 10 | init { 11 | System.loadLibrary("test_primitives") 12 | } 13 | } 14 | 15 | @Test 16 | fun testNullableInts() { 17 | check("null" == printNullableInt(null)) 18 | check("7" == printNullableInt(7)) 19 | } 20 | 21 | @Test 22 | fun testNullableString() { 23 | check("null" == printNullableString(null)) 24 | check("hey" == printNullableString("hey")) 25 | } 26 | 27 | @Test 28 | fun testNullableBoolean() { 29 | check("null" == printNullableBoolean(null)) 30 | check("true" == printNullableBoolean(true)) 31 | } 32 | 33 | @Test 34 | fun testNullableByteArray() { 35 | check("null" == printNullableByteArray(null)) 36 | check("[0, 1, 2]" == printNullableByteArray(byteArrayOf(0, 1, 2))) 37 | } 38 | 39 | @Test 40 | fun testNullableBooleanList() { 41 | check("null" == printNullableBooleanList(null)) 42 | check("[false, true]" == printNullableBooleanList(listOf(false, true))) 43 | } 44 | 45 | @Test 46 | fun testNullableListSize() { 47 | check(null == getNullableListSize(null)) 48 | check(4 == getNullableListSize(listOf(0, 1, 2, 3))) 49 | } 50 | 51 | @Test 52 | fun testCreateNullableList() { 53 | check(null == createNullableList(null, null, null)) 54 | check(listOf(1) == createNullableList(null, 1, null)) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/codec/IdentityCodec.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.codec 2 | 3 | import com.squareup.kotlinpoet.CodeBlock 4 | import io.deepmedia.tools.knee.plugin.compiler.codec.Codec 5 | import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext 6 | import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext 7 | import io.deepmedia.tools.knee.plugin.compiler.jni.JniType 8 | import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder 9 | import org.jetbrains.kotlin.ir.builders.irGet 10 | import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration 11 | import org.jetbrains.kotlin.ir.expressions.IrExpression 12 | 13 | /** 14 | * A codec that needs no runtime transformations. This doesn't mean that kn and jvm types are identical, 15 | * some transformations might be done by the JNI runtime itself, but there's else we should do at the ends 16 | * of the bridge. 17 | */ 18 | class IdentityCodec(type: JniType.Real) : Codec(type.kn, type.jvm, type) { 19 | override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { 20 | return irGet(jni) 21 | } 22 | override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { 23 | return irGet(local) 24 | } 25 | override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { 26 | return jni 27 | } 28 | override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { 29 | return local 30 | } 31 | 32 | override fun toString(): String { 33 | return "IdentityCodec(${encodedType::class.simpleName})" 34 | } 35 | } -------------------------------------------------------------------------------- /tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/SuspendTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import kotlinx.coroutines.* 4 | import org.junit.Test 5 | 6 | class SuspendTests { 7 | 8 | companion object { 9 | init { 10 | System.loadLibrary("test_coroutines") 11 | } 12 | } 13 | 14 | @Test 15 | fun testSuspend() = runBlocking { 16 | check(30 == sumInts(10, 20, 0)) 17 | } 18 | 19 | @Test 20 | fun testSuspendWithDelay() = runBlocking { 21 | check(30 == sumInts(10, 20, 1000)) 22 | } 23 | 24 | // Use more complex types 25 | @Test 26 | fun testSuspendStrings() = runBlocking { 27 | check("Hello, world!" == sumStrings("Hello, ", "world!", 100)) 28 | } 29 | @Test 30 | fun testSuspendNullableStrings() = runBlocking { 31 | check(null == sumNullableStrings(null, null, 50)) 32 | check("foo:null" == sumNullableStrings("foo:", null, 50)) 33 | check("null:foo" == sumNullableStrings(null, ":foo", 50)) 34 | check("foo:foo" == sumNullableStrings("foo:", "foo", 50)) 35 | } 36 | 37 | @Test 38 | fun testSuspendLists() = runBlocking { 39 | check(listOf(0, 1, 2, 3) == sumLists(listOf(0, 1), listOf(2, 3), 100)) 40 | } 41 | 42 | @Test 43 | fun testCancellation() = runBlocking(Dispatchers.Default) { 44 | val res = withTimeoutOrNull(1000) { 45 | sumInts(0, 0, 2000) 46 | } 47 | check(res == null) 48 | } 49 | 50 | @Test 51 | fun testFailure() = runBlocking { 52 | val e = runCatching { crash("!!!", 100) }.exceptionOrNull() 53 | check(e?.message != null && e.message!!.contains("!!!")) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/test-imports/src/backendMain/kotlin/FlowDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.flow.* 6 | import kotlinx.coroutines.* 7 | import kotlinx.coroutines.channels.* 8 | import kotlin.coroutines.EmptyCoroutineContext 9 | import kotlin.coroutines.cancellation.CancellationException 10 | 11 | @KneeInterface typealias StringFlowCollector = FlowCollector 12 | @KneeInterface typealias StringFlow = Flow 13 | @KneeInterface typealias StringSharedFlow = SharedFlow 14 | @KneeInterface typealias StringMutableSharedFlow = MutableSharedFlow 15 | @KneeInterface typealias StringStateFlow = StateFlow 16 | @KneeInterface typealias IntStateFlow = StateFlow 17 | @KneeInterface typealias IntStateFlowCollector = FlowCollector 18 | @KneeInterface typealias StringMutableStateFlow = MutableStateFlow 19 | 20 | @Knee fun makeFlow(): Flow { 21 | return flow { 22 | delay(100) 23 | emit("Hello") 24 | delay(80) 25 | emit("from KN!") 26 | } 27 | } 28 | @Knee fun makeSharedFlow(): SharedFlow { 29 | return makeFlow().shareIn(CoroutineScope(EmptyCoroutineContext), SharingStarted.Lazily, 1) 30 | } 31 | 32 | @Knee suspend fun collectFlow(flow: Flow): String { 33 | return flow.toList().joinToString(separator = " ") 34 | } 35 | 36 | @Knee suspend fun collectSharedFlow(flow: SharedFlow): String { 37 | return flow.first() 38 | } 39 | 40 | @Knee fun makeMutableSharedFlow(replay: Int, onBufferOverflow: BufferOverflow): MutableSharedFlow { 41 | return MutableSharedFlow(replay = replay, onBufferOverflow = onBufferOverflow) 42 | } 43 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_imports.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_coroutines.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_primitives.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /docs/features/enums.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enums 3 | description: > 4 | Understand how Knee compiler plugin can serialize Kotlin enumerations and let you pass them from Kotlin Native 5 | to the JVM and vice versa, including support for externally defined enums. 6 | --- 7 | 8 | # Enums 9 | 10 | ## Annotating enums 11 | 12 | Enums can be easily serialized through their ordinal value. You can use the `@KneeEnum` annotation to tell the 13 | compiler that: 14 | 15 | - this native enum is expected to be serialized, so a JVM clone must be generated 16 | - the compiler must serialize and deserialize these types whenever they are part of a [callable](callables) declaration, e.g. a function argument or return type 17 | 18 | In the following example: 19 | 20 | ```kotlin 21 | @KneeEnum enum class DayOfWeek { 22 | Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday 23 | } 24 | 25 | @Knee fun getCurrentDay(): DayOfWeek = ... 26 | ``` 27 | 28 | Your JVM code can retrieve the current day with `getCurrentDay()` and receive a valid `DayOfWeek` instance back. 29 | If you wish to have a different JVM name, use the name parameter: 30 | 31 | ```kotlin 32 | // Kotlin/Native 33 | @KneeEnum(name = "WeekDay") enum class DayOfWeek { ... } 34 | 35 | // Kotlin/JVM 36 | val currentDay: WeekDay = getCurrentDay() 37 | ``` 38 | 39 | ## Importing enums 40 | 41 | If you wish to annotate existing enums that you don't control, for example those coming from a different module, 42 | note that you can use `@KneeEnum` on type aliases. For example: 43 | 44 | ```kotlin 45 | @KneeEnum typealias DeprecationLevel = kotlin.DeprecationLevel 46 | @KneeEnum typealias BufferOverflow = kotlinx.coroutines.channels.BufferOverflow 47 | ``` 48 | 49 | If the declaration is not found on the frontend, a clone will be generated, otherwise the existing declaration will be used. -------------------------------------------------------------------------------- /tests/test-misc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | targetSdk = 34 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation("androidx.test:runner:1.5.2") 27 | androidTestImplementation("androidx.test:rules:1.5.0") 28 | } 29 | 30 | knee { 31 | 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | 37 | // frontend 38 | androidTarget() 39 | 40 | // backend 41 | applyDefaultHierarchyTemplate { 42 | common { 43 | group("backend") { 44 | withAndroidNative() 45 | } 46 | } 47 | } 48 | 49 | androidNativeArm64() 50 | androidNativeX64() 51 | androidNativeArm32() 52 | androidNativeX86() 53 | } 54 | 55 | /** 56 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 57 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 58 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 59 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 60 | */ 61 | configurations.matching { 62 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 63 | }.all { 64 | isTransitive = true 65 | } -------------------------------------------------------------------------------- /tests/test-classes/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | targetSdk = 34 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation("androidx.test:runner:1.5.2") 27 | androidTestImplementation("androidx.test:rules:1.5.0") 28 | } 29 | 30 | knee { 31 | 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | 37 | applyDefaultHierarchyTemplate { 38 | common { 39 | group("backend") { 40 | withAndroidNative() 41 | } 42 | } 43 | } 44 | 45 | // frontend 46 | androidTarget() 47 | 48 | // backend 49 | androidNativeArm64() 50 | androidNativeX64() 51 | androidNativeArm32() 52 | androidNativeX86() 53 | } 54 | 55 | /** 56 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 57 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 58 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 59 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 60 | */ 61 | configurations.matching { 62 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 63 | }.all { 64 | isTransitive = true 65 | } -------------------------------------------------------------------------------- /tests/test-imports/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | targetSdk = 34 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation("androidx.test:runner:1.5.2") 27 | androidTestImplementation("androidx.test:rules:1.5.0") 28 | } 29 | 30 | knee { 31 | 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | 37 | // frontend 38 | androidTarget() 39 | 40 | // backend 41 | applyDefaultHierarchyTemplate { 42 | common { 43 | group("backend") { 44 | withAndroidNative() 45 | } 46 | } 47 | } 48 | 49 | androidNativeArm64() 50 | androidNativeX64() 51 | androidNativeArm32() 52 | androidNativeX86() 53 | } 54 | 55 | /** 56 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 57 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 58 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 59 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 60 | */ 61 | configurations.matching { 62 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 63 | }.all { 64 | isTransitive = true 65 | } -------------------------------------------------------------------------------- /tests/test-coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | targetSdk = 34 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation("androidx.test:runner:1.5.2") 27 | androidTestImplementation("androidx.test:rules:1.5.0") 28 | } 29 | 30 | knee { 31 | 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | 37 | // frontend 38 | androidTarget() 39 | 40 | // backend 41 | applyDefaultHierarchyTemplate { 42 | common { 43 | group("backend") { 44 | withAndroidNative() 45 | } 46 | } 47 | } 48 | 49 | androidNativeArm64() 50 | androidNativeX64() 51 | androidNativeArm32() 52 | androidNativeX86() 53 | } 54 | 55 | /** 56 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 57 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 58 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 59 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 60 | */ 61 | configurations.matching { 62 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 63 | }.all { 64 | isTransitive = true 65 | } -------------------------------------------------------------------------------- /tests/test-interfaces/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | targetSdk = 34 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation("androidx.test:runner:1.5.2") 27 | androidTestImplementation("androidx.test:rules:1.5.0") 28 | } 29 | 30 | knee { 31 | 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | 37 | // frontend 38 | androidTarget() 39 | 40 | // backend 41 | applyDefaultHierarchyTemplate { 42 | common { 43 | group("backend") { 44 | withAndroidNative() 45 | } 46 | } 47 | } 48 | 49 | androidNativeArm64() 50 | androidNativeX64() 51 | androidNativeArm32() 52 | androidNativeX86() 53 | } 54 | 55 | /** 56 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 57 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 58 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 59 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 60 | */ 61 | configurations.matching { 62 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 63 | }.all { 64 | isTransitive = true 65 | } -------------------------------------------------------------------------------- /experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.sample 2 | 3 | import androidx.activity.compose.setContent 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Surface 7 | import androidx.compose.material.lightColors 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | 11 | 12 | class NotesActivity : androidx.activity.ComponentActivity() { 13 | companion object { 14 | init { 15 | System.loadLibrary("compose_notes") 16 | } 17 | } 18 | 19 | override fun onCreate(savedInstanceState: android.os.Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | MaterialTheme(colors = lightColors()) { 23 | Surface { 24 | RootScreen(Modifier.fillMaxSize()) 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | sealed interface Destination { 32 | object List : Destination 33 | class Detail(val note: Note) : Destination 34 | object Editor : Destination 35 | } 36 | 37 | @Composable 38 | fun RootScreen(modifier: Modifier = Modifier) { 39 | val noteManager = remember { NoteManager() } 40 | /* DisposableEffect(Unit) { onDispose { noteManager.finalize() } } */ 41 | 42 | var currentDestination by remember { mutableStateOf(Destination.List) } 43 | when (val dest = currentDestination) { 44 | is Destination.List -> ListScreen(noteManager, modifier) { currentDestination = it } 45 | is Destination.Detail -> DetailScreen(noteManager, dest.note, modifier) { currentDestination = it } 46 | is Destination.Editor -> EditorScreen(noteManager, modifier) { currentDestination = it } 47 | } 48 | } -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Intro 3 | description: > 4 | Knee provides seamless two-way communication between Kotlin/Native and Kotlin/JVM. Supports suspend functions, classes, 5 | interfaces, no-copy buffers and much more. 6 | docs: 7 | - install 8 | - concepts 9 | - configure 10 | - initialize 11 | - features 12 | - utilities 13 | --- 14 | 15 | # Intro 16 | 17 | Knee is a Kotlin compiler plugin and companion runtime tools that provides seamless communication between Kotlin/Native 18 | binaries and Kotlin/JVM, using a thin and efficient layer around the JNI interface. 19 | 20 | With Knee, you can write idiomatic Kotlin/Native code, annotate it and then invoke it transparently from JVM 21 | as if they were running on the same environment. 22 | 23 | For a brief overview of Knee's capabilities and to see sample code, we recommend checking the [features](features) page 24 | where you'll learn about all supported features such as: 25 | 26 | - Ability to call [functions](features/callables#functions), get or set [properties](features/callables#properties) across JNI 27 | - [Suspend functions](features/suspend-functions) with two-way cancellation, holding structured concurrency together 28 | - [Exception support](features/exceptions), including custom exception types 29 | - Built-in serialization of [language primitives](features/builtin-types#primitives): numbers, strings, nullables, `Unit`, `Nothing` 30 | - Built-in serialization of [collection types](features/builtin-types#collections): lists, sets, efficient arrays 31 | - Serialization of [enums](features/enums), [classes](features/classes) and [objects](features/objects) 32 | - Serialization of [interfaces](features/interfaces) for two-way invocations 33 | - Lambdas and [generics](features/interfaces#importing-interfaces) support 34 | - [No-copy buffers](features/buffers), mapping `java.nio` buffers to `CPointer` on native 35 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeFeature.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenDeclaration 4 | import org.jetbrains.kotlin.ir.declarations.IrDeclaration 5 | import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName 6 | import org.jetbrains.kotlin.ir.util.* 7 | 8 | abstract class KneeFeature( 9 | val source: Ir, 10 | private val annotation: String, 11 | ) { 12 | 13 | var expectSources: List = emptyList() 14 | 15 | init { 16 | require(source.fileOrNull?.getPackageFragment()?.packageFqName?.isRoot != true) { 17 | "$this can't be in root package." 18 | } 19 | requireNotNull(source.fqNameWhenAvailable) { 20 | "$this must have a fully qualified name." 21 | } 22 | check(!source.isExpect) { 23 | "$this can't be an `expect` type." 24 | } 25 | } 26 | 27 | 28 | val irProducts = mutableListOf() 29 | val codegenProducts = mutableListOf>() 30 | 31 | fun dump(rawIr: Boolean = false): String { 32 | val hasProducts = irProducts.isNotEmpty() || codegenProducts.isNotEmpty() 33 | return if (!hasProducts) { 34 | if (rawIr) source.dump() else source.dumpKotlinLike() 35 | } else { 36 | val ir = irProducts.map { if (rawIr) it.dump() else it.dumpKotlinLike() } 37 | val codegen = codegenProducts.map { it.toString() } 38 | listOf("IR (${ir.size})", *ir.toTypedArray(), "CODEGEN (${codegen.size})", *codegen.toTypedArray()) 39 | .joinToString(separator = "\n") 40 | } 41 | } 42 | 43 | final override fun toString() = "@$annotation " + (source.fqNameWhenAvailable?.asString() ?: source.name.asString()) 44 | } -------------------------------------------------------------------------------- /knee-annotations/src/backendMain/kotlin/Knee.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.annotations 4 | 5 | 6 | @Target( 7 | AnnotationTarget.FUNCTION, 8 | AnnotationTarget.CONSTRUCTOR, 9 | /** 10 | * Allows annotating a property with: 11 | * @property:Knee 12 | * val prop: Int = 42 13 | */ 14 | AnnotationTarget.PROPERTY, 15 | /** 16 | * Allows annotating a property with: 17 | * @Knee 18 | * val prop: Int = 42 19 | * Like [AnnotationTarget.PROPERTY], the declaration can be found during visitIrProperty 20 | * so apparently we don't need special logic for this case. 21 | */ 22 | AnnotationTarget.FIELD) 23 | @Retention(AnnotationRetention.BINARY) 24 | annotation class Knee 25 | 26 | /** 27 | * This annotation is used internally only. 28 | */ 29 | @Retention(AnnotationRetention.BINARY) 30 | annotation class KneeMetadata(val metadata: String) 31 | 32 | @Target( 33 | AnnotationTarget.CLASS, 34 | AnnotationTarget.TYPEALIAS 35 | ) 36 | @Retention(AnnotationRetention.BINARY) 37 | annotation class KneeEnum(val name: String = "") 38 | 39 | @Target( 40 | AnnotationTarget.CLASS, 41 | AnnotationTarget.TYPEALIAS 42 | ) 43 | @Retention(AnnotationRetention.BINARY) 44 | annotation class KneeClass(val name: String = "") 45 | 46 | @Target( 47 | AnnotationTarget.CLASS, 48 | AnnotationTarget.TYPEALIAS 49 | ) 50 | @Retention(AnnotationRetention.BINARY) 51 | annotation class KneeObject(val name: String = "") 52 | 53 | @Target( 54 | AnnotationTarget.CLASS, 55 | AnnotationTarget.TYPEALIAS 56 | ) 57 | @Retention(AnnotationRetention.BINARY) 58 | annotation class KneeInterface(val name: String = "") 59 | 60 | @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) 61 | @Retention(AnnotationRetention.BINARY) 62 | annotation class KneeRaw(val name: String) 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/features/objects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Objects 3 | description: > 4 | Understand how Knee compiler plugin can serialize declared objects and let you pass them from Kotlin Native 5 | to the JVM and vice versa, including support for externally defined objects. 6 | --- 7 | 8 | # Objects 9 | 10 | ## Annotating objects 11 | 12 | Whenever you declare an object, you can use the `@KneeObject` annotation to tell the compiler that it should be processed. 13 | Knee supports objects in different scenarios: 14 | 15 | - top level objects 16 | - objects nested inside another declaration 17 | - `companion` objects 18 | 19 | ```kotlin 20 | @KneeObject object Foo { 21 | ... 22 | } 23 | 24 | class Utilities { 25 | @KneeObject object Bar { ... } 26 | @KneeObject companion object { ... } 27 | } 28 | ``` 29 | 30 | Under the hood, objects are *not* actually serialized and passed through the JNI interface: since there can only be a single 31 | instance of an object, no extra information is needed and the compiler can retrieve the object field statically on both 32 | platforms. 33 | 34 | ## Annotating members 35 | 36 | All callable members (functions, properties, constructors) of an object can be made available to the JVM side, but 37 | they must be explicitly marked with the `@Knee` annotation as described in the [callables](callables) documentation. 38 | 39 | ```kotlin 40 | @KneeObject object Game { 41 | @Knee fun start() { ... } 42 | fun loop() { ... } 43 | } 44 | ``` 45 | 46 | In the example above, only the `start` function will be available on the JVM side. 47 | 48 | ## Importing objects 49 | 50 | If you wish to annotate existing objects that you don't control, for example those coming from a different module, 51 | you can technically use `@KneeObject` on type aliases. Unfortunately as of now, this functionality is very limited in that you 52 | can't choose which declarations will be imported. -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/import/ImportInfo.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.import 2 | 3 | import com.squareup.kotlinpoet.TypeVariableName 4 | import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeName 5 | import io.deepmedia.tools.knee.plugin.compiler.utils.simple 6 | import org.jetbrains.kotlin.backend.common.lower.parents 7 | import org.jetbrains.kotlin.ir.IrBuiltIns 8 | import org.jetbrains.kotlin.ir.builders.declarations.buildClass 9 | import org.jetbrains.kotlin.ir.declarations.* 10 | import org.jetbrains.kotlin.ir.types.* 11 | import org.jetbrains.kotlin.ir.util.* 12 | 13 | class ImportInfo( 14 | val type: IrSimpleType, 15 | private val declaration: IrDeclarationWithName, 16 | ) { 17 | val id: String get() = declaration.name.asString() 18 | 19 | // this is writable! 20 | val file: IrFile get() = declaration.file 21 | 22 | private val typeParameters = type.classOrNull!!.owner.typeParameters.map { it.symbol } 23 | private val typeArguments = type.arguments 24 | 25 | val typeVariables = typeParameters.map { 26 | // type = kotlin.ranges.ClosedRange 27 | // declaration.name = closedFloatRange 28 | // type parameter = T 29 | // super type = kotlin.Comparable 30 | TypeVariableName( 31 | name = it.owner.name.asString(), 32 | bounds = it.owner.superTypes.map { 33 | it.simple("ImportInfo.typeParameters.map").asTypeName() 34 | } 35 | ) 36 | } 37 | 38 | val substitutor = IrTypeSubstitutor( 39 | typeParameters = typeParameters, 40 | typeArguments = typeArguments, 41 | allowEmptySubstitution = false 42 | ) 43 | 44 | // Does the same that substitutor, to be used in function.copyValueParametersFrom... 45 | val substitutionMap = typeParameters.zip(typeArguments.map { it.typeOrNull!! }).toMap() 46 | } 47 | -------------------------------------------------------------------------------- /tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerClassTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | 5 | class InnerClassTests { 6 | 7 | companion object { 8 | init { 9 | System.loadLibrary("test_classes") 10 | } 11 | } 12 | 13 | 14 | @Test 15 | fun testProperty() { 16 | val item = Outer.InnerCounter() 17 | currentInnerCounter = item 18 | check(item == currentInnerCounter) 19 | check(item !== currentInnerCounter) 20 | } 21 | 22 | @Test 23 | fun testEquals() { 24 | // Counter implements equals based on the value 25 | val first = Outer.InnerCounter(initialValue = 50u) 26 | val second = Outer.InnerCounter(initialValue = 100u) 27 | val third = Outer.InnerCounter(initialValue = 100u) 28 | check(first != second) 29 | check(second == third) 30 | } 31 | 32 | @Test 33 | fun testString() { 34 | val item = Outer.InnerCounter(initialValue = 0u) 35 | check(item.toString() == "Outer.InnerCounter(0)") 36 | item.increment() 37 | check(item.toString() == "Outer.InnerCounter(1)") 38 | } 39 | 40 | @Test 41 | fun testHashCode() { 42 | val item = Outer.InnerCounter(initialValue = 30u) 43 | check(30 == item.hashCode()) 44 | } 45 | 46 | @Test 47 | fun testSameNativeObject() { 48 | val item = Outer.InnerCounter() 49 | currentInnerCounter = item 50 | check(isCurrentInnerCounter(item, checkIdentity = false)) 51 | check(isCurrentInnerCounter(item, checkIdentity = true)) 52 | } 53 | 54 | @Test 55 | fun testMutability() { 56 | currentInnerCounter = Outer.InnerCounter(10u) 57 | currentInnerCounter!!.increment() 58 | check(currentInnerCounter!!.get() == 11u) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenProperty 4 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 5 | import org.jetbrains.kotlin.ir.declarations.IrProperty 6 | import org.jetbrains.kotlin.ir.util.copyAnnotationsFrom 7 | 8 | class KneeDownwardProperty( 9 | source: IrProperty, 10 | parentInstance: KneeFeature<*>? 11 | ) : KneeFeature(source, "Knee") { 12 | 13 | sealed class Kind { 14 | class InterfaceMember(val owner: KneeInterface) : Kind() 15 | class ClassMember(val owner: KneeClass) : Kind() 16 | class ObjectMember(val owner: KneeObject) : Kind() 17 | object TopLevel : Kind() 18 | 19 | val importInfo: ImportInfo? get() = when (this) { 20 | TopLevel -> null 21 | is ClassMember -> owner.importInfo 22 | is ObjectMember -> owner.importInfo 23 | is InterfaceMember -> owner.importInfo 24 | } 25 | } 26 | 27 | val kind = when (parentInstance) { 28 | is KneeInterface -> Kind.InterfaceMember(parentInstance) 29 | is KneeClass -> Kind.ClassMember(parentInstance) 30 | is KneeObject -> Kind.ObjectMember(parentInstance) 31 | null -> Kind.TopLevel 32 | else -> error("Unsupported parent instance: $parentInstance") 33 | } 34 | 35 | val setter: KneeDownwardFunction? = source.setter?.let { 36 | it.copyAnnotationsFrom(source) 37 | KneeDownwardFunction(it, parentInstance, this) 38 | } 39 | 40 | val getter: KneeDownwardFunction = requireNotNull(source.getter) { "$this must have a getter." }.let { 41 | it.copyAnnotationsFrom(source) 42 | KneeDownwardFunction(it, parentInstance, this) 43 | } 44 | 45 | lateinit var codegenImplementation: CodegenProperty 46 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeUpwardFunction.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.functions.UpwardFunctionSignature 4 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 5 | import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex 6 | import org.jetbrains.kotlin.descriptors.ClassKind 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction 9 | import org.jetbrains.kotlin.ir.util.isOverridable 10 | import org.jetbrains.kotlin.ir.util.parentAsClass 11 | 12 | /** 13 | * source is an abstract function belonging to some interface. 14 | * The implementation will be added to some "Impl" class. 15 | */ 16 | class KneeUpwardFunction( 17 | source: IrSimpleFunction, 18 | parentInterface: KneeInterface?, 19 | ) : KneeFeature(source, "Knee⬆") { 20 | 21 | /** 22 | * Read [UpwardFunctionSignature] for more info. 23 | */ 24 | sealed class Kind { 25 | class InterfaceMember(val parent: KneeInterface) : Kind() 26 | 27 | val importInfo: ImportInfo? get() = when (this) { 28 | is InterfaceMember -> parent.importInfo 29 | } 30 | } 31 | 32 | val kind: Kind = Kind.InterfaceMember(parentInterface!!) 33 | 34 | init { 35 | source.requireNotComplex(this, allowSuspend = true) 36 | require(source.isOverridable) { "$this is not overridable." } 37 | require(source.parent is IrClass && source.parentAsClass.kind == ClassKind.INTERFACE) { 38 | "$this is not member of an interface." 39 | } 40 | } 41 | 42 | /** 43 | * The generated implementation. Might be set beforehand for example by reverse property handling. 44 | * If present, we shouldn't generate a new one of course. 45 | */ 46 | var implementation: IrSimpleFunction? = null 47 | } 48 | -------------------------------------------------------------------------------- /knee-compiler-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | kotlin("plugin.serialization") 4 | id("io.deepmedia.tools.deployer") 5 | id("com.github.johnrengelman.shadow") version "8.1.1" 6 | } 7 | 8 | dependencies { 9 | compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") 10 | implementation("com.squareup:kotlinpoet:1.18.1") 11 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") 12 | } 13 | 14 | kotlin { 15 | target { 16 | compilerOptions { 17 | optIn.add("org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI") 18 | } 19 | } 20 | } 21 | 22 | // Annoying configuration needed because of https://youtrack.jetbrains.com/issue/KT-53477/ 23 | // Compiler plugins can't have dependency in Native, unless we use a fat jar. 24 | tasks.shadowJar.configure { 25 | // Remove the -all suffix, otherwise the plugin jar is not picked up 26 | // (very important. it won't throw an error either, just won't apply) 27 | archiveClassifier.set("") 28 | // But also change the destination directory (normally: build/libs), otherwise our output jar 29 | // will overwrite the `jar` task output (which has no classifier), and when two tasks have the same 30 | // outputs, Gradle can go crazy. Example: 31 | // 32 | // Task ':knee-compiler-plugin:signArtifacts0ForLocalPublication' uses this output of task 33 | // ':knee-compiler-plugin:jar' without declaring an explicit or implicit dependency. 34 | // This can lead to incorrect results being produced, depending on what order the tasks are executed. 35 | destinationDirectory.set(layout.buildDirectory.get().dir("libs").dir("shadow")) 36 | } 37 | 38 | deployer { 39 | content { 40 | component { 41 | fromArtifactSet { 42 | artifact(tasks.shadowJar) 43 | } 44 | kotlinSources() 45 | emptyDocs() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/.idea/runConfigurations/test_interfaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | -------------------------------------------------------------------------------- /docs/features/suspend-functions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Suspend functions 3 | description: > 4 | Understand how Knee can provide structured concurrency across the JNI boundary thanks to Kotlin suspend functions, 5 | from Kotlin Native to JVM and vice versa. 6 | --- 7 | 8 | # Suspend Functions 9 | 10 | ## Declaration 11 | 12 | All [functions](callables#functions) that support the `@Knee` annotation can also be marked as `suspend`. 13 | The developer UX is exactly the same: 14 | 15 | ```kotlin 16 | // Kotlin/Native 17 | @Knee suspend fun computeNumber(): Int = coroutineScope { 18 | val num1 = async { loadFirstNumber() } 19 | val num2 = async { loadSecondNumber() } 20 | num1.await() + num2.await() 21 | } 22 | 23 | // Kotlin/JVM 24 | scope.launch { 25 | val number = computeNumber() 26 | println("Found number: $number") 27 | } 28 | ``` 29 | 30 | ## Structured concurrency 31 | 32 | The underlying implementation is very complex in order to support two-way cancellation and error propagation. 33 | In the example above: 34 | 35 | - If the JVM `scope` is cancelled, the native coroutines are also cancelled 36 | - If the native coroutines are cancelled, `computeNumber` throws a `CancellationException` 37 | - Errors are propagated up/down and the exception type, if possible, is [preserved](exceptions) 38 | 39 | In short, calling a `@Knee` suspend function is no different than calling a local suspend function 40 | and you can expect the same level of support. In particular Knee preserves the hierarchy of coroutines 41 | and keeps them connected across the JNI bridge. 42 | 43 | ##### Context elements 44 | 45 | Knee does no attempt at preserving the `CoroutineContext`. All context element, most notably the `CoroutineDispatcher`, 46 | will be lost when the JNI bridge is crossed: 47 | 48 | - `@Knee` suspend functions called from K/JVM are invoked on `Dispatchers.Unconfined` on the native backend 49 | - `@Knee` suspend functions called from K/N are invoked on `Dispatchers.Unconfined` on the JVM frontend -------------------------------------------------------------------------------- /experiments/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 41 | 42 | -------------------------------------------------------------------------------- /experiments/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/compiler/BoxMethods.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.deepmedia.tools.knee.runtime.compiler 4 | 5 | import io.deepmedia.tools.knee.runtime.* 6 | import platform.android.* 7 | 8 | 9 | internal fun initBoxMethods(environment: JniEnvironment) = with(BoxMethods) { 10 | longConstructor = MethodIds.get(environment, "java.lang.Long", "", "(J)V", false) 11 | longValue = MethodIds.get(environment, "java.lang.Long", "longValue", "()J", false) 12 | intConstructor = MethodIds.get(environment, "java.lang.Integer", "", "(I)V", false) 13 | intValue = MethodIds.get(environment, "java.lang.Integer", "intValue", "()I", false) 14 | byteConstructor = MethodIds.get(environment, "java.lang.Byte", "", "(B)V", false) 15 | byteValue = MethodIds.get(environment, "java.lang.Byte", "byteValue", "()B", false) 16 | boolConstructor = MethodIds.get(environment, "java.lang.Boolean", "", "(Z)V", false) 17 | boolValue = MethodIds.get(environment, "java.lang.Boolean", "booleanValue", "()Z", false) 18 | doubleConstructor = MethodIds.get(environment, "java.lang.Double", "", "(D)V", false) 19 | doubleValue = MethodIds.get(environment, "java.lang.Double", "doubleValue", "()D", false) 20 | floatConstructor = MethodIds.get(environment, "java.lang.Float", "", "(F)V", false) 21 | floatValue = MethodIds.get(environment, "java.lang.Float", "floatValue", "()F", false) 22 | } 23 | 24 | @PublishedApi 25 | internal object BoxMethods { 26 | lateinit var longConstructor: jmethodID 27 | lateinit var longValue: jmethodID 28 | lateinit var intConstructor: jmethodID 29 | lateinit var intValue: jmethodID 30 | lateinit var byteConstructor: jmethodID 31 | lateinit var byteValue: jmethodID 32 | lateinit var boolConstructor: jmethodID 33 | lateinit var boolValue: jmethodID 34 | lateinit var doubleConstructor: jmethodID 35 | lateinit var doubleValue: jmethodID 36 | lateinit var floatConstructor: jmethodID 37 | lateinit var floatValue: jmethodID 38 | } 39 | -------------------------------------------------------------------------------- /tests/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 42 | 43 | -------------------------------------------------------------------------------- /tests/test-primitives/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | id("com.android.application") version "8.1.1" 6 | id("io.deepmedia.tools.knee") 7 | } 8 | 9 | configurations.configureEach { 10 | resolutionStrategy { 11 | cacheChangingModulesFor(0, "seconds") 12 | } 13 | } 14 | 15 | android { 16 | namespace = "io.deepmedia.tools.knee.tests" 17 | compileSdk = 34 18 | defaultConfig { 19 | minSdk = 26 20 | //noinspection EditedTargetSdkVersion 21 | targetSdk = 34 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | /* sourceSets { 25 | getByName("debug").jniLibs { 26 | this.srcDir(layout.buildDirectory.get().dir("knee").dir("bin").dir("debug")) 27 | } 28 | } */ 29 | } 30 | 31 | dependencies { 32 | androidTestImplementation("androidx.test:runner:1.5.2") 33 | androidTestImplementation("androidx.test:rules:1.5.0") 34 | } 35 | 36 | knee { 37 | 38 | } 39 | 40 | kotlin { 41 | jvmToolchain(11) 42 | 43 | // frontend 44 | androidTarget() 45 | 46 | // backend 47 | applyDefaultHierarchyTemplate { 48 | common { 49 | group("backend") { 50 | withAndroidNative() 51 | } 52 | } 53 | } 54 | 55 | androidNativeArm64() 56 | androidNativeX64() 57 | androidNativeArm32() 58 | androidNativeX86() 59 | } 60 | 61 | /** 62 | * This is to make included parent build work. Kotlin Compiler Plugins have a configuration 63 | * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin 64 | * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds 65 | * read from the "default" configuration and there doesn't seem to be a clean way to solve this. 66 | */ 67 | configurations.matching { 68 | it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") 69 | }.all { 70 | isTransitive = true 71 | } -------------------------------------------------------------------------------- /tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ClassTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.SharedFlow 5 | import org.junit.Test 6 | 7 | // TODO: test garbage collection 8 | class ClassTests { 9 | 10 | companion object { 11 | init { 12 | System.loadLibrary("test_classes") 13 | } 14 | } 15 | 16 | @Test 17 | fun testProperty() { 18 | val item = Counter() 19 | currentCounter = item 20 | check(item == currentCounter) 21 | check(item !== currentCounter) 22 | } 23 | 24 | @Test 25 | fun testEquals() { 26 | // Counter implements equals based on the value 27 | val first = Counter(initialValue = 50u) 28 | val second = Counter(initialValue = 100u) 29 | val third = Counter(initialValue = 100u) 30 | check(first != second) 31 | check(second == third) 32 | } 33 | 34 | @Test 35 | fun testString() { 36 | val item = Counter(initialValue = 0u) 37 | check(item.toString() == "Counter(0)") 38 | item.increment() 39 | check(item.toString() == "Counter(1)") 40 | } 41 | 42 | @Test 43 | fun testHashCode() { 44 | val item = Counter(initialValue = 30u) 45 | check(30 == item.hashCode()) 46 | } 47 | 48 | @Test 49 | fun testSameNativeObject() { 50 | val item = Counter() 51 | currentCounter = item 52 | check(isCurrentCounter(item, checkIdentity = false)) 53 | check(isCurrentCounter(item, checkIdentity = true)) 54 | } 55 | 56 | @Test 57 | fun testArray() { 58 | val array = arrayOfCounters(4) 59 | check(array.size == 4) 60 | } 61 | 62 | @Test 63 | fun testMutability() { 64 | currentCounter = Counter(10u) 65 | currentCounter!!.increment() 66 | check(currentCounter!!.get() == 11u) 67 | } 68 | 69 | @Test 70 | fun testFlip() { 71 | val item = Counter() 72 | check(item.flip(false)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeInitializer.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext 4 | import org.jetbrains.kotlin.ir.expressions.IrCall 5 | import org.jetbrains.kotlin.ir.expressions.IrExpression 6 | import org.jetbrains.kotlin.ir.types.IrType 7 | import org.jetbrains.kotlin.ir.util.dumpKotlinLike 8 | 9 | /* class KneeInit(source: IrSimpleFunction) : KneeFeature(source, "KneeInit") { 10 | 11 | init { 12 | source.requireNotComplex(this) 13 | require(source.isTopLevel) { "$this must be a top level function." } 14 | } 15 | } 16 | 17 | fun KneeInit.validate(context: KneeContext) { 18 | val returnType = source.returnType 19 | val expectedReturnType = context.symbols.builtIns.unitType 20 | require(returnType == expectedReturnType) { 21 | "$this must return ${expectedReturnType.dumpKotlinLike()} (not ${returnType.dumpKotlinLike()})" 22 | } 23 | val arg0 = source.valueParameters.firstOrNull()?.type 24 | val expectedArg0 = context.symbols.jniEnvironmentType 25 | require(arg0 == expectedArg0) { 26 | "$this first parameter must be ${(expectedArg0 as IrType).dumpKotlinLike()} (not ${arg0?.dumpKotlinLike()})" 27 | } 28 | } 29 | 30 | 31 | private fun DeclarationIrBuilder.irInitLambdas(context: KneeContext, inits: List): IrExpression { 32 | val symbols = context.symbols 33 | val type = symbols.klass(functionXInterface(1)).typeWith(symbols.jniEnvironmentType, symbols.builtIns.unitType) 34 | return irListOf(symbols, type, inits.map { init -> 35 | irLambda( 36 | context = context, 37 | parent = this@irInitLambdas.parent, 38 | valueParameters = listOf(symbols.jniEnvironmentType), 39 | returnType = symbols.builtIns.unitType, 40 | content = { lambda -> 41 | val environment = irGet(lambda.valueParameters[0]) 42 | +irCall(init.source).apply { putValueArgument(0, environment) } 43 | } 44 | ) 45 | }) 46 | }*/ 47 | 48 | 49 | class KneeInitializer(val expression: IrCall) -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/jni/JniSignature.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.jni 2 | 3 | object JniSignature { 4 | 5 | fun get(type: JniType): String = buildString { 6 | appendJniType(type, isReturnType = true) 7 | } 8 | 9 | fun get(returnType: JniType, argumentTypes: List): String = buildString { 10 | append('(') 11 | argumentTypes.forEach { 12 | appendJniType(it, isReturnType = false) 13 | } 14 | append(')') 15 | appendJniType(returnType, isReturnType = true) 16 | } 17 | 18 | /** 19 | * Table 3-2 Java VM Type Signatures 20 | * Z = boolean 21 | * B = byte 22 | * C = char 23 | * S = short 24 | * I = int 25 | * J = long 26 | * F = float 27 | * D = double 28 | * L; = class 29 | * [ = array type 30 | * () = method 31 | */ 32 | private fun StringBuilder.appendJniType(type: JniType, isReturnType: Boolean) { 33 | when (type) { 34 | is JniType.Void -> { 35 | require(isReturnType) { "JniType.Void is not allowed here." } 36 | append('V') 37 | } 38 | 39 | // PRIMITIVE TYPES 40 | is JniType.BooleanAsUByte -> append('Z') 41 | is JniType.Byte -> append('B') 42 | // builtIns.charType -> append('C') 43 | // builtIns.shortType -> append('S') 44 | is JniType.Int -> append('I') 45 | is JniType.Long -> append('J') 46 | is JniType.Float -> append('F') 47 | is JniType.Double -> append('D') 48 | 49 | // OBJECT TYPES 50 | is JniType.Object -> when (val arrayElement = type.arrayElement) { 51 | null -> { 52 | append('L') 53 | append(type.jvm.jvmClassName) // cares about dollar signs 54 | append(';') 55 | } 56 | else -> { 57 | append("[") 58 | appendJniType(arrayElement, isReturnType) 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/EnumTests.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import org.junit.Test 4 | import java.lang.System.identityHashCode 5 | import kotlin.math.abs 6 | 7 | class EnumTests { 8 | 9 | companion object { 10 | init { 11 | System.loadLibrary("test_misc") 12 | } 13 | } 14 | 15 | @Test 16 | fun testProperty_topLevel() { 17 | check(currentDay == null) 18 | currentDay = Day.Monday 19 | check(currentDay == Day.Monday) 20 | } 21 | 22 | @Test 23 | fun testEncodeDecode_topLevel() { 24 | check(getDayAfter(Day.Monday) == Day.Tuesday) 25 | check(getDayAfter(Day.Sunday) == Day.Monday) 26 | } 27 | 28 | @Test 29 | fun testList_topLevel() { 30 | val all = getAllDays() 31 | check(all.contains(Day.Monday)) 32 | check(all.size == 7) 33 | } 34 | 35 | @Test 36 | fun testProperty_insideClass() { 37 | check(currentInnerDay1 == null) 38 | currentInnerDay1 = OuterClass.InnerDay.Friday 39 | check(currentInnerDay1 == OuterClass.InnerDay.Friday) 40 | } 41 | 42 | @Test 43 | fun testEncodeDecode_insideClass() { 44 | check(getInnerDay1After(OuterClass.InnerDay.Saturday) == OuterClass.InnerDay.Sunday) 45 | check(getInnerDay1After(OuterClass.InnerDay.Monday) == OuterClass.InnerDay.Tuesday) 46 | check(getInnerDay1After(OuterClass.InnerDay.Sunday) == OuterClass.InnerDay.Monday) 47 | } 48 | 49 | @Test 50 | fun testProperty_insideInterface() { 51 | check(currentInnerDay2 == null) 52 | currentInnerDay2 = OuterInterface.InnerDay.Wednesday 53 | check(currentInnerDay2 == OuterInterface.InnerDay.Wednesday) 54 | } 55 | 56 | @Test 57 | fun testEncodeDecode_insideInterface() { 58 | check(getInnerDay2After(OuterInterface.InnerDay.Saturday) == OuterInterface.InnerDay.Sunday) 59 | check(getInnerDay2After(OuterInterface.InnerDay.Monday) == OuterInterface.InnerDay.Tuesday) 60 | check(getInnerDay2After(OuterInterface.InnerDay.Sunday) == OuterInterface.InnerDay.Monday) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/test-imports/src/backendMain/kotlin/LambdaDefinitions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlinx.coroutines.flow.* 6 | import kotlinx.coroutines.* 7 | 8 | 9 | @KneeInterface typealias SimpleLambda = () -> Unit 10 | @KneeInterface typealias ComplexLambda = (String, Long) -> String 11 | // The point of ComplexLambda2 is to test two identical lambdas with different generics 12 | @KneeInterface typealias ComplexLambda2 = (Int, UInt) -> String 13 | 14 | @KneeInterface typealias SimpleSuspendLambda = suspend () -> Unit 15 | @KneeInterface typealias ComplexSuspendLambda = suspend (String, Int) -> ULong 16 | 17 | @Knee lateinit var currentSimpleLambda: () -> Unit 18 | @Knee fun makeSimpleLambda(): () -> Unit = { } 19 | @Knee fun invokeSimpleLambda(lambda: () -> Unit) { lambda.invoke() } 20 | 21 | @Knee lateinit var currentComplexLambda: (String, Long) -> String 22 | @Knee fun makeComplexLambda(): (String, Long) -> String = { a, b -> a + b } 23 | @Knee fun invokeComplexLambda(lambda: (String, Long) -> String, arg0: String, arg1: Long): String { 24 | val result = lambda.invoke(arg0, arg1) 25 | return result 26 | } 27 | 28 | @Knee lateinit var currentComplexLambda2: (Int, UInt) -> String 29 | @Knee fun makeComplexLambda2(): (Int, UInt) -> String = { a, b -> (a + b.toInt()).toString() } 30 | @Knee fun invokeComplexLambda2(lambda: (Int, UInt) -> String, arg0: Int, arg1: UInt): String { 31 | return lambda.invoke(arg0, arg1) 32 | } 33 | 34 | @Knee lateinit var currentSimpleSuspendLambda: suspend () -> Unit 35 | @Knee fun makeSimpleSuspendLambda(): suspend () -> Unit = { } 36 | @Knee suspend fun invokeSimpleSuspendLambda(lambda: suspend () -> Unit) { 37 | kotlinx.coroutines.delay(500) 38 | lambda.invoke() 39 | } 40 | 41 | @Knee lateinit var currentSuspendComplexLambda: suspend (String, Int) -> ULong 42 | @Knee fun makeSuspendComplexLambda(): suspend (String, Int) -> ULong = { a, b -> (a.length + b).toULong() } 43 | @Knee suspend fun invokeSuspendComplexLambda(lambda: suspend (String, Int) -> ULong, arg0: String, arg1: Int): ULong { 44 | kotlinx.coroutines.delay(500) 45 | return lambda.invoke(arg0, arg1) 46 | } 47 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeObject.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass 4 | import io.deepmedia.tools.knee.plugin.compiler.features.KneeClass.Companion.hasAnnotationCopyingFromParents 5 | import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo 6 | import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds 7 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 8 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds 9 | import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex 10 | import org.jetbrains.kotlin.descriptors.ClassKind 11 | import org.jetbrains.kotlin.ir.declarations.* 12 | import org.jetbrains.kotlin.ir.util.* 13 | import org.jetbrains.kotlin.name.FqName 14 | 15 | class KneeObject( 16 | source: IrClass, 17 | val importInfo: ImportInfo? = null, 18 | ) : KneeFeature(source, "KneeObject") { 19 | 20 | val members: List 21 | val properties: List 22 | 23 | init { 24 | source.requireNotComplex( 25 | this, ClassKind.OBJECT, 26 | typeArguments = importInfo?.type?.arguments ?: emptyList() 27 | ) 28 | 29 | val members = source.functions 30 | .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } 31 | // exclude static function (see isStaticMethodOfClass impl) 32 | // and property accessors (one should use @Knee on the property instead) 33 | .filter { it.dispatchReceiverParameter != null } 34 | .filter { !it.isPropertyAccessor } 35 | .onEach { it.requireNotComplex("$this member ${it.name}", allowSuspend = true) } 36 | .toList() 37 | 38 | val properties = source.properties 39 | .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } 40 | .toList() 41 | 42 | this.members = members.map { KneeDownwardFunction(it, parentInstance = this, parentProperty = null) } 43 | this.properties = properties.map { KneeDownwardProperty(it, parentInstance = this) } 44 | } 45 | 46 | lateinit var codegenClone: CodegenClass 47 | } 48 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/features/KneeImport.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.features 2 | 3 | /* class KneeImport(source: IrClass) : KneeFeature(source, "KneeImport") { 4 | 5 | val interfaces: List 6 | val enums: List 7 | val classes: List 8 | 9 | init { 10 | val interfaces = mutableListOf() 11 | val enums = mutableListOf() 12 | val classes = mutableListOf() 13 | source.requireNotComplex(this, ClassKind.INTERFACE) 14 | source.acceptChildrenVoid(object : IrElementVisitorVoid { 15 | override fun visitElement(element: IrElement) = Unit 16 | 17 | override fun visitProperty(declaration: IrProperty) { 18 | if (declaration.hasAnnotation(kneeInterfaceAnnotation)) { 19 | val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType 20 | val info = ImportInfo(type, declaration) 21 | interfaces.add(KneeInterface( 22 | source = type.classOrNull!!.owner, 23 | importInfo = info 24 | )) 25 | } else if (declaration.hasAnnotation(kneeEnumAnnotation)) { 26 | val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType 27 | val info = ImportInfo(type, declaration) 28 | enums.add(KneeEnum( 29 | source = type.classOrNull!!.owner, 30 | importInfo = info 31 | )) 32 | }else if (declaration.hasAnnotation(kneeClassAnnotation)) { 33 | val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType 34 | val info = ImportInfo(type, declaration) 35 | classes.add(KneeClass( 36 | source = type.classOrNull!!.owner, 37 | importInfo = info 38 | )) 39 | } 40 | } 41 | }) 42 | this.interfaces = interfaces 43 | this.enums = enums 44 | this.classes = classes 45 | } 46 | } */ 47 | -------------------------------------------------------------------------------- /tests/test-classes/src/backendMain/kotlin/ClassDefinintions.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.tests 2 | 3 | import io.deepmedia.tools.knee.annotations.* 4 | import io.deepmedia.tools.knee.runtime.* 5 | import kotlin.random.Random 6 | import kotlin.random.nextUInt 7 | 8 | @KneeClass 9 | class Counter @Knee constructor(initialValue: UInt) { 10 | var value: UInt = initialValue 11 | 12 | @Knee constructor() : this(initialValue = Random.nextUInt()) 13 | @Knee fun increment() { value += 1u } 14 | @Knee fun decrement() { value -= 1u } 15 | @Knee fun add(delta: UInt) { value += delta } 16 | @Knee fun get(): UInt = value 17 | @Knee fun flip(bool: Boolean): Boolean = !bool 18 | override fun toString(): String = "Counter($value)" 19 | override fun hashCode(): Int = value.toInt() 20 | override fun equals(other: Any?): Boolean { 21 | return other is Counter && other.value == value 22 | } 23 | } 24 | 25 | @Knee var currentCounter: Counter? = null 26 | 27 | @Knee fun isCurrentCounter(counter: Counter, checkIdentity: Boolean): Boolean { 28 | if (checkIdentity) return counter === currentCounter 29 | return counter == currentCounter 30 | } 31 | 32 | @Knee fun arrayOfCounters(size: Int): Array { 33 | return Array(size) { Counter() } 34 | } 35 | 36 | interface Outer { 37 | 38 | @KneeClass 39 | class InnerCounter @Knee constructor(initialValue: UInt) { 40 | var value: UInt = initialValue 41 | 42 | @Knee constructor() : this(initialValue = Random.nextUInt()) 43 | @Knee fun increment() { value += 1u } 44 | @Knee fun decrement() { value -= 1u } 45 | @Knee fun add(delta: UInt) { value += delta } 46 | @Knee fun get(): UInt = value 47 | override fun toString(): String = "Outer.InnerCounter($value)" 48 | override fun hashCode(): Int = value.toInt() 49 | override fun equals(other: Any?): Boolean { 50 | return other is InnerCounter && other.value == value 51 | } 52 | } 53 | } 54 | 55 | @Knee var currentInnerCounter: Outer.InnerCounter? = null 56 | 57 | @Knee fun isCurrentInnerCounter(counter: Outer.InnerCounter, checkIdentity: Boolean): Boolean { 58 | if (checkIdentity) return counter === currentInnerCounter 59 | return counter == currentInnerCounter 60 | } -------------------------------------------------------------------------------- /docs/concepts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concepts 3 | description: Discover Knee's main concepts - the motivation behind the plugin and notes about its architecture and design. 4 | --- 5 | 6 | # Concepts 7 | 8 | ## Motivation 9 | 10 | Native and JVM binaries have historically communicated through the Java Native Interface, which is a bridge across two 11 | different environments and runtimes. This imposes strict restrictions about what kind of data can be passed through 12 | the interface and how, together with the need to write very tedious boilerplate communication code on both platforms. 13 | 14 | Additionally, when using Kotlin/Native, the developer is required to deal with verbose, low-level `kotlinx.cinterop` types 15 | in order to pass and receive JNI data. But Kotlin also creates an opportunity to improve communication by using the same language on the two ends of 16 | the bridge. 17 | 18 | Our aim with Knee is to leverage this fact and, with the power of Kotlin compiler plugins, provide a transparent, seamless 19 | interface between the two environments so that all the conversion boilerplate - whenever is needed - is hidden from the 20 | developer. 21 | 22 | ## Design 23 | 24 | > **Note**: Support is currently limited to Android Native targets, where jni.h is imported by default. 25 | > Adding other platforms should be straightforward though, and we welcome contributions on this. 26 | 27 | In source code and documentation, you may see the following terms representing the two ends of the bridge: 28 | - **backend** refers to the Kotlin/Native side (code, environment, binaries) 29 | - **frontend** refers to the Kotlin/JVM side (code, environment, binaries) 30 | 31 | Knee is designed around the use case where the vast majority of the logic lives in the backend module, 32 | and the frontend is just a very thin wrapper around it. This way, developers can just **write once in the backend**. 33 | 34 | This is done via a compiler plugin and a companion runtime library, that together: 35 | 36 | - Analyze backend code, transforming it where needed and generating glue code and JNI boilerplate code 37 | - Generate frontend source code as `.kt` files, including all the declarations that are supposed to pass through the bridge 38 | - Provide runtime utilities to deal with JNI functions in general (e.g. `currentJavaVirtualMachine`) 39 | 40 | 41 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/codec/StringCodecs.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.codec 2 | 3 | import com.squareup.kotlinpoet.CodeBlock 4 | import com.squareup.kotlinpoet.STRING 5 | import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType 6 | import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext 7 | import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeString 8 | import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeString 9 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 10 | import io.deepmedia.tools.knee.plugin.compiler.jni.JniType 11 | import org.jetbrains.kotlin.ir.builders.* 12 | import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration 13 | import org.jetbrains.kotlin.ir.expressions.IrExpression 14 | import org.jetbrains.kotlin.ir.types.IrSimpleType 15 | 16 | fun stringCodecs(context: KneeContext): List = listOf( 17 | StringCodec(context.symbols) // .withCollectionCodecs(context) 18 | ) 19 | 20 | private class StringCodec(symbols: KneeSymbols) : Codec( 21 | localType = symbols.builtIns.stringType as IrSimpleType, 22 | encodedType = JniType.Object(symbols, CodegenType.from(STRING)) 23 | ) { 24 | private val encode = symbols.functions(encodeString).single() 25 | private val decode = symbols.functions(decodeString).single() 26 | 27 | override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { 28 | return irCall(decode).apply { 29 | putValueArgument(0, irGet(irContext.environment)) 30 | putValueArgument(1, irGet(jni)) 31 | } 32 | } 33 | 34 | override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { 35 | return irCall(encode).apply { 36 | putValueArgument(0, irGet(irContext.environment)) 37 | putValueArgument(1, irGet(local)) 38 | } 39 | } 40 | 41 | // In codegen / JVM world, a String is already a String - nothing to do 42 | 43 | override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { 44 | return jni 45 | } 46 | 47 | override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { 48 | return local 49 | } 50 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/UpwardProperties.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext 4 | import io.deepmedia.tools.knee.plugin.compiler.features.KneeUpwardProperty 5 | import org.jetbrains.kotlin.descriptors.Modality 6 | import org.jetbrains.kotlin.ir.builders.declarations.* 7 | import org.jetbrains.kotlin.ir.declarations.IrProperty 8 | import org.jetbrains.kotlin.name.Name 9 | 10 | fun processUpwardProperty(property: KneeUpwardProperty, context: KneeContext) { 11 | property.makeIr(context) 12 | } 13 | 14 | private fun KneeUpwardProperty.makeIr(context: KneeContext): IrProperty { 15 | val implementationClass = kind.parent.irImplementation 16 | return implementationClass.addProperty { 17 | this.name = source.name 18 | this.origin = source.origin 19 | this.modality = Modality.FINAL 20 | this.isVar = source.isVar 21 | }.also { implementation -> 22 | implementation.overriddenSymbols += source.symbol 23 | val propertyType = getter.source.returnType 24 | // backing field: there's none, getter and setter delegate to JVM. 25 | // setter and getter: we add blank ones here, then body is added by ReverseFunctions.kt 26 | getter.let { knee -> 27 | knee.implementation = implementation.addGetter { 28 | this.returnType = propertyType 29 | }.apply { 30 | // Removing, handled in ReverseFunctions.kt 31 | // dispatchReceiverParameter = implementationClass.thisReceiver!!.copyTo(this) 32 | } 33 | } 34 | setter?.let { knee -> 35 | knee.implementation = implementation.factory.buildFun { 36 | this.returnType = context.symbols.builtIns.unitType 37 | this.name = Name.special("") 38 | }.apply { 39 | implementation.setter = this 40 | correspondingPropertySymbol = implementation.symbol 41 | parent = implementation.parent 42 | // Removing, handled in ReverseFunctions.kt 43 | // dispatchReceiverParameter = implementationClass.thisReceiver!!.copyTo(this) 44 | // addValueParameter("value", propertyType) 45 | } 46 | } 47 | }.also { 48 | irProducts.add(it) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/serialization/Classes.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.serialization 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.builtins.ListSerializer 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | import org.jetbrains.kotlin.ir.declarations.IrClass 11 | import org.jetbrains.kotlin.ir.util.classIdOrFail 12 | import org.jetbrains.kotlin.name.ClassId 13 | import org.jetbrains.kotlin.name.FqName 14 | 15 | fun IrClassListSerializer(symbols: KneeSymbols) = ListSerializer(IrClassSerializer(symbols)) 16 | 17 | class IrClassSerializer(private val symbols: KneeSymbols) : KSerializer { 18 | override val descriptor: SerialDescriptor get() = ClassIdSerializer.descriptor 19 | override fun serialize(encoder: Encoder, value: IrClass) { 20 | encoder.encodeSerializableValue(ClassIdSerializer, value.classIdOrFail) 21 | } 22 | override fun deserialize(decoder: Decoder): IrClass { 23 | return symbols.klass(decoder.decodeSerializableValue(ClassIdSerializer)).owner 24 | } 25 | } 26 | 27 | object ClassIdSerializer : KSerializer { 28 | override val descriptor get() = ClassIdSurrogate.serializer().descriptor 29 | override fun serialize(encoder: Encoder, value: ClassId) { 30 | encoder.encodeSerializableValue(ClassIdSurrogate.serializer(), ClassIdSurrogate(value)) 31 | } 32 | override fun deserialize(decoder: Decoder): ClassId { 33 | return decoder.decodeSerializableValue(ClassIdSurrogate.serializer()).classId 34 | } 35 | } 36 | 37 | @Serializable 38 | private data class ClassIdSurrogate( 39 | @Serializable(with = FqNameSerializer::class) private val packageFqName: FqName, 40 | @Serializable(with = FqNameSerializer::class) private val relativeClassName: FqName, 41 | private val isLocal: Boolean 42 | ) { 43 | constructor(classId: ClassId) : this(classId.packageFqName, classId.relativeClassName, classId.isLocal) 44 | // constructor(klass: IrClass) : this(klass.classIdOrFail) 45 | val classId get() = ClassId(packageFqName, relativeClassName, isLocal) 46 | // fun klass(symbols: KneeSymbols): IrClass = symbols.klass(classId).owner 47 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/serialization/Types.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.serialization 2 | 3 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 4 | import kotlinx.serialization.Contextual 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | import org.jetbrains.kotlin.ir.declarations.IrClass 10 | import org.jetbrains.kotlin.ir.types.* 11 | import org.jetbrains.kotlin.types.Variance 12 | 13 | class IrSimpleTypeSerializer(private val symbols: KneeSymbols) : KSerializer { 14 | override val descriptor get() = IrSimpleTypeSurrogate.serializer().descriptor 15 | override fun serialize(encoder: Encoder, value: IrSimpleType) { 16 | encoder.encodeSerializableValue(IrSimpleTypeSurrogate.serializer(), IrSimpleTypeSurrogate(value)) 17 | } 18 | override fun deserialize(decoder: Decoder): IrSimpleType { 19 | return decoder.decodeSerializableValue(IrSimpleTypeSurrogate.serializer()).simpleType(symbols) 20 | } 21 | } 22 | 23 | @Serializable 24 | private data class IrSimpleTypeSurrogate( 25 | private val nullable: Boolean, 26 | private val typeArguments: List, 27 | @Contextual private val classRef: IrClass 28 | ) { 29 | constructor(type: IrSimpleType) : this( 30 | nullable = type.isNullable(), 31 | classRef = type.classOrFail.owner, 32 | typeArguments = type.arguments.map { 33 | check(it is IrTypeProjection) { "Type arguments should be IrTypeProjection, was: $it" } 34 | check(it.variance == Variance.INVARIANT) { "Type arguments variance should be INVARIANT, was: ${it.variance}" } 35 | val simpleType = checkNotNull(it.type as IrSimpleType) { "Type arguments should be IrSimpleType, was: ${it.type}" } 36 | IrSimpleTypeSurrogate(simpleType) 37 | // val klass = checkNotNull(it.type.classOrNull) { "Type arguments should be classes, was: ${it.type.dumpKotlinLike()}" } 38 | // ModuleMetadataClass(klass.owner) 39 | } 40 | ) 41 | fun simpleType(symbols: KneeSymbols): IrSimpleType { 42 | // val args = typeArguments.map { it.klass(symbols).defaultType }.toTypedArray() 43 | val args = typeArguments.map { it.simpleType(symbols) }.toTypedArray() 44 | val res = classRef.typeWith(*args) 45 | return res.withNullability(nullable) 46 | } 47 | } -------------------------------------------------------------------------------- /knee-runtime/src/backendMain/kotlin/compiler/Instances.kn.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.runtime.compiler 2 | 3 | import io.deepmedia.tools.knee.runtime.* 4 | import kotlinx.cinterop.* 5 | import platform.android.* 6 | 7 | 8 | // internal lateinit var kneeWrapInstance: jmethodID // public fun kneeWrapInstance(ref: Long, className: String): Any? 9 | // internal lateinit var kneeUnwrapInstance: jmethodID // public fun kneeUnwrapInstance(instance: Any): Long 10 | 11 | private const val InstancesKtFqn = "io.deepmedia.tools.knee.runtime.compiler.InstancesKt" 12 | 13 | internal fun initInstances(environment: JniEnvironment) { 14 | // kneeWrapInstance = MethodIds.get(environment, InstancesKtFqn, "kneeWrapInstance", "(JLjava/lang/String;)Ljava/lang/Object;", true) 15 | // kneeUnwrapInstance = MethodIds.get(environment, InstancesKtFqn, "kneeUnwrapInstance", "(Ljava/lang/Object;)J", true) 16 | environment.registerNatives( 17 | classFqn = InstancesKtFqn, 18 | JniNativeMethod( 19 | name = "kneeDisposeInstance", 20 | signature = "(J)V", 21 | pointer = staticCFunction { _, _, ref -> 22 | runCatching { ref.toCPointer()!!.asStableRef().dispose() } 23 | } 24 | ), 25 | JniNativeMethod( 26 | name = "kneeDescribeInstance", 27 | signature = "(J)Ljava/lang/String;", 28 | pointer = staticCFunction { env, _, ref -> 29 | ref.toCPointer()!!.asStableRef().get().toString().let { env.newStringUTF(it) } 30 | } 31 | ), 32 | JniNativeMethod( 33 | name = "kneeHashInstance", 34 | signature = "(J)I", 35 | pointer = staticCFunction { env, _, ref -> 36 | ref.toCPointer()!!.asStableRef().get().hashCode() 37 | } 38 | ), 39 | JniNativeMethod( 40 | name = "kneeCompareInstance", 41 | signature = "(JJ)Z", 42 | pointer = staticCFunction { env, _, ref0, ref1 -> 43 | val i0 = ref0.toCPointer()?.asStableRef()?.get() 44 | val i1 = ref1.toCPointer()?.asStableRef()?.get() 45 | if (i0 != null && i0 == i1) 1u else 0u 46 | } 47 | ) 48 | ) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /knee-gradle-plugin/src/main/kotlin/KneeExtension.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.gradle 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.file.Directory 5 | import org.gradle.api.file.DirectoryProperty 6 | import org.gradle.api.file.ProjectLayout 7 | import org.gradle.api.model.ObjectFactory 8 | import org.gradle.api.provider.Property 9 | import org.gradle.api.provider.Provider 10 | import org.gradle.api.provider.ProviderFactory 11 | import org.gradle.kotlin.dsl.property 12 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 13 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 14 | import javax.inject.Inject 15 | 16 | abstract class KneeExtension @Inject constructor(objects: ObjectFactory, private val layout: ProjectLayout, private val providers: ProviderFactory) { 17 | 18 | private fun Property.conventions(key: String, fallback: Boolean): Property { 19 | val env = providers.environmentVariable("io.deepmedia.knee.$key").map { it.toBoolean() } 20 | val prop = providers.gradleProperty("io.deepmedia.knee.$key").map { it.toBoolean() } 21 | return convention(prop.orElse(env).orElse(fallback)) 22 | } 23 | 24 | val enabled: Property = objects.property().convention(true) 25 | 26 | val verboseLogs: Property = objects.property().conventions("verboseLogs", false) 27 | 28 | val verboseRuntime: Property = objects.property().conventions("verboseRuntime", false) 29 | 30 | val verboseSources: Property = objects.property().conventions("verboseSources", false) 31 | 32 | val connectTargets: Property = objects.property().conventions("connectTargets", true) 33 | 34 | /* val autoBind: Property = objects 35 | .property() 36 | .convention(false) */ 37 | 38 | val generatedSourceDirectory: DirectoryProperty = objects 39 | .directoryProperty() 40 | .convention(layout.buildDirectory.map { it.dir("knee").dir("src") }) 41 | 42 | fun generatedSourceDirectory(target: KotlinNativeTarget): Provider { 43 | // note: this is possible because we only apply on the main compilation 44 | // otherwise we'd have to add a main/test subdirectory/suffix for example. 45 | return generatedSourceDirectory.dir(target.name) 46 | } 47 | 48 | internal fun log(message: String) { 49 | if (verboseLogs.get()) println("[KneePlugin] [${projectName}] $message") 50 | } 51 | 52 | internal lateinit var projectName: String 53 | } -------------------------------------------------------------------------------- /knee-compiler-plugin/src/main/kotlin/codec/PrimitiveCodecs.kt: -------------------------------------------------------------------------------- 1 | package io.deepmedia.tools.knee.plugin.compiler.codec 2 | 3 | import com.squareup.kotlinpoet.CodeBlock 4 | import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext 5 | import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols 6 | import io.deepmedia.tools.knee.plugin.compiler.jni.JniType 7 | import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeBoolean 8 | import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeBoolean 9 | import org.jetbrains.kotlin.ir.builders.* 10 | import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration 11 | import org.jetbrains.kotlin.ir.expressions.IrExpression 12 | import org.jetbrains.kotlin.ir.types.IrSimpleType 13 | 14 | fun primitiveCodecs(context: KneeContext) = listOf( 15 | // TODO: JVM shorts. Becomes jshort which is UChar in native. 16 | // TODO: JVM chars. Becomes jchar which is UShort in native. 17 | *IdentityCodec(JniType.Byte(context.symbols)).withCollectionCodecs(context), 18 | *IdentityCodec(JniType.Int(context.symbols)).withCollectionCodecs(context), 19 | *IdentityCodec(JniType.Long(context.symbols)).withCollectionCodecs(context), 20 | *IdentityCodec(JniType.Float(context.symbols)).withCollectionCodecs(context), 21 | *IdentityCodec(JniType.Double(context.symbols)).withCollectionCodecs(context), 22 | // JVM booleans. Becomes jboolean which is UByte in native. 23 | *BooleanCodec(context.symbols).withCollectionCodecs(context) 24 | ) 25 | 26 | private class BooleanCodec(symbols: KneeSymbols) : Codec(symbols.builtIns.booleanType as IrSimpleType, JniType.BooleanAsUByte(symbols)) { 27 | private val create = symbols.functions(encodeBoolean).single() 28 | private val decode = symbols.functions(decodeBoolean).single() 29 | 30 | override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { 31 | return irCall(decode).apply { 32 | putValueArgument(0, irGet(jni)) 33 | } 34 | } 35 | 36 | override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { 37 | return irCall(create).apply { 38 | putValueArgument(0, irGet(local)) 39 | } 40 | } 41 | 42 | override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String) = jni 43 | override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String) = local 44 | 45 | override fun toString(): String { 46 | return "BooleanCodec" 47 | } 48 | } --------------------------------------------------------------------------------