Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding Generator API #240

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: 8
distribution: liberica
java-version: 11
cache: gradle

- name: Build
Expand Down Expand Up @@ -68,8 +68,8 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: 8
distribution: liberica
java-version: 11
cache: gradle

- name: Install Dependencies (for Windows build)
Expand Down Expand Up @@ -124,6 +124,8 @@ jobs:
touch bin/binding.sha1
cat bin/binding.sha1 > /tmp/hash
find imgui-binding/src/main -type f -print0 | sort -z | xargs -0 sha1sum > bin/binding.sha1
find imgui-binding/src/generated -type f -print0 | sort -z | xargs -0 sha1sum >> bin/binding.sha1
find include -type f -print0 | sort -z | xargs -0 sha1sum >> bin/binding.sha1
cmp /tmp/hash bin/binding.sha1

- name: Update
Expand Down Expand Up @@ -151,8 +153,8 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: 8
distribution: liberica
java-version: 11
cache: gradle

- name: Download Artifacts
Expand Down
22 changes: 22 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import tool.generator.ast.task.GenerateAst

import java.text.SimpleDateFormat

ext {
Expand Down Expand Up @@ -26,6 +28,8 @@ allprojects {
'Build-Timestamp': new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis()),
'Build-Revision': 'git rev-parse HEAD'.execute().text.trim(),
'Build-Jdk': "${System.getProperty('java.version')} (${System.getProperty('java.vendor')})",
'Source-Compatibility': tasks.withType(JavaCompile).find().sourceCompatibility,
'Target-Compatibility': tasks.withType(JavaCompile).find().targetCompatibility,
'Created-By': "Gradle ${gradle.gradleVersion}"
)
}
Expand All @@ -45,3 +49,21 @@ tasks.register('buildAll') { t ->
}
}
}

tasks.register('generateAst', GenerateAst) {
headerFiles = [
(file('include/imgui/imgui.h')): [],
(file('include/imgui/imgui_internal.h')): [
"#define IMGUI_HAS_TABLE",
"#define IMGUI_HAS_VIEWPORT",
"#define IMGUI_HAS_DOCK",
],
(file('include/ImGuiFileDialog/ImGuiFileDialog.h')): [],
(file('include/imgui-knobs/imgui-knobs.h')): [],
(file('include/imguizmo/ImGuizmo.h')): [],
(file('include/imnodes/imnodes.h')): [],
(file('include/implot/implot.h')): [],
(file('include/imgui-node-editor/imgui_node_editor.h')): [],
(file('include/ImGuiColorTextEdit/TextEditor.h')): [],
]
}
18 changes: 0 additions & 18 deletions buildSrc/build.gradle

This file was deleted.

27 changes: 27 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
plugins {
groovy
`kotlin-dsl`
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}

repositories {
mavenCentral()
}

dependencies {
implementation(gradleApi())
implementation(localGroovy())

implementation("com.badlogicgames.gdx:gdx-jnigen:2.4.0")
implementation("org.reflections:reflections:0.10.2")
implementation("com.lordcodes.turtle:turtle:0.6.0")
implementation("fr.inria.gforge.spoon:spoon-core:10.3.0")

implementation("com.fasterxml.jackson.core:jackson-core:2.14.2")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class GenerateLibs extends DefaultTask {
private final boolean isLocal = System.properties.containsKey('local')
private final boolean withFreeType = Boolean.valueOf(System.properties.getProperty('freetype', 'false'))

private final String sourceDir = project.file('src/main/java')
private final String sourceDir = project.file('src/generated/java')
private final String classpath = project.file('build/classes/java/main')
private final String rootDir = (isLocal ? project.buildDir.path : '/tmp/imgui')
private final String jniDir = "$rootDir/jni"
Expand Down
155 changes: 155 additions & 0 deletions buildSrc/src/main/kotlin/tool/generator/api/BindingSourceProcessor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package tool.generator.api

import spoon.reflect.code.CtJavaDoc
import spoon.reflect.declaration.CtElement
import spoon.reflect.declaration.CtField
import spoon.reflect.declaration.CtMethod
import spoon.reflect.declaration.CtType

class BindingSourceProcessor(
private val type: CtType<*>
) {
companion object {
private const val BLANK_SPACE = " "

private val CLEANUP_ANNOTATIONS_REGEX = Regex(
"@" + CLEANUP_ANNOTATIONS_LIST.joinToString(separator = "|", prefix = "(", postfix = ")") + ".*"
)

private val CLEANUP_IMPORTS_REGEX = Regex(
"import.+" + CLEANUP_ANNOTATIONS_LIST.joinToString(separator = "|", prefix = "(", postfix = ")")
)

fun isProcessable(type: CtType<*>): Boolean {
return type.hasAnnotation(A_NAME_BINDING_SOURCE)
}
}

private val originalContent: String = type.position.file.readText()

fun process() {
val contentToReplace = mutableMapOf<ReplacePos, String>()
contentToReplace += collectBindingFields()
contentToReplace += collectBindingMethods()
contentToReplace += collectBindingAstEnums()

val contentBuilder = StringBuilder(originalContent)
contentToReplace.keys.sortedDescending().forEach { replacePos ->
contentBuilder.replace(replacePos.startIdx, replacePos.endIdx, contentToReplace[replacePos])
}

cleanupBindingAnnotations(contentBuilder)

type.position.file.writeText(contentBuilder.toString())
}

private fun collectBindingFields(): Map<ReplacePos, String> {
val result = mutableMapOf<ReplacePos, String>()
for (field in type.fields) {
if (field.hasAnnotation(A_NAME_BINDING_FIELD)) {
result += processBindingField(field)
}
}
return result
}

private fun collectBindingMethods(): Map<ReplacePos, String> {
val result = mutableMapOf<ReplacePos, String>()
for (method in type.methods) {
if (method.hasAnnotation(A_NAME_BINDING_METHOD)) {
result += processBindingMethod(method)
}
}
return result
}

private fun collectBindingAstEnums(): Map<ReplacePos, String> {
val result = mutableMapOf<ReplacePos, String>()
for (field in type.fields) {
if (field.hasAnnotation(A_NAME_BINDING_AST_ENUM)) {
result += processBindingAstEnum(field)
}
}
return result
}

private fun cleanupBindingAnnotations(contentBuilder: StringBuilder) {
val linesToRemove = mutableListOf<String>()
contentBuilder.lineSequence().forEach { line ->
if (line.contains(CLEANUP_IMPORTS_REGEX)) {
linesToRemove += line
}
if (line.contains(CLEANUP_ANNOTATIONS_REGEX)) {
linesToRemove += line
}
}
linesToRemove.reversed().forEach { line ->
val idx = contentBuilder.indexOf(line)
contentBuilder.delete(idx, idx + line.length + 1)
}
}

private fun processBindingMethod(method: CtMethod<*>): Map<ReplacePos, String> {
val contentToReplace = mutableMapOf<ReplacePos, String>()

// Remove the original javadoc comment from the generated content.
method.comments.find { it is CtJavaDoc }?.let {
// sourceEnd has an additional offset of 6.
// 1 - '/'
// 2 - '\n'
// 3-6 - ' '
contentToReplace[ReplacePos(it.position.sourceStart, it.position.sourceEnd + 6)] = ""
}

val content = jvmMethodContent(method) + jniMethodContent(method)
contentToReplace[method.findReplacePos()] = content.joinWithIndention()
return contentToReplace
}

private fun processBindingField(field: CtField<*>): Map<ReplacePos, String> {
val contentToReplace = mutableMapOf<ReplacePos, String>()

// Remove the original javadoc comment from the generated content.
field.comments.find { it is CtJavaDoc }?.let {
// sourceEnd has an additional offset of 6.
// 1 - '/'
// 2 - '\n'
// 3-6 - ' '
contentToReplace[ReplacePos(it.position.sourceStart, it.position.sourceEnd + 6)] = ""
}

val content = jvmFieldContent(field) + jniFieldContent(field)
contentToReplace[field.findReplacePos()] = content.joinWithIndention()
return contentToReplace
}

private fun processBindingAstEnum(field: CtField<*>): Map<ReplacePos, String> {
val content = astEnumContent(field.getAnnotation(A_NAME_BINDING_AST_ENUM)!!)
val contentStr = content.joinWithIndention()
return mapOf(field.findReplacePos() to contentStr)
}

private fun List<String>.joinWithIndention(): String {
return joinToString("\n\n") { str ->
str.lines().joinToString("\n") {
it.takeIf(String::isNotBlank)?.prependIndent(BLANK_SPACE) ?: it
}
}
}

private fun CtElement.findReplacePos(): ReplacePos {
var startIdx = annotations[0].position.sourceStart
while (originalContent[startIdx] != '\n') {
startIdx--
}
startIdx++
val endIdx = position.sourceEnd + 1
return ReplacePos(startIdx, endIdx)
}

private data class ReplacePos(val startIdx: Int, val endIdx: Int) : Comparable<ReplacePos> {
override fun compareTo(other: ReplacePos): Int {
return startIdx.compareTo(other.startIdx)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package tool.generator.api

import org.apache.commons.io.FileUtils
import spoon.reflect.declaration.CtType

class ExcludedSourceProcessor(
private val type: CtType<*>
) {
companion object {
fun isProcessable(type: CtType<*>): Boolean {
return type.hasAnnotation(A_NAME_EXCLUDED_SOURCE)
}
}

fun process() {
val sourceFile = type.position.file
val parentDir = sourceFile.parentFile
sourceFile.delete()
if (FileUtils.isEmptyDirectory(parentDir)) {
parentDir.delete()
}
}
}
60 changes: 60 additions & 0 deletions buildSrc/src/main/kotlin/tool/generator/api/ast_content.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tool.generator.api

import spoon.reflect.declaration.CtAnnotation
import spoon.reflect.declaration.ModifierKind
import tool.generator.ast.AstEnumConstantDecl
import tool.generator.ast.AstParser

fun astEnumContent(markerAnnotation: CtAnnotation<*>): List<String> {
val result = mutableListOf<String>()

val file = markerAnnotation.getValueAsString(A_VALUE_FILE)!!
val qualType = markerAnnotation.getValueAsString(A_VALUE_QUAL_TYPE)!!
val sanitizeName = markerAnnotation.getValueAsString(A_VALUE_SANITIZE_NAME)?.takeIf(String::isNotEmpty) ?: qualType
val astRoot = AstParser.readFromResource(file)

val enums = astRoot.decls.filterDecls0 {
it is AstEnumConstantDecl && it.qualType == qualType
}.map { it as AstEnumConstantDecl }

val factory = markerAnnotation.factory

enums.forEach { e ->
val f = factory.createField<Nothing>()

f.setModifiers<Nothing>(setOf(ModifierKind.PUBLIC, ModifierKind.STATIC, ModifierKind.FINAL))
f.setType<Nothing>(factory.createTypeParam("int"))
f.setSimpleName<Nothing>(e.name.replace(sanitizeName, ""))
buildString {
if (e.docComment != null) {
appendLine(e.docComment)
if (e.value != null) {
appendLine()
append("<p>")
}
}
if (e.value != null) {
append("Definition: {@code ${e.value}}")
}
}.let {
if (it.isNotEmpty()) {
f.setDocComment<Nothing>(sanitizeDocComment(it))
}
}
f.setAssignment<Nothing>(factory.createCodeSnippetExpression((e.evaluatedValue ?: e.order).toString()))

result += f.prettyprint()
}

return result
}

private fun sanitizeDocComment(doc: String): String {
return doc
.replace("*/", "* /")
.replace(" > ", "{@code >}")
.replace("< <", "{@code <<}")
.replace(" < ", "{@code <}")
.replace("->", "{@code ->}")
.replace("&", "{@code &}")
}
Loading