├── .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 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/experiments/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/experiments/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/experiments/.idea/runConfigurations/compose_notes_l.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/experiments/.idea/runConfigurations/expect_actual_c.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/experiments/.idea/runConfigurations/expect_actual_l.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/runtime_deployLocal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deployLocal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/gradle_plugin_deployLocal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deployGithub.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deploySonatype.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/annotations_deployLocal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/.idea/runConfigurations/link_imports.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | [](https://github.com/deepmedia/Knee/actions)
2 | [](https://github.com/deepmedia/Knee/releases)
3 | [](https://github.com/deepmedia/Knee/issues)
4 |
5 | 
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/tests/.idea/runConfigurations/test_classes.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 |
--------------------------------------------------------------------------------
/tests/.idea/runConfigurations/test_imports.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 |
--------------------------------------------------------------------------------
/tests/.idea/runConfigurations/test_coroutines.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 |
--------------------------------------------------------------------------------
/tests/.idea/runConfigurations/test_primitives.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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/experiments/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/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 |
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 |
38 |
39 |
40 |
41 |
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 | }
--------------------------------------------------------------------------------