You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建Kotlin注解类,基于字段位置与类型自动生成Getter方法

Automating Dump Data Getters/Setters with Kotlin Annotations

Got it, let's build this annotation-based solution to clean up your IDumpData interface. The idea is to replace all those manual offset calculations with annotations, letting code generation handle the tedious bits. Here's how to pull it off step by step:

1. Define Custom Annotations

First, we need two annotations: one to mark the interface that needs code generation, and another to specify which block each property maps to.

// Marks an interface that should have dump data accessors generated
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class DumpToClass

// Specifies which block (index) a property belongs to
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
annotation class Block(val value: Int)

2. Implement a Kotlin Symbol Processing (KSP) Processor

KSP is Kotlin's official tool for source code generation—perfect for this job. We'll write a processor that scans for @DumpToClass interfaces, analyzes each @Block property, calculates its offset in the block based on type size, and generates the getter (and setter, if needed) implementations.

Step 2.1: Add KSP Dependencies

First, set up your build.gradle.kts (or build.gradle) to include KSP:

plugins {
    kotlin("jvm") version "1.9.0"
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}

dependencies {
    implementation(kotlin("stdlib"))
    ksp("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13")
}

// Make generated code visible to the compiler
sourceSets.main {
    kotlin.srcDir("build/generated/ksp/main/kotlin")
}

Step 2.2: Write the Annotation Processor

Here's a simplified version of the processor that handles getters (and setters for mutable properties) for your target types:

import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate
import java.io.OutputStreamWriter

class DumpDataProcessor(private val codeGenerator: CodeGenerator, private val logger: KSPLogger) : SymbolProcessor {

    private val typeSizeMap = mapOf(
        "UInt" to 4,
        "UByte" to 1,
        "UShort" to 2
    )

    override fun process(resolver: Resolver): List<KSAnnotated> {
        val invalidSymbols = mutableListOf<KSAnnotated>()

        // Find all interfaces marked with @DumpToClass
        resolver.getSymbolsWithAnnotation(DumpToClass::class.qualifiedName!!)
            .filterIsInstance<KSClassDeclaration>()
            .forEach { interfaceDeclaration ->
                if (!interfaceDeclaration.validate()) {
                    invalidSymbols.add(interfaceDeclaration)
                    return@forEach
                }

                // Group properties by their block number
                val propertiesByBlock = interfaceDeclaration.getAllProperties()
                    .filter { it.annotations.any { ann -> ann.shortName.getShortName() == "Block" } }
                    .groupBy { 
                        it.annotations.first { ann -> ann.shortName.getShortName() == "Block" }
                            .arguments.first().value as Int 
                    }

                // Generate implementation class
                generateImplementation(interfaceDeclaration, propertiesByBlock)
            }

        return invalidSymbols
    }

    private fun generateImplementation(interfaceDeclaration: KSClassDeclaration, propertiesByBlock: Map<Int, List<KSPropertyDeclaration>>) {
        val packageName = interfaceDeclaration.packageName.asString()
        val interfaceName = interfaceDeclaration.simpleName.getShortName()
        val implClassName = "${interfaceName}Impl"

        val codeBuilder = StringBuilder()
        codeBuilder.append("package $packageName\n\n")
        codeBuilder.append("class $implClassName(override var blocks: Array<ByteArray>) : $interfaceName {\n")

        // For each block, track the current offset
        propertiesByBlock.forEach { (blockIndex, properties) ->
            var currentOffset = 0

            properties.forEach { property ->
                val propertyName = property.simpleName.getShortName()
                val propertyType = property.type.resolve().declaration.simpleName.getShortName()
                val size = typeSizeMap[propertyType] ?: run {
                    logger.error("Unsupported type $propertyType for property $propertyName")
                    return@forEach
                }

                // Generate getter
                codeBuilder.append("    override val $propertyName: $propertyType\n")
                codeBuilder.append("        get() = blocks[$blockIndex].let { block ->\n")
                when (propertyType) {
                    "UInt" -> codeBuilder.append("            block.getUIntAt($currentOffset)\n")
                    "UByte" -> codeBuilder.append("            block[$currentOffset].toUByte()\n")
                    "UShort" -> codeBuilder.append("            block.getUShortAt($currentOffset)\n")
                }
                codeBuilder.append("        }\n\n")

                // If it's a var, generate setter
                if (property.isMutable) {
                    codeBuilder.append("    override var $propertyName: $propertyType\n")
                    codeBuilder.append("        set(value) {\n")
                    when (propertyType) {
                        "UInt" -> codeBuilder.append("            blocks[$blockIndex].putUIntAt($currentOffset, value)\n")
                        "UByte" -> codeBuilder.append("            blocks[$blockIndex][$currentOffset] = value.toByte()\n")
                        "UShort" -> codeBuilder.append("            blocks[$blockIndex].putUShortAt($currentOffset, value)\n")
                    }
                    codeBuilder.append("        }\n\n")
                }

                currentOffset += size
            }
        }

        codeBuilder.append("}\n")

        // Write the generated class to file
        val file = codeGenerator.createNewFile(
            dependencies = Dependencies(true, interfaceDeclaration.containingFile!!),
            packageName = packageName,
            fileName = implClassName
        )
        OutputStreamWriter(file).use { it.write(codeBuilder.toString()) }
    }
}

// Helper extensions for ByteArray (add these to your codebase)
fun ByteArray.getUIntAt(offset: Int): UInt =
    ((this[offset].toUInt() shl 24) or
            (this[offset+1].toUInt() shl 16) or
            (this[offset+2].toUInt() shl 8) or
            this[offset+3].toUInt())

fun ByteArray.getUShortAt(offset: Int): UShort =
    ((this[offset].toUShort() shl 8) or this[offset+1].toUShort())

fun ByteArray.putUIntAt(offset: Int, value: UInt) {
    this[offset] = (value shr 24).toByte()
    this[offset+1] = (value shr 16).toByte()
    this[offset+2] = (value shr 8).toByte()
    this[offset+3] = value.toByte()
}

fun ByteArray.putUShortAt(offset: Int, value: UShort) {
    this[offset] = (value shr 8).toByte()
    this[offset+1] = value.toByte()
}

class DumpDataProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return DumpDataProcessor(environment.codeGenerator, environment.logger)
    }
}

3. Use the Annotations in Your Interface

Now you can write your simplified interface exactly as you wanted:

@DumpToClass
interface IDumpData {
    // block 0
    @Block(0)
    val id: UInt
    @Block(0)
    val crc: UByte
    //unsigned char unused_0[11];
    // block 60
    @Block(60)
    val version: UShort
    @Block(60)
    val type: UByte
    @Block(60)
    val data1: UInt
    @Block(60)
    val data2: UShort
    @Block(60)
    val data3: UShort
    @Block(60)
    val data4: UShort
    @Block(60)
    val size: UShort
    //unsigned char unused_60;
    var blocks: Array<ByteArray>// Array(64) { ByteArray(16) }
}

When you build your project, KSP will generate an IDumpDataImpl class that implements all the getters (and setters for mutable properties) with the correct offset calculations—matching your original manual implementation exactly.

Key Notes

  • Unused Fields: The processor ignores comments like //unsigned char unused_0[11];, so you can keep them in your interface for documentation purposes.
  • Type Extensibility: The current processor handles UInt, UByte, and UShort. You can extend the typeSizeMap and add cases in the getter/setter generation to support other unsigned types (like ULong) if needed.
  • Validation: The processor checks for valid symbols and logs errors if it encounters unsupported types or invalid annotations.

内容的提问来源于stack exchange,提问作者dr_begemot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 16:37:37