diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8d2c90..422cdbf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,4 +38,4 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - script: cd tests && ./gradlew connectedCheck \ No newline at end of file + script: cd tests && ./gradlew connectedCheck --stacktrace \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bf5ee4f..f4488dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 81b0c79..3c7d420 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -21,4 +21,4 @@ jobs: distribution: temurin cache: gradle - name: Publish Nexus Snapshot - run: ./gradlew deployNexusSnapshot \ No newline at end of file + run: ./gradlew deployNexusSnapshot --stacktrace \ No newline at end of file diff --git a/docs/features/index.mdx b/docs/features/index.mdx index bb99fa5..80a43eb 100644 --- a/docs/features/index.mdx +++ b/docs/features/index.mdx @@ -10,6 +10,7 @@ docs: - builtin-types - enums - classes + - objects - interfaces - buffers --- diff --git a/docs/features/objects.mdx b/docs/features/objects.mdx new file mode 100644 index 0000000..5b35a0a --- /dev/null +++ b/docs/features/objects.mdx @@ -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. \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx index a9390b6..c826a70 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -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 diff --git a/knee-annotations/src/backendMain/kotlin/Knee.kt b/knee-annotations/src/backendMain/kotlin/Knee.kt index d195d34..322c179 100644 --- a/knee-annotations/src/backendMain/kotlin/Knee.kt +++ b/knee-annotations/src/backendMain/kotlin/Knee.kt @@ -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 diff --git a/knee-compiler-plugin/src/main/kotlin/Classes.kt b/knee-compiler-plugin/src/main/kotlin/Classes.kt index 572b439..52048bc 100644 --- a/knee-compiler-plugin/src/main/kotlin/Classes.kt +++ b/knee-compiler-plugin/src/main/kotlin/Classes.kt @@ -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 @@ -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) } diff --git a/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt b/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt index c245086..da93c3a 100644 --- a/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt +++ b/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt @@ -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 @@ -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) } } @@ -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) } } diff --git a/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt b/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt index 2c37004..1de2bf0 100644 --- a/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt +++ b/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt @@ -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) diff --git a/knee-compiler-plugin/src/main/kotlin/Interfaces.kt b/knee-compiler-plugin/src/main/kotlin/Interfaces.kt index 40e67b2..9b4b40a 100644 --- a/knee-compiler-plugin/src/main/kotlin/Interfaces.kt +++ b/knee-compiler-plugin/src/main/kotlin/Interfaces.kt @@ -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 @@ -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) diff --git a/knee-compiler-plugin/src/main/kotlin/MainBir.kt b/knee-compiler-plugin/src/main/kotlin/MainBir.kt index 4a2a54b..aa39f4d 100644 --- a/knee-compiler-plugin/src/main/kotlin/MainBir.kt +++ b/knee-compiler-plugin/src/main/kotlin/MainBir.kt @@ -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) } diff --git a/knee-compiler-plugin/src/main/kotlin/Objects.kt b/knee-compiler-plugin/src/main/kotlin/Objects.kt new file mode 100644 index 0000000..62b3ff0 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/Objects.kt @@ -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" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt b/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt index 04b7913..aa407e4 100644 --- a/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt +++ b/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt @@ -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, diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt index f00f355..8001bc0 100644 --- a/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt @@ -69,21 +69,23 @@ class KneeClass( return propertyOverriddenSymbols.any { it in throwableSymbols } } - @Suppress("UNCHECKED_CAST") - private fun > 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.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.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.hasAnnotationCopyingFromParents(annotation: FqName): Boolean { + if (hasAnnotation(annotation)) return true + val parent = findAnnotatedParentRecursive(annotation) ?: return false + copyAnnotationsFrom(parent) + return true + } + } } diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt index 5e1ee6d..7498b5d 100644 --- a/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt @@ -23,31 +23,31 @@ class KneeCollector(module: IrModuleFragment) : IrElementVisitorVoid { private val classes = mutableListOf() private val enums = mutableListOf() private val interfaces = mutableListOf() + private val objects = mutableListOf() private val importedClasses = mutableListOf() private val importedEnums = mutableListOf() private val importedInterfaces = mutableListOf() + private val importedObjects = mutableListOf() // private val imports = mutableListOf() private val topLevelDownwardFunctions = mutableListOf() private val topLevelDownwardProperties = mutableListOf() val allInterfaces get() = interfaces + importedInterfaces - // + imports.flatMap { it.interfaces } - val allEnums get() = enums + importedEnums - // + imports.flatMap { it.enums } - val allClasses get() = classes + importedClasses - // + imports.flatMap { it.classes } + val allObjects get() = objects + importedObjects val allDownwardProperties get() = topLevelDownwardProperties + allClasses.flatMap { it.properties } + + allObjects.flatMap { it.properties } + allInterfaces.flatMap { it.downwardProperties } val allDownwardFunctions get() = topLevelDownwardFunctions + allDownwardProperties.flatMap { it.functions } + allClasses.flatMap { it.functions } + + allObjects.flatMap { it.functions } + allInterfaces.flatMap { it.downwardFunctions } val allUpwardProperties get() = @@ -57,7 +57,8 @@ class KneeCollector(module: IrModuleFragment) : IrElementVisitorVoid { allUpwardProperties.flatMap { it.functions } + allInterfaces.flatMap { it.upwardFunctions } - private val KneeClass.functions get() = constructors + members // + disposer + private val KneeClass.functions get() = constructors + members + private val KneeObject.functions get() = members private val KneeDownwardProperty.functions get() = listOfNotNull(getter, setter) private val KneeUpwardProperty.functions get() = listOfNotNull(getter, setter) @@ -117,18 +118,19 @@ class KneeCollector(module: IrModuleFragment) : IrElementVisitorVoid { } override fun visitTypeAlias(declaration: IrTypeAlias) { + fun importInfo() = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) if (declaration.hasAnnotation(AnnotationIds.KneeEnum)) { hasDeclarations = true - val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) - importedEnums.add(KneeEnum(declaration.expandedType.classOrFail.owner, importInfo)) + importedEnums.add(KneeEnum(declaration.expandedType.classOrFail.owner, importInfo())) } else if (declaration.hasAnnotation(AnnotationIds.KneeClass)) { hasDeclarations = true - val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) - importedClasses.add(KneeClass(declaration.expandedType.classOrFail.owner, importInfo)) + importedClasses.add(KneeClass(declaration.expandedType.classOrFail.owner, importInfo())) + } else if (declaration.hasAnnotation(AnnotationIds.KneeObject)) { + hasDeclarations = true + importedObjects.add(KneeObject(declaration.expandedType.classOrFail.owner, importInfo())) } else if (declaration.hasAnnotation(AnnotationIds.KneeInterface)) { hasDeclarations = true - val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) - importedInterfaces.add(KneeInterface(declaration.expandedType.classOrFail.owner, importInfo)) + importedInterfaces.add(KneeInterface(declaration.expandedType.classOrFail.owner, importInfo())) } super.visitTypeAlias(declaration) } @@ -140,6 +142,9 @@ class KneeCollector(module: IrModuleFragment) : IrElementVisitorVoid { } else if (declaration.hasAnnotation(AnnotationIds.KneeClass)) { hasDeclarations = true classes.add(KneeClass(declaration)) + } else if (declaration.hasAnnotation(AnnotationIds.KneeObject)) { + hasDeclarations = true + objects.add(KneeObject(declaration)) } else if (declaration.hasAnnotation(AnnotationIds.KneeInterface)) { hasDeclarations = true interfaces.add(KneeInterface(declaration)) diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt index 786ba16..dca3958 100644 --- a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlin.ir.util.* class KneeDownwardFunction( source: IrFunction, - parentInstance: KneeFeature<*>?, // class or interface + parentInstance: KneeFeature<*>?, // class or interface or object parentProperty: KneeDownwardProperty? ) : KneeFeature(source, "Knee") { @@ -24,9 +24,13 @@ class KneeDownwardFunction( class InterfaceMember(val owner: KneeInterface, property: KneeDownwardProperty?) : Kind(property) + class ObjectMember(val owner: KneeObject, property: KneeDownwardProperty?) : Kind(property) + val importInfo: ImportInfo? get() = when (this) { - is TopLevel, is ClassConstructor, is ClassMember -> null + is TopLevel -> null + is ClassConstructor, is ClassMember -> null // TODO: why? is InterfaceMember -> owner.importInfo + is ObjectMember -> owner.importInfo } } @@ -44,6 +48,11 @@ class KneeDownwardFunction( && source.parentAsClass.kind == ClassKind.INTERFACE -> { Kind.InterfaceMember(parentInstance as KneeInterface, parentProperty) } + source.dispatchReceiverParameter != null + && source.parent is IrClass + && source.parentAsClass.kind == ClassKind.OBJECT -> { + Kind.ObjectMember(parentInstance as KneeObject, parentProperty) + } else -> error("$this must be top level, a class constructor, a class destructor or a class member.") } diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt index b831138..d89f368 100644 --- a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt @@ -13,11 +13,13 @@ class KneeDownwardProperty( sealed class Kind { class InterfaceMember(val owner: KneeInterface) : Kind() class ClassMember(val owner: KneeClass) : Kind() + class ObjectMember(val owner: KneeObject) : Kind() object TopLevel : Kind() val importInfo: ImportInfo? get() = when (this) { TopLevel -> null is ClassMember -> owner.importInfo + is ObjectMember -> owner.importInfo is InterfaceMember -> owner.importInfo } } @@ -25,10 +27,11 @@ class KneeDownwardProperty( val kind = when (parentInstance) { is KneeInterface -> Kind.InterfaceMember(parentInstance) is KneeClass -> Kind.ClassMember(parentInstance) - else -> Kind.TopLevel + is KneeObject -> Kind.ObjectMember(parentInstance) + null -> Kind.TopLevel + else -> error("Unsupported parent instance: $parentInstance") } - val setter: KneeDownwardFunction? = source.setter?.let { it.copyAnnotationsFrom(source) KneeDownwardFunction(it, parentInstance, this) diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeObject.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeObject.kt new file mode 100644 index 0000000..d102a05 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeObject.kt @@ -0,0 +1,47 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.features.KneeClass.Companion.hasAnnotationCopyingFromParents +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.FqName + +class KneeObject( + source: IrClass, + val importInfo: ImportInfo? = null, +) : KneeFeature(source, "KneeObject") { + + val members: List + val properties: List + + init { + source.requireNotComplex( + this, ClassKind.OBJECT, + typeArguments = importInfo?.type?.arguments ?: emptyList() + ) + + val members = source.functions + .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } + // exclude static function (see isStaticMethodOfClass impl) + // and property accessors (one should use @Knee on the property instead) + .filter { it.dispatchReceiverParameter != null } + .filter { !it.isPropertyAccessor } + .onEach { it.requireNotComplex("$this member ${it.name}", allowSuspend = true) } + .toList() + + val properties = source.properties + .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } + .toList() + + this.members = members.map { KneeDownwardFunction(it, parentInstance = this, parentProperty = null) } + this.properties = properties.map { KneeDownwardProperty(it, parentInstance = this) } + } + + lateinit var codegenClone: CodegenClass +} diff --git a/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt index ded8b26..1648496 100644 --- a/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt +++ b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt @@ -92,7 +92,13 @@ class DownwardFunctionSignature(source: IrFunction, kind: Kind, context: KneeCon val extraParameters: List> = buildList { // instance member functions should pass the handle reference so that we can decode them in IR. - if (kind is Kind.ClassMember || kind is Kind.InterfaceMember) { + // Note that this is NOT strictly needed for ObjectMember, but doing so would complicate code + // in other parts of the plugin (DownwardFunctionsIr would need to handle this case specifically) + val needsReceiverInstance = when (kind) { + is Kind.ClassMember, is Kind.InterfaceMember, is Kind.ObjectMember -> true + is Kind.ClassConstructor, is Kind.TopLevel -> false + } + if (needsReceiverInstance) { add(Extra.ReceiverInstance to context.mapper.get(source.parentAsClass.thisReceiver!!.type.simple("DownwardSignature.extraParams").concrete(kind.importInfo))) } // suspend functions should pass the 'continuation'. It is an instance of KneeSuspendInvoker @@ -135,10 +141,8 @@ class DownwardFunctionSignature(source: IrFunction, kind: Kind, context: KneeCon // jobject or jclass depending on static vs instance function. In practice this won't make // any difference because jclass is a typealias for jobject, but whatever. add(KnPrefix.JniObjectOrClass to context.symbols.typeAliasUnwrapped(when (kind) { - is Kind.TopLevel, - is Kind.ClassConstructor -> PlatformIds.jobject - is Kind.ClassMember -> PlatformIds.jclass - is Kind.InterfaceMember -> PlatformIds.jclass + is Kind.TopLevel, is Kind.ClassConstructor -> PlatformIds.jobject + is Kind.ClassMember, is Kind.ObjectMember, is Kind.InterfaceMember -> PlatformIds.jclass })) } @@ -205,6 +209,7 @@ class DownwardFunctionSignature(source: IrFunction, kind: Kind, context: KneeCon when (kind) { is Kind.InterfaceMember -> kind.owner.codegenImplementation.type is Kind.ClassMember -> kind.owner.codegenClone.type + is Kind.ObjectMember -> kind.owner.codegenClone.type is Kind.ClassConstructor -> { // Technically for constructors we codegen in the companion object, but it makes no difference // from the JVM perspective, it's a function inside the owner class. @@ -228,6 +233,7 @@ class DownwardFunctionSignature(source: IrFunction, kind: Kind, context: KneeCon fun mapper(name: String): String = "$" + when (kind) { is Kind.TopLevel, is Kind.ClassMember, + is Kind.ObjectMember, is Kind.InterfaceMember -> listOfNotNull(prefix, name, suffix).joinToString(separator = "_") is Kind.ClassConstructor -> { // standard name for constructors is for all of them, so we must make it unique in some way. diff --git a/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt b/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt index 4b11320..1ab5c74 100644 --- a/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt +++ b/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt @@ -37,7 +37,7 @@ object InstancesCodegen { .build()) } - fun TypeSpec.Builder.addObjectOverrides(verbose: Boolean) { + fun TypeSpec.Builder.addAnyOverrides(verbose: Boolean) { val pkg = "io.deepmedia.tools.knee.runtime.compiler" val type = this.build().name!! addFunction(FunSpec.builder("finalize") diff --git a/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt b/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt index 67e80d8..ea7db25 100644 --- a/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt +++ b/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt @@ -25,6 +25,7 @@ object AnnotationIds { val Knee = FqName("io.deepmedia.tools.knee.annotations.Knee") val KneeEnum = FqName("io.deepmedia.tools.knee.annotations.KneeEnum") val KneeClass = FqName("io.deepmedia.tools.knee.annotations.KneeClass") + val KneeObject = FqName("io.deepmedia.tools.knee.annotations.KneeObject") val KneeInterface = FqName("io.deepmedia.tools.knee.annotations.KneeInterface") val KneeRaw = FqName("io.deepmedia.tools.knee.annotations.KneeRaw") } diff --git a/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt b/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt index 7bec3f8..0b96e73 100644 --- a/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt +++ b/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt @@ -22,7 +22,10 @@ fun IrClass.asTypeSpec(rename: ((String) -> String)? = null): TypeSpec.Builder { val name = codegenName.map { rename?.invoke(it) ?: it }.asString() return when (kind) { ClassKind.ENUM_CLASS -> TypeSpec.enumBuilder(name) - ClassKind.OBJECT -> TypeSpec.objectBuilder(name) + ClassKind.OBJECT -> when { + isCompanion -> TypeSpec.companionObjectBuilder(if (name == "Companion") null else name) + else -> TypeSpec.objectBuilder(name) + } ClassKind.INTERFACE -> TypeSpec.interfaceBuilder(name) ClassKind.ANNOTATION_CLASS -> TypeSpec.annotationBuilder(name) ClassKind.ENUM_ENTRY -> error("Enum entries ($this) can't become a TypeSpec.") diff --git a/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt b/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt index 0eee076..3fd36d2 100644 --- a/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt +++ b/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt @@ -21,6 +21,11 @@ import org.jetbrains.kotlin.konan.target.KonanTarget @Suppress("unused") class KneePlugin : KotlinCompilerPluginSupportPlugin { + companion object { + @Suppress("ConstPropertyName") + const val Version = KneeVersion + } + override fun apply(target: Project) { val knee = target.extensions.create("knee", KneeExtension::class.java) knee.projectName = target.name diff --git a/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ObjectTests.kt b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ObjectTests.kt new file mode 100644 index 0000000..b4f81fa --- /dev/null +++ b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ObjectTests.kt @@ -0,0 +1,56 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import org.junit.Test + +class ObjectTests { + + companion object { + init { + System.loadLibrary("test_classes") + } + } + + @Test + fun testTopLevel_exists() { + TopLevelObject::class.toString() + } + + @Test + fun testTopLevel_toString() { + TopLevelObject.reset() + val tl = TopLevelObject.toString() + check(tl == "TopLevelObject(0)") { tl } + } + + @Test + fun testTopLevel_functions() { + TopLevelObject.reset() + TopLevelObject.increment() + TopLevelObject.increment() + check(TopLevelObject.toString() == "TopLevelObject(2)") { TopLevelObject.toString() } + TopLevelObject.decrement() + check(TopLevelObject.toString() == "TopLevelObject(1)") { TopLevelObject.toString() } + } + + @Test + fun testTopLevel_property() { + TopLevelObject.reset() + TopLevelObject.value = 15 + check(TopLevelObject.value == 15) { TopLevelObject.value } + } + + @Test + fun testInner_property() { + ObjectParent.InnerObject.value = 15 + check(ObjectParent.InnerObject.value == 15) { ObjectParent.InnerObject.value } + } + + @Test + fun testCompanion_property() { + ObjectParent.value = 15 + check(ObjectParent.value == 15) { ObjectParent.value } + } + +} diff --git a/tests/test-classes/src/backendMain/kotlin/Definintions.kt b/tests/test-classes/src/backendMain/kotlin/ClassDefinintions.kt similarity index 98% rename from tests/test-classes/src/backendMain/kotlin/Definintions.kt rename to tests/test-classes/src/backendMain/kotlin/ClassDefinintions.kt index 5245317..fe92235 100644 --- a/tests/test-classes/src/backendMain/kotlin/Definintions.kt +++ b/tests/test-classes/src/backendMain/kotlin/ClassDefinintions.kt @@ -2,7 +2,6 @@ package io.deepmedia.tools.knee.tests import io.deepmedia.tools.knee.annotations.* import io.deepmedia.tools.knee.runtime.* -import kotlin.native.identityHashCode import kotlin.random.Random import kotlin.random.nextUInt diff --git a/tests/test-classes/src/backendMain/kotlin/ObjectDefinitions.kt b/tests/test-classes/src/backendMain/kotlin/ObjectDefinitions.kt new file mode 100644 index 0000000..bfe750c --- /dev/null +++ b/tests/test-classes/src/backendMain/kotlin/ObjectDefinitions.kt @@ -0,0 +1,25 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlin.random.Random +import kotlin.random.nextUInt + +@KneeObject +object TopLevelObject { + @Knee var value: Int = 0 + @Knee fun reset() { value = 0 } + @Knee fun increment() { value += 1 } + @Knee fun decrement() { value -= 1 } + @Knee override fun toString(): String = "TopLevelObject($value)" +} + +class ObjectParent { + @KneeObject object InnerObject { + @Knee var value: Int = 0 + } + + @KneeObject companion object { + @Knee var value: Int = 0 + } +}