Skip to content

Commit

Permalink
Object and companion object support (#15)
Browse files Browse the repository at this point in the history
* Expose plugin version

* KneeObject annotation, collect objects

* Basic ObjectCodec

* Support functions inside objects

* Support properties inside objects

* Support companion objects

* Object docs

* Fix override modifier
  • Loading branch information
natario1 authored Sep 11, 2024
1 parent 30a80d5 commit 31f9c05
Show file tree
Hide file tree
Showing 27 changed files with 359 additions and 51 deletions.
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

0 comments on commit 31f9c05

Please sign in to comment.