Add Interface synthesis to the carpenter

Currently the carpenter only generates concreate classes, add the
ability to also synthesise interfaces and have those interfaces used by
other gnerated classes
This commit is contained in:
Katelyn Baker 2017-06-22 14:31:52 +01:00
parent aaf7de0d02
commit a330360834
2 changed files with 149 additions and 17 deletions

View File

@ -71,7 +71,7 @@ class ClassCarpenter {
/**
* A Schema represents a desired class.
*/
class Schema(val name: String, fields: Map<String, Class<out Any?>>, val superclass: Schema? = null, val interfaces: List<Class<*>> = emptyList()) {
open class Schema(val name: String, fields: Map<String, Class<out Any?>>, val superclass: Schema? = null, val interfaces: List<Class<*>> = emptyList()) {
val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't.
val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap()
@ -79,6 +79,20 @@ class ClassCarpenter {
fun descriptorsIncludingSuperclasses(): Map<String, String> = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors)
}
class ClassSchema(
name: String,
fields: Map<String, Class<out Any?>>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema (name, fields, superclass, interfaces)
class InterfaceSchema(
name: String,
fields: Map<String, Class<out Any?>>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema (name, fields, superclass, interfaces)
class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.")
class InterfaceMismatch(msg: String) : RuntimeException(msg)
@ -111,11 +125,16 @@ class ClassCarpenter {
hierarchy += cursor
cursor = cursor.superclass
}
hierarchy.reversed().forEach { generateClass(it) }
hierarchy.reversed().forEach {
when (it) {
is ClassSchema -> generateClass(it)
is InterfaceSchema -> generateInterface(it)
}
}
return _loaded[schema.name]!!
}
private fun generateClass(schema: Schema): Class<*> {
private fun generateClass(schema: ClassSchema): Class<*> {
val jvmName = schema.name.jvm
// Lazy: we could compute max locals/max stack ourselves, it'd be faster.
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
@ -137,6 +156,25 @@ class ClassCarpenter {
return clazz
}
private fun generateInterface(schema: InterfaceSchema): Class<*> {
val jvmName = schema.name.jvm
// Lazy: we could compute max locals/max stack ourselves, it'd be faster.
val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
with(cw) {
val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray()
visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, jvmName, null, "java/lang/Object", interfaces)
generateAbstractGetters(schema)
visitEnd()
}
val clazz = classloader.load(schema.name, cw.toByteArray())
_loaded[schema.name] = clazz
return clazz
}
private fun ClassWriter.generateFields(schema: Schema) {
for ((name, desc) in schema.descriptors) {
visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd()
@ -202,6 +240,17 @@ class ClassCarpenter {
}
}
private fun ClassWriter.generateAbstractGetters(schema: InterfaceSchema) {
for ((name, type) in schema.fields) {
val descriptor = schema.descriptors[name]
val opcodes = ACC_ABSTRACT + ACC_PUBLIC
with (visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) {
// abstract method doesn't have any implementation so just end
visitEnd()
}
}
}
private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) {
with(visitMethod(ACC_PUBLIC, "<init>", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) {
visitCode()
@ -262,7 +311,8 @@ class ClassCarpenter {
method.name.startsWith("get") -> method.name.substring(3).decapitalize()
else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}")
}
if (fieldNameFromItf !in allFields)
if ((schema is ClassSchema) and (fieldNameFromItf !in allFields))
throw InterfaceMismatch("Interface ${itf.name} requires a field named ${fieldNameFromItf} but that isn't found in the schema or any superclass schemas")
}
}

View File

@ -21,7 +21,7 @@ class ClassCarpenterTest {
@Test
fun empty() {
val clazz = cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null))
val clazz = cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null))
assertEquals(0, clazz.nonSyntheticFields.size)
assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString
assertEquals(0, clazz.declaredConstructors[0].parameterCount)
@ -30,7 +30,7 @@ class ClassCarpenterTest {
@Test
fun prims() {
val clazz = cc.build(ClassCarpenter.Schema("gen.Prims", mapOf(
val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Prims", mapOf(
"anIntField" to Int::class.javaPrimitiveType!!,
"aLongField" to Long::class.javaPrimitiveType!!,
"someCharField" to Char::class.javaPrimitiveType!!,
@ -65,7 +65,7 @@ class ClassCarpenterTest {
}
private fun genPerson(): Pair<Class<*>, Any> {
val clazz = cc.build(ClassCarpenter.Schema("gen.Person", mapOf(
val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Person", mapOf(
"age" to Int::class.javaPrimitiveType!!,
"name" to String::class.java
)))
@ -88,14 +88,14 @@ class ClassCarpenterTest {
@Test(expected = ClassCarpenter.DuplicateName::class)
fun duplicates() {
cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null))
cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null))
cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null))
cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null))
}
@Test
fun `can refer to each other`() {
val (clazz1, i) = genPerson()
val clazz2 = cc.build(ClassCarpenter.Schema("gen.Referee", mapOf(
val clazz2 = cc.build(ClassCarpenter.ClassSchema("gen.Referee", mapOf(
"ref" to clazz1
)))
val i2 = clazz2.constructors[0].newInstance(i)
@ -104,8 +104,8 @@ class ClassCarpenterTest {
@Test
fun superclasses() {
val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to String::class.java), schema1)
val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to String::class.java), schema1)
val clazz = cc.build(schema2)
val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess
assertEquals("xa", i["a"])
@ -115,8 +115,8 @@ class ClassCarpenterTest {
@Test
fun interfaces() {
val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java))
val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java))
val clazz = cc.build(schema2)
val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface
assertEquals("xa", i.a)
@ -125,10 +125,92 @@ class ClassCarpenterTest {
@Test(expected = ClassCarpenter.InterfaceMismatch::class)
fun `mismatched interface`() {
val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.Schema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java))
val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java))
val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java))
val clazz = cc.build(schema2)
val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface
assertEquals(1, i.b)
}
}
@Test
fun `generate interface`() {
val schema1 = ClassCarpenter.InterfaceSchema("gen.Interface", mapOf("a" to Int::class.java))
val iface = cc.build(schema1)
assert (iface.isInterface())
assert (iface.constructors.isEmpty())
assertEquals (iface.declaredMethods.size, 1)
assertEquals (iface.declaredMethods[0].name, "getA")
val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf (iface))
val clazz = cc.build(schema2)
val testA = 42
val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess
assertEquals(testA, i["a"])
}
@Test
fun `generate multiple interfaces`() {
val iFace1 = ClassCarpenter.InterfaceSchema("gen.Interface1", mapOf("a" to Int::class.java, "b" to String::class.java))
val iFace2 = ClassCarpenter.InterfaceSchema("gen.Interface2", mapOf("c" to Int::class.java, "d" to String::class.java))
val class1 = ClassCarpenter.ClassSchema(
"gen.Derived",
mapOf(
"a" to Int::class.java,
"b" to String::class.java,
"c" to Int::class.java,
"d" to String::class.java),
interfaces = listOf (cc.build (iFace1), cc.build (iFace2)))
val clazz = cc.build(class1)
val testA = 42
val testB = "don't touch me, I'm scared"
val testC = 0xDEAD
val testD = "wibble"
val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess
assertEquals(testA, i["a"])
assertEquals(testB, i["b"])
assertEquals(testC, i["c"])
assertEquals(testD, i["d"])
}
@Test
fun `interface implementing interface`() {
val iFace1 = ClassCarpenter.InterfaceSchema(
"gen.Interface1",
mapOf (
"a" to Int::class.java,
"b" to String::class.java))
val iFace2 = ClassCarpenter.InterfaceSchema(
"gen.Interface2",
mapOf(
"c" to Int::class.java,
"d" to String::class.java),
interfaces = listOf (cc.build (iFace1)))
val class1 = ClassCarpenter.ClassSchema(
"gen.Derived",
mapOf(
"a" to Int::class.java,
"b" to String::class.java,
"c" to Int::class.java,
"d" to String::class.java),
interfaces = listOf (cc.build (iFace2)))
val clazz = cc.build(class1)
val testA = 99
val testB = "green is not a creative colour"
val testC = 7
val testD = "I like jam"
val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess
assertEquals(testA, i["a"])
assertEquals(testB, i["b"])
assertEquals(testC, i["c"])
assertEquals(testD, i["d"])
}
}