Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object and companion object support #15

Merged
merged 8 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: cd tests && ./gradlew connectedCheck
script: cd tests && ./gradlew connectedCheck --stacktrace
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
distribution: temurin
cache: gradle
- name: Publish to Maven Central
run: ./gradlew deployNexus
run: ./gradlew deployNexus --stacktrace
- name: Publish to GitHub Packages
run: ./gradlew deployGithub
run: ./gradlew deployGithub --stacktrace
2 changes: 1 addition & 1 deletion .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
distribution: temurin
cache: gradle
- name: Publish Nexus Snapshot
run: ./gradlew deployNexusSnapshot
run: ./gradlew deployNexusSnapshot --stacktrace
1 change: 1 addition & 0 deletions docs/features/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ docs:
- builtin-types
- enums
- classes
- objects
- interfaces
- buffers
---
Expand Down
52 changes: 52 additions & 0 deletions docs/features/objects.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Objects
description: >
Understand how Knee compiler plugin can serialize declared objects and let you pass them from Kotlin Native
to the JVM and vice versa, including support for externally defined objects.
---

# Objects

## Annotating objects

Whenever you declare an object, you can use the `@KneeObject` annotation to tell the compiler that it should be processed.
Knee supports objects in different scenarios:

- top level objects
- objects nested inside another declaration
- `companion` objects

```kotlin
@KneeObject object Foo {
...
}

class Utilities {
@KneeObject object Bar { ... }
@KneeObject companion object { ... }
}
```

Under the hood, objects are *not* actually serialized and passed through the JNI interface: since there can only be a single
instance of an object, no extra information is needed and the compiler can retrieve the object field statically on both
platforms.

## Annotating members

All callable members (functions, properties, constructors) of an object can be made available to the JVM side, but
they must be explicitly marked with the `@Knee` annotation as described in the [callables](callables) documentation.

```kotlin
@KneeObject object Game {
@Knee fun start() { ... }
fun loop() { ... }
}
```

In the example above, only the `start` function will be available on the JVM side.

## Importing objects

If you wish to annotate existing objects that you don't control, for example those coming from a different module,
you can technically use `@KneeObject` on type aliases. Unfortunately as of now, this functionality is very limited in that you
can't choose which declarations will be imported.
4 changes: 2 additions & 2 deletions docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ where you'll learn about all supported features such as:
- [Exception support](features/exceptions), including custom exception types
- Built-in serialization of [language primitives](features/builtin-types#primitives): numbers, strings, nullables, `Unit`, `Nothing`
- Built-in serialization of [collection types](features/builtin-types#collections): lists, sets, efficient arrays
- Custom [enums](features/enums) and [classes](features/classes)
- Custom [interfaces](features/interfaces) for two-way invocations
- Serialization of [enums](features/enums), [classes](features/classes) and [objects](features/objects)
- Serialization of [interfaces](features/interfaces) for two-way invocations
- Lambdas and [generics](features/interfaces#importing-interfaces) support
- [No-copy buffers](features/buffers), mapping `java.nio` buffers to `CPointer` on native
7 changes: 7 additions & 0 deletions knee-annotations/src/backendMain/kotlin/Knee.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ annotation class KneeEnum(val name: String = "")
@Retention(AnnotationRetention.BINARY)
annotation class KneeClass(val name: String = "")

@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS
)
@Retention(AnnotationRetention.BINARY)
annotation class KneeObject(val name: String = "")

@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS
Expand Down
4 changes: 2 additions & 2 deletions knee-compiler-plugin/src/main/kotlin/Classes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import io.deepmedia.tools.knee.plugin.compiler.codec.Codec
import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportAdapters
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addHandleConstructorAndField
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addObjectOverrides
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addAnyOverrides
import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeClass
import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeClass
import io.deepmedia.tools.knee.plugin.compiler.utils.asModifier
Expand Down Expand Up @@ -62,7 +62,7 @@ private fun KneeClass.makeCodegen(codegen: KneeCodegen) {
if (codegen.verbose) spec.addKdoc("knee:classes")
spec.addModifiers(source.visibility.asModifier())
spec.addHandleConstructorAndField(preserveSymbols = isThrowable) // for exception handling
spec.addObjectOverrides(codegen.verbose)
spec.addAnyOverrides(codegen.verbose)
if (isThrowable) {
spec.superclass(THROWABLE)
}
Expand Down
18 changes: 16 additions & 2 deletions knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.ir.builders.declarations.buildVariable
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.types.isAny
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.Name

Expand Down Expand Up @@ -57,7 +58,16 @@ private fun KneeDownwardFunction.makeCodegen(codegen: KneeCodegen, signature: Do
if (source.isSuspend) {
addModifiers(KModifier.SUSPEND)
}
if (kind is Kind.InterfaceMember) {
// Needs override: (source as? IrSimpleFunction)?.overriddenSymbols?.isNotEmpty() == true
// But the JVM hierarchy doesn't match the KN hierarchy, supertypes may be missing, so this needs to be treated differently.
// Could merge this logic with that of DownwardProperties
val isOverride = when {
kind is Kind.InterfaceMember -> true
source !is IrSimpleFunction -> false
source.overriddenSymbols.any { it.owner.parentClassOrNull?.defaultType?.isAny() == true } -> true // toString(), equals() or hashCode()
else -> false
}
if (isOverride) {
addModifiers(KModifier.OVERRIDE)
}
}
Expand All @@ -67,7 +77,11 @@ private fun KneeDownwardFunction.makeCodegen(codegen: KneeCodegen, signature: Do
// Add it unless getter or setter or constructor because KotlinPoet will throw in this case
// E.g. 'IllegalStateException: get() cannot have a return type'
signature.result.let {
if (!source.isGetter && !source.isSetter && kind !is Kind.ClassConstructor) {
val needsExplicitReturnType = when (kind) {
is Kind.ClassConstructor -> false
is Kind.TopLevel, is Kind.ClassMember, is Kind.InterfaceMember, is Kind.ObjectMember -> true
}
if (!source.isGetter && !source.isSetter && needsExplicitReturnType) {
returns(it.localCodegenType.name)
}
}
Expand Down
5 changes: 5 additions & 0 deletions knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ private fun KneeDownwardProperty.makeCodegen(codegen: KneeCodegen, symbols: Knee
codegen.prepareContainer(source, kind.importInfo).addChild(property)
codegenImplementation = property
}
is KneeDownwardProperty.Kind.ObjectMember -> {
val property = makeProperty(isOverride = false)
codegen.prepareContainer(source, kind.importInfo).addChild(property)
codegenImplementation = property
}
is KneeDownwardProperty.Kind.TopLevel -> {
val property = makeProperty()
codegen.prepareContainer(source, kind.importInfo).addChild(property)
Expand Down
4 changes: 2 additions & 2 deletions knee-compiler-plugin/src/main/kotlin/Interfaces.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import io.deepmedia.tools.knee.plugin.compiler.import.concrete
import io.deepmedia.tools.knee.plugin.compiler.import.writableParent
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addHandleConstructorAndField
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addObjectOverrides
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addAnyOverrides
import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds
import io.deepmedia.tools.knee.plugin.compiler.utils.*
import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeInterface
Expand Down Expand Up @@ -133,7 +133,7 @@ private fun KneeInterface.makeCodegenImplementation(codegen: KneeCodegen, contex
if (codegen.verbose) addKdoc("knee:interfaces:impl")
addSuperinterface(source.defaultType.concrete(importInfo).asTypeName())
addHandleConstructorAndField(false)
addObjectOverrides(codegen.verbose)
addAnyOverrides(codegen.verbose)
}
codegenImplementation = CodegenClass(builder).apply {
container.addChild(this)
Expand Down
2 changes: 2 additions & 0 deletions knee-compiler-plugin/src/main/kotlin/MainBir.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ private fun process(context: KneeContext, codegen: KneeCodegen) {
context.log.logMessage("[*] Preprocessing target:${context.module.name} platform:${context.plugin.platform}")
data.allInterfaces.processEach(context) { preprocessInterface(it, context) }
data.allClasses.processEach(context) { preprocessClass(it, context) }
data.allObjects.processEach(context) { preprocessObject(it, context) }

context.log.logMessage("[*] Processing target:${context.module.name} platform:${context.plugin.platform}")
data.allEnums.processEach(context) { processEnum(it, context, codegen) }
data.allClasses.processEach(context) { processClass(it, context, codegen, initInfo) }
data.allObjects.processEach(context) { processObject(it, context, codegen) }
data.allInterfaces.processEach(context) { processInterface(it, context, codegen, initInfo) }
data.allUpwardProperties.processEach(context) { processUpwardProperty(it, context) }
data.allDownwardProperties.processEach(context) { processDownwardProperty(it, context, codegen) }
Expand Down
63 changes: 63 additions & 0 deletions knee-compiler-plugin/src/main/kotlin/Objects.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.deepmedia.tools.knee.plugin.compiler

import com.squareup.kotlinpoet.*
import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass
import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen
import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext
import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols
import io.deepmedia.tools.knee.plugin.compiler.jni.JniType
import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext
import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext
import io.deepmedia.tools.knee.plugin.compiler.codec.Codec
import io.deepmedia.tools.knee.plugin.compiler.features.KneeObject
import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.HandleField
import io.deepmedia.tools.knee.plugin.compiler.utils.asModifier
import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeSpec
import io.deepmedia.tools.knee.plugin.compiler.utils.codegenFqName
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.util.*

fun preprocessObject(klass: KneeObject, context: KneeContext) {
context.mapper.register(ObjectCodec(
symbols = context.symbols,
irClass = klass.source,
))
}

fun processObject(klass: KneeObject, context: KneeContext, codegen: KneeCodegen) {
klass.makeCodegen(codegen)
}

private fun KneeObject.makeCodegen(codegen: KneeCodegen) {
val container = codegen.prepareContainer(source, importInfo)
codegenClone = container.addChildIfNeeded(CodegenClass(source.asTypeSpec())).apply {
if (codegen.verbose) spec.addKdoc("knee:objects")
spec.addModifiers(source.visibility.asModifier())
codegenProducts.add(this)
}
}

class ObjectCodec(
symbols: KneeSymbols,
private val irClass: IrClass,
) : Codec(irClass.defaultType, JniType.Byte(symbols)) {

override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression {
return irByte(0)
}

override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression {
return irGetObject(irClass.symbol)
}

override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String {
return irClass.codegenFqName.asString()
}

override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String {
return "0"
}
}
3 changes: 3 additions & 0 deletions knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import org.jetbrains.kotlin.ir.expressions.IrExpression
*
* It's very useful when type is not known as in generics - in many cases we want to
* know the function signature so we need a fixed [JniType]. This is what this does.
*
* Note that this only works thanks to some inner codec passed to the constructor,
* so the generic type is reified.
*/
class GenericCodec(
private val symbols: KneeSymbols,
Expand Down
32 changes: 17 additions & 15 deletions knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,23 @@ class KneeClass(
return propertyOverriddenSymbols.any { it in throwableSymbols }
}

@Suppress("UNCHECKED_CAST")
private fun <T: IrOverridableDeclaration<*>> T.findAnnotatedParentRecursive(annotation: FqName): T? {
return overriddenSymbols.asSequence().map {
val t = it.owner as T
if (t.hasAnnotation(annotation)) return@map t
t.findAnnotatedParentRecursive(annotation)
}.firstOrNull { it != null }
}
lateinit var codegenClone: CodegenClass

private fun <T: IrOverridableDeclaration<*>> T.hasAnnotationCopyingFromParents(annotation: FqName): Boolean {
if (hasAnnotation(annotation)) return true
val parent = findAnnotatedParentRecursive(annotation) ?: return false
copyAnnotationsFrom(parent)
return true
}
companion object {
@Suppress("UNCHECKED_CAST")
private fun <T: IrOverridableDeclaration<*>> T.findAnnotatedParentRecursive(annotation: FqName): T? {
return overriddenSymbols.asSequence().map {
val t = it.owner as T
if (t.hasAnnotation(annotation)) return@map t
t.findAnnotatedParentRecursive(annotation)
}.firstOrNull { it != null }
}

lateinit var codegenClone: CodegenClass
fun <T: IrOverridableDeclaration<*>> T.hasAnnotationCopyingFromParents(annotation: FqName): Boolean {
if (hasAnnotation(annotation)) return true
val parent = findAnnotatedParentRecursive(annotation) ?: return false
copyAnnotationsFrom(parent)
return true
}
}
}
Loading