如何创建Kotlin注解类,基于字段位置与类型自动生成Getter方法
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
typeSizeMapand 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

